diff options
Diffstat (limited to 'includes')
541 files changed, 41910 insertions, 16311 deletions
diff --git a/includes/Action.php b/includes/Action.php index 37c48488..51922251 100644 --- a/includes/Action.php +++ b/includes/Action.php @@ -1,15 +1,6 @@ <?php /** - * @defgroup Actions Action done on pages - */ - -/** - * 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 - * + * Base classes for actions done on pages. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,23 +18,39 @@ * * @file */ + +/** + * @defgroup Actions Action done on pages + */ + +/** + * 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 + * + * 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). The FormAction and FormlessAction classes respresent these two groups. + */ abstract class Action { /** * Page on which we're performing the action - * @var Page + * @var Page $page */ protected $page; /** * IContextSource if specified; otherwise we'll use the Context from the Page - * @var IContextSource + * @var IContextSource $context */ protected $context; /** * The fields used to create the HTMLForm - * @var Array + * @var Array $fields */ protected $fields; @@ -78,7 +85,7 @@ abstract class Action { * @param $action String * @param $page Page * @param $context IContextSource - * @return Action|false|null false if the action is disabled, null + * @return Action|bool|null false if the action is disabled, null * if it is not recognised */ public final static function factory( $action, Page $page, IContextSource $context = null ) { @@ -128,7 +135,7 @@ abstract class Action { if ( !$context->canUseWikiPage() ) { return 'view'; } - + $action = Action::factory( $actionName, $context->getWikiPage() ); if ( $action instanceof Action ) { return $action->getName(); @@ -266,6 +273,7 @@ abstract class Action { * * @param $user User: the user to check, or null to use the context user * @throws ErrorPageError + * @return bool True on success */ protected function checkCanExecute( User $user ) { $right = $this->getRestriction(); @@ -277,7 +285,7 @@ abstract class Action { } if ( $this->requiresUnblock() && $user->isBlocked() ) { - $block = $user->mBlock; + $block = $user->getBlock(); throw new UserBlockedError( $block ); } @@ -287,6 +295,7 @@ abstract class Action { if ( $this->requiresWrite() && wfReadOnly() ) { throw new ReadOnlyError(); } + return true; } /** @@ -332,7 +341,7 @@ abstract class Action { * @return String */ protected function getDescription() { - return wfMsgHtml( strtolower( $this->getName() ) ); + return $this->msg( strtolower( $this->getName() ) )->escaped(); } /** @@ -350,6 +359,9 @@ abstract class Action { public abstract function execute(); } +/** + * An action which shows a form and does something based on the input from the form + */ abstract class FormAction extends Action { /** @@ -385,7 +397,7 @@ abstract class FormAction extends Action { // 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 = new HTMLForm( $this->fields, $this->getContext(), $this->getName() ); $form->setSubmitCallback( array( $this, 'onSubmit' ) ); // Retain query parameters (uselang etc) @@ -444,8 +456,8 @@ abstract class FormAction extends Action { /** * @see Action::execute() * @throws ErrorPageError - * @param array|null $data - * @param bool $captureErrors + * @param $data array|null + * @param $captureErrors bool * @return bool */ public function execute( array $data = null, $captureErrors = true ) { @@ -486,9 +498,7 @@ abstract class FormAction extends Action { } /** - * 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). + * An action which just does something, without showing a form first. */ abstract class FormlessAction extends Action { @@ -501,15 +511,23 @@ abstract class FormlessAction extends Action { /** * We don't want an HTMLForm + * @return bool */ protected function getFormFields() { return false; } + /** + * @param $data Array + * @return bool + */ public function onSubmit( $data ) { return false; } + /** + * @return bool + */ public function onSuccess() { return false; } diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index 5bc9f067..b00cf309 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -1,10 +1,28 @@ <?php /** - * @defgroup Ajax Ajax + * Handle ajax requests and send them to the proper handler. + * + * 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 Ajax - * Handle ajax requests and send them to the proper handler. + */ + +/** + * @defgroup Ajax Ajax */ /** @@ -12,16 +30,26 @@ * @ingroup Ajax */ class AjaxDispatcher { - /** The way the request was made, either a 'get' or a 'post' */ + /** + * The way the request was made, either a 'get' or a 'post' + * @var string $mode + */ private $mode; - /** Name of the requested handler */ + /** + * Name of the requested handler + * @var string $func_name + */ private $func_name; - /** Arguments passed */ + /** Arguments passed + * @var array $args + */ private $args; - /** Load up our object with user supplied data */ + /** + * Load up our object with user supplied data + */ function __construct() { wfProfileIn( __METHOD__ ); @@ -62,13 +90,14 @@ class AjaxDispatcher { wfProfileOut( __METHOD__ ); } - /** Pass the request to our internal function. + /** + * Pass the request to our internal function. * BEWARE! Data are passed as they have been supplied by the user, * they should be carefully handled in the function processing the * request. */ function performAction() { - global $wgAjaxExportList, $wgOut, $wgUser; + global $wgAjaxExportList, $wgUser; if ( empty( $this->mode ) ) { return; @@ -84,7 +113,7 @@ class AjaxDispatcher { 'Bad Request', "unknown function " . (string) $this->func_name ); - } elseif ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) + } elseif ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) && !$wgUser->isAllowed( 'read' ) ) { wfHttpError( @@ -94,14 +123,8 @@ class AjaxDispatcher { } else { wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" ); - if ( strpos( $this->func_name, '::' ) !== false ) { - $func = explode( '::', $this->func_name, 2 ); - } else { - $func = $this->func_name; - } - try { - $result = call_user_func_array( $func, $this->args ); + $result = call_user_func_array( $this->func_name, $this->args ); if ( $result === false || $result === null ) { wfDebug( __METHOD__ . ' ERROR while dispatching ' @@ -134,7 +157,6 @@ class AjaxDispatcher { } } - $wgOut = null; wfProfileOut( __METHOD__ ); } } diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index e60ca23c..6bf94ccb 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -1,6 +1,21 @@ <?php /** - * Response handler for Ajax requests + * Response handler for Ajax requests. + * + * 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 Ajax @@ -13,27 +28,52 @@ * @ingroup Ajax */ class AjaxResponse { - /** Number of seconds to get the response cached by a proxy */ + + /** + * Number of seconds to get the response cached by a proxy + * @var int $mCacheDuration + */ private $mCacheDuration; - /** HTTP header Content-Type */ + /** + * HTTP header Content-Type + * @var string $mContentType + */ private $mContentType; - /** Disables output. Can be set by calling $AjaxResponse->disable() */ + /** + * Disables output. Can be set by calling $AjaxResponse->disable() + * @var bool $mDisabled + */ private $mDisabled; - /** Date for the HTTP header Last-modified */ + /** + * Date for the HTTP header Last-modified + * @var string|false $mLastModified + */ private $mLastModified; - /** HTTP response code */ + /** + * HTTP response code + * @var string $mResponseCode + */ private $mResponseCode; - /** HTTP Vary header */ + /** + * HTTP Vary header + * @var string $mVary + */ private $mVary; - /** Content of our HTTP response */ + /** + * Content of our HTTP response + * @var string $mText + */ private $mText; + /** + * @param $text string|null + */ function __construct( $text = null ) { $this->mCacheDuration = null; $this->mVary = null; @@ -49,41 +89,67 @@ class AjaxResponse { } } + /** + * Set the number of seconds to get the response cached by a proxy + * @param $duration int + */ function setCacheDuration( $duration ) { $this->mCacheDuration = $duration; } + /** + * Set the HTTP Vary header + * @param $vary string + */ function setVary( $vary ) { $this->mVary = $vary; } + /** + * Set the HTTP response code + * @param $code string + */ function setResponseCode( $code ) { $this->mResponseCode = $code; } + /** + * Set the HTTP header Content-Type + * @param $type string + */ function setContentType( $type ) { $this->mContentType = $type; } + /** + * Disable output. + */ function disable() { $this->mDisabled = true; } - /** Add content to the response */ + /** + * Add content to the response + * @param $text string + */ function addText( $text ) { if ( ! $this->mDisabled && $text ) { $this->mText .= $text; } } - /** Output text */ + /** + * Output text + */ function printText() { if ( ! $this->mDisabled ) { print $this->mText; } } - /** Construct the header and output it */ + /** + * Construct the header and output it + */ function sendHeaders() { global $wgUseSquid, $wgUseESI; @@ -139,8 +205,10 @@ class AjaxResponse { /** * checkLastModified tells the client to use the client-cached response if * possible. If sucessful, the AjaxResponse is disabled so that - * any future call to AjaxResponse::printText() have no effect. The method - * returns true iff the response code was set to 304 Not Modified. + * any future call to AjaxResponse::printText() have no effect. + * + * @param $timestamp string + * @return bool Returns true if the response code was set to 304 Not Modified. */ function checkLastModified ( $timestamp ) { global $wgCachePages, $wgCacheEpoch, $wgUser; @@ -148,21 +216,21 @@ class AjaxResponse { if ( !$timestamp || $timestamp == '19700101000000' ) { wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" ); - return; + return false; } if ( !$wgCachePages ) { wfDebug( "$fname: CACHE DISABLED\n", false ); - return; + return false; } if ( $wgUser->getOption( 'nocache' ) ) { wfDebug( "$fname: USER DISABLED CACHE\n", false ); - return; + return false; } $timestamp = wfTimestamp( TS_MW, $timestamp ); - $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) ); + $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->getTouched(), $wgCacheEpoch ) ); if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { # IE sends sizes after the date like this: @@ -191,11 +259,12 @@ class AjaxResponse { wfDebug( "$fname: client did not send If-Modified-Since header\n", false ); $this->mLastModified = $lastmod; } + return false; } /** - * @param $mckey - * @param $touched + * @param $mckey string + * @param $touched int * @return bool */ function loadFromMemcached( $mckey, $touched ) { @@ -222,7 +291,7 @@ class AjaxResponse { } /** - * @param $mckey + * @param $mckey string * @param $expiry int * @return bool */ diff --git a/includes/Article.php b/includes/Article.php index b07f309c..9ab4b6ba 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -1,6 +1,22 @@ <?php /** - * File for articles + * User interface for page actions. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file */ @@ -9,7 +25,7 @@ * * This maintains WikiPage functions for backwards compatibility. * - * @TODO: move and rewrite code to an Action class + * @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 @@ -23,42 +39,69 @@ class Article extends Page { */ /** - * @var IContextSource + * The context this Article is executed in + * @var IContextSource $mContext */ protected $mContext; /** - * @var WikiPage + * The WikiPage object of this instance + * @var WikiPage $mPage */ protected $mPage; /** - * @var ParserOptions: ParserOptions object for $wgUser articles + * ParserOptions object for $wgUser articles + * @var ParserOptions $mParserOptions */ public $mParserOptions; + /** + * Content of the revision we are working on + * @var string $mContent + */ var $mContent; // !< + + /** + * Is the content ($mContent) already loaded? + * @var bool $mContentLoaded + */ var $mContentLoaded = false; // !< + + /** + * The oldid of the article that is to be shown, 0 for the + * current revision + * @var int|null $mOldId + */ var $mOldId; // !< /** - * @var Title + * Title from which we were redirected here + * @var Title $mRedirectedFrom */ var $mRedirectedFrom = null; /** - * @var mixed: boolean false or URL string + * URL to redirect to or false if none + * @var string|false $mRedirectUrl */ var $mRedirectUrl = false; // !< + + /** + * Revision ID of revision we are working on + * @var int $mRevIdFetched + */ var $mRevIdFetched = 0; // !< /** - * @var Revision + * Revision we are working on + * @var Revision $mRevision */ var $mRevision = null; /** - * @var ParserOutput + * ParserOutput object + * @var ParserOutput $mParserOutput */ var $mParserOutput; @@ -188,11 +231,9 @@ class Article extends Page { * This function has side effects! Do not use this function if you * only want the real revision text if any. * - * @return Return the text of this revision + * @return string Return the text of this revision */ public function getContent() { - global $wgUser; - wfProfileIn( __METHOD__ ); if ( $this->mPage->getID() === 0 ) { @@ -204,7 +245,8 @@ class Article extends Page { $text = ''; } } else { - $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' ); + $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; + $text = wfMessage( $message )->text(); } wfProfileOut( __METHOD__ ); @@ -235,11 +277,10 @@ class Article extends Page { * @return int The old id for the request */ public function getOldIDFromRequest() { - global $wgRequest; - $this->mRedirectUrl = false; - $oldid = $wgRequest->getIntOrNull( 'oldid' ); + $request = $this->getContext()->getRequest(); + $oldid = $request->getIntOrNull( 'oldid' ); if ( $oldid === null ) { return 0; @@ -248,17 +289,21 @@ class Article extends Page { if ( $oldid !== 0 ) { # Load the given revision and check whether the page is another one. # In that case, update this instance to reflect the change. - $this->mRevision = Revision::newFromId( $oldid ); - if ( $this->mRevision !== null ) { - // Revision title doesn't match the page title given? - if ( $this->mPage->getID() != $this->mRevision->getPage() ) { - $function = array( get_class( $this->mPage ), 'newFromID' ); - $this->mPage = call_user_func( $function, $this->mRevision->getPage() ); + if ( $oldid === $this->mPage->getLatest() ) { + $this->mRevision = $this->mPage->getRevision(); + } else { + $this->mRevision = Revision::newFromId( $oldid ); + if ( $this->mRevision !== null ) { + // Revision title doesn't match the page title given? + if ( $this->mPage->getID() != $this->mRevision->getPage() ) { + $function = array( get_class( $this->mPage ), 'newFromID' ); + $this->mPage = call_user_func( $function, $this->mRevision->getPage() ); + } } } } - if ( $wgRequest->getVal( 'direction' ) == 'next' ) { + if ( $request->getVal( 'direction' ) == 'next' ) { $nextid = $this->getTitle()->getNextRevisionID( $oldid ); if ( $nextid ) { $oldid = $nextid; @@ -266,7 +311,7 @@ class Article extends Page { } else { $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); } - } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { + } elseif ( $request->getVal( 'direction' ) == 'prev' ) { $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); if ( $previd ) { $oldid = $previd; @@ -306,9 +351,7 @@ class Article extends Page { # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. - $t = $this->getTitle()->getPrefixedText(); - $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : ''; - $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ; + $this->mContent = wfMessage( 'missing-revision', $oldid )->plain(); if ( $oldid ) { # $this->mRevision might already be fetched by getOldIDFromRequest() @@ -400,8 +443,7 @@ class Article extends Page { * page of the given title. */ public function view() { - global $wgUser, $wgOut, $wgRequest, $wgParser; - global $wgUseFileCache, $wgUseETag, $wgDebugToolbar; + global $wgParser, $wgUseFileCache, $wgUseETag, $wgDebugToolbar; wfProfileIn( __METHOD__ ); @@ -411,17 +453,19 @@ class Article extends Page { # the first call of this method even if $oldid is used way below. $oldid = $this->getOldID(); + $user = $this->getContext()->getUser(); # Another whitelist check in case getOldID() is altering the title - $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $wgUser ); + $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $user ); if ( count( $permErrors ) ) { wfDebug( __METHOD__ . ": denied on secondary read check\n" ); wfProfileOut( __METHOD__ ); throw new PermissionsError( 'read', $permErrors ); } + $outputPage = $this->getContext()->getOutput(); # getOldID() may as well want us to redirect somewhere else if ( $this->mRedirectUrl ) { - $wgOut->redirect( $this->mRedirectUrl ); + $outputPage->redirect( $this->mRedirectUrl ); wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); wfProfileOut( __METHOD__ ); @@ -429,7 +473,7 @@ class Article extends Page { } # If we got diff in the query, we want to see a diff page instead of the article. - if ( $wgRequest->getCheck( 'diff' ) ) { + if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) { wfDebug( __METHOD__ . ": showing diff page\n" ); $this->showDiffPage(); wfProfileOut( __METHOD__ ); @@ -438,31 +482,31 @@ class Article extends Page { } # Set page title (may be overridden by DISPLAYTITLE) - $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); + $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() ); - $wgOut->setArticleFlag( true ); + $outputPage->setArticleFlag( true ); # Allow frames by default - $wgOut->allowClickjacking(); + $outputPage->allowClickjacking(); $parserCache = ParserCache::singleton(); $parserOptions = $this->getParserOptions(); # Render printable version, use printable version cache - if ( $wgOut->isPrintable() ) { + if ( $outputPage->isPrintable() ) { $parserOptions->setIsPrintable( true ); $parserOptions->setEditSection( false ); - } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit' ) ) { + } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) { $parserOptions->setEditSection( false ); } # Try client and file cache if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) { if ( $wgUseETag ) { - $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) ); + $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) ); } # Is it client cached? - if ( $wgOut->checkLastModified( $this->mPage->getTouched() ) ) { + if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) { wfDebug( __METHOD__ . ": done 304\n" ); wfProfileOut( __METHOD__ ); @@ -471,8 +515,8 @@ class Article extends Page { } elseif ( $wgUseFileCache && $this->tryFileCache() ) { wfDebug( __METHOD__ . ": done file cache\n" ); # tell wgOut that output is taken care of - $wgOut->disable(); - $this->mPage->doViewUpdates( $wgUser ); + $outputPage->disable(); + $this->mPage->doViewUpdates( $user ); wfProfileOut( __METHOD__ ); return; @@ -482,7 +526,7 @@ class Article extends Page { # Should the parser cache be used? $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid ); wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); - if ( $wgUser->getStubThreshold() ) { + if ( $user->getStubThreshold() ) { wfIncrStats( 'pcache_miss_stub' ); } @@ -520,14 +564,14 @@ class Article extends Page { } else { wfDebug( __METHOD__ . ": showing parser cache contents\n" ); } - $wgOut->addParserOutput( $this->mParserOutput ); + $outputPage->addParserOutput( $this->mParserOutput ); # Ensure that UI elements requiring revision ID have # the correct version information. - $wgOut->setRevisionId( $this->mPage->getLatest() ); + $outputPage->setRevisionId( $this->mPage->getLatest() ); # Preload timestamp to avoid a DB hit $cachedTimestamp = $this->mParserOutput->getTimestamp(); if ( $cachedTimestamp !== null ) { - $wgOut->setRevisionTimestamp( $cachedTimestamp ); + $outputPage->setRevisionTimestamp( $cachedTimestamp ); $this->mPage->setTimestamp( $cachedTimestamp ); } $outputDone = true; @@ -551,16 +595,16 @@ class Article extends Page { # Ensure that UI elements requiring revision ID have # the correct version information. - $wgOut->setRevisionId( $this->getRevIdFetched() ); + $outputPage->setRevisionId( $this->getRevIdFetched() ); # Preload timestamp to avoid a DB hit - $wgOut->setRevisionTimestamp( $this->getTimestamp() ); + $outputPage->setRevisionTimestamp( $this->getTimestamp() ); # Pages containing custom CSS or JavaScript get special treatment if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) { wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); $this->showCssOrJsPage(); $outputDone = true; - } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) { + } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) { # Allow extensions do their own custom view for certain pages $outputDone = true; } else { @@ -569,10 +613,10 @@ class Article extends Page { if ( $rt ) { wfDebug( __METHOD__ . ": showing redirect=no page\n" ); # Viewing a redirect page (e.g. with parameter redirect=no) - $wgOut->addHTML( $this->viewRedirect( $rt ) ); + $outputPage->addHTML( $this->viewRedirect( $rt ) ); # Parse just to get categories, displaytitle, etc. $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions ); - $wgOut->addParserOutputNoText( $this->mParserOutput ); + $outputPage->addParserOutputNoText( $this->mParserOutput ); $outputDone = true; } } @@ -587,12 +631,12 @@ class Article extends Page { if ( !$poolArticleView->execute() ) { $error = $poolArticleView->getError(); if ( $error ) { - $wgOut->clearHTML(); // for release() errors - $wgOut->enableClientCache( false ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $outputPage->clearHTML(); // for release() errors + $outputPage->enableClientCache( false ); + $outputPage->setRobotPolicy( 'noindex,nofollow' ); $errortext = $error->getWikiText( false, 'view-pool-error' ); - $wgOut->addWikiText( '<div class="errorbox">' . $errortext . '</div>' ); + $outputPage->addWikiText( '<div class="errorbox">' . $errortext . '</div>' ); } # Connection or timeout error wfProfileOut( __METHOD__ ); @@ -600,12 +644,12 @@ class Article extends Page { } $this->mParserOutput = $poolArticleView->getParserOutput(); - $wgOut->addParserOutput( $this->mParserOutput ); + $outputPage->addParserOutput( $this->mParserOutput ); # Don't cache a dirty ParserOutput object if ( $poolArticleView->getIsDirty() ) { - $wgOut->setSquidMaxage( 0 ); - $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" ); + $outputPage->setSquidMaxage( 0 ); + $outputPage->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" ); } $outputDone = true; @@ -634,17 +678,17 @@ class Article extends Page { if ( $this->getTitle()->isMainPage() ) { $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage(); if ( !$msg->isDisabled() ) { - $wgOut->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); + $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); } } # Check for any __NOINDEX__ tags on the page using $pOutput $policy = $this->getRobotPolicy( 'view', $pOutput ); - $wgOut->setIndexPolicy( $policy['index'] ); - $wgOut->setFollowPolicy( $policy['follow'] ); + $outputPage->setIndexPolicy( $policy['index'] ); + $outputPage->setFollowPolicy( $policy['follow'] ); $this->showViewFooter(); - $this->mPage->doViewUpdates( $wgUser ); + $this->mPage->doViewUpdates( $user ); wfProfileOut( __METHOD__ ); } @@ -654,11 +698,10 @@ class Article extends Page { * @param $pOutput ParserOutput */ public function adjustDisplayTitle( ParserOutput $pOutput ) { - global $wgOut; # Adjust the title if it was set by displaytitle, -{T|}- or language conversion $titleText = $pOutput->getTitleText(); if ( strval( $titleText ) !== '' ) { - $wgOut->setPageTitle( $titleText ); + $this->getContext()->getOutput()->setPageTitle( $titleText ); } } @@ -667,13 +710,13 @@ class Article extends Page { * Article::view() only, other callers should use the DifferenceEngine class. */ public function showDiffPage() { - global $wgRequest, $wgUser; - - $diff = $wgRequest->getVal( 'diff' ); - $rcid = $wgRequest->getVal( 'rcid' ); - $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); - $purge = $wgRequest->getVal( 'action' ) == 'purge'; - $unhide = $wgRequest->getInt( 'unhide' ) == 1; + $request = $this->getContext()->getRequest(); + $user = $this->getContext()->getUser(); + $diff = $request->getVal( 'diff' ); + $rcid = $request->getVal( 'rcid' ); + $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) ); + $purge = $request->getVal( 'action' ) == 'purge'; + $unhide = $request->getInt( 'unhide' ) == 1; $oldid = $this->getOldID(); $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide ); @@ -683,7 +726,7 @@ class Article extends Page { if ( $diff == 0 || $diff == $this->mPage->getLatest() ) { # Run view updates for current revision only - $this->mPage->doViewUpdates( $wgUser ); + $this->mPage->doViewUpdates( $user ); } } @@ -695,22 +738,21 @@ class Article extends Page { * page views. */ protected function showCssOrJsPage() { - global $wgOut; - $dir = $this->getContext()->getLanguage()->getDir(); $lang = $this->getContext()->getLanguage()->getCode(); - $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>", + $outputPage = $this->getContext()->getOutput(); + $outputPage->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->getTitle(), $wgOut ) ) ) { + if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) { // Wrap the whole lot in a <pre> and don't parse $m = array(); 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" ); + $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); + $outputPage->addHTML( htmlspecialchars( $this->mContent ) ); + $outputPage->addHTML( "\n</pre>\n" ); } } @@ -722,8 +764,7 @@ class Article extends Page { * TODO: actions other than 'view' */ public function getRobotPolicy( $action, $pOutput ) { - global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies; - global $wgDefaultRobotPolicy, $wgRequest; + global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy; $ns = $this->getTitle()->getNamespace(); @@ -745,13 +786,13 @@ class Article extends Page { 'index' => 'noindex', 'follow' => 'nofollow' ); - } elseif ( $wgOut->isPrintable() ) { + } elseif ( $this->getContext()->getOutput()->isPrintable() ) { # Discourage indexing of printable versions, but encourage following return array( 'index' => 'noindex', 'follow' => 'follow' ); - } elseif ( $wgRequest->getInt( 'curid' ) ) { + } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) { # For ?curid=x urls, disallow indexing return array( 'index' => 'noindex', @@ -794,7 +835,7 @@ class Article extends Page { * merging of several policies using array_merge(). * @param $policy Mixed, returns empty array on null/false/'', transparent * to already-converted arrays, converts String. - * @return Array: 'index' => <indexpolicy>, 'follow' => <followpolicy> + * @return Array: 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\> */ public static function formatRobotPolicy( $policy ) { if ( is_array( $policy ) ) { @@ -820,15 +861,16 @@ class Article extends Page { /** * If this request is a redirect view, send "redirected from" subtitle to - * $wgOut. Returns true if the header was needed, false if this is not a - * redirect view. Handles both local and remote redirects. + * the output. Returns true if the header was needed, false if this is not + * a redirect view. Handles both local and remote redirects. * * @return boolean */ public function showRedirectedFromHeader() { - global $wgOut, $wgRequest, $wgRedirectSources; + global $wgRedirectSources; + $outputPage = $this->getContext()->getOutput(); - $rdfrom = $wgRequest->getVal( 'rdfrom' ); + $rdfrom = $this->getContext()->getRequest()->getVal( 'rdfrom' ); if ( isset( $this->mRedirectedFrom ) ) { // This is an internally redirected page view. @@ -841,21 +883,21 @@ class Article extends Page { array( 'redirect' => 'no' ) ); - $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); + $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); // Set the fragment if one was specified in the redirect if ( strval( $this->getTitle()->getFragment() ) != '' ) { $fragment = Xml::escapeJsString( $this->getTitle()->getFragmentForURL() ); - $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); + $outputPage->addInlineScript( "redirectToFragment(\"$fragment\");" ); } // Add a <link rel="canonical"> tag - $wgOut->addLink( array( 'rel' => 'canonical', + $outputPage->addLink( array( 'rel' => 'canonical', 'href' => $this->getTitle()->getLocalURL() ) ); - // Tell $wgOut the user arrived at this article through a redirect - $wgOut->setRedirectedFrom( $this->mRedirectedFrom ); + // Tell the output object that the user arrived at this article through a redirect + $outputPage->setRedirectedFrom( $this->mRedirectedFrom ); return true; } @@ -864,7 +906,7 @@ class Article extends Page { // If it was reported from a trusted site, supply a backlink. if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { $redir = Linker::makeExternalLink( $rdfrom, $rdfrom ); - $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); + $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) ); return true; } @@ -878,11 +920,9 @@ class Article extends Page { * [[MediaWiki:Talkpagetext]]. For Article::view(). */ public function showNamespaceHeader() { - global $wgOut; - if ( $this->getTitle()->isTalkPage() ) { if ( !wfMessage( 'talkpageheader' )->isDisabled() ) { - $wgOut->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) ); + $this->getContext()->getOutput()->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) ); } } } @@ -891,11 +931,9 @@ class Article extends Page { * Show the footer section of an ordinary page view */ public function showViewFooter() { - global $wgOut; - # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page if ( $this->getTitle()->getNamespace() == NS_USER_TALK && IP::isValid( $this->getTitle()->getText() ) ) { - $wgOut->addWikiMsg( 'anontalkpagetext' ); + $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' ); } # If we have been passed an &rcid= parameter, we want to give the user a @@ -912,33 +950,32 @@ class Article extends Page { * desired, does nothing. */ public function showPatrolFooter() { - global $wgOut, $wgRequest, $wgUser; - - $rcid = $wgRequest->getVal( 'rcid' ); + $request = $this->getContext()->getRequest(); + $outputPage = $this->getContext()->getOutput(); + $user = $this->getContext()->getUser(); + $rcid = $request->getVal( 'rcid' ); - if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol' ) ) { + if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol', $user ) ) { return; } - $token = $wgUser->getEditToken( $rcid ); - $wgOut->preventClickjacking(); + $token = $user->getEditToken( $rcid ); + $outputPage->preventClickjacking(); - $wgOut->addHTML( + $link = Linker::linkKnown( + $this->getTitle(), + wfMessage( 'markaspatrolledtext' )->escaped(), + array(), + array( + 'action' => 'markpatrolled', + 'rcid' => $rcid, + 'token' => $token, + ) + ); + + $outputPage->addHTML( "<div class='patrollink'>" . - wfMsgHtml( - 'markaspatrolledlink', - Linker::link( - $this->getTitle(), - wfMsgHtml( 'markaspatrolledtext' ), - array(), - array( - 'action' => 'markpatrolled', - 'rcid' => $rcid, - 'token' => $token, - ), - array( 'known', 'noclasses' ) - ) - ) . + wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() . '</div>' ); } @@ -948,7 +985,8 @@ class Article extends Page { * namespace, show the default message text. To be called from Article::view(). */ public function showMissingArticle() { - global $wgOut, $wgRequest, $wgUser, $wgSend404Code; + global $wgSend404Code; + $outputPage = $this->getContext()->getOutput(); # Show info in user (talk) namespace. Does the user exist? Is he blocked? if ( $this->getTitle()->getNamespace() == NS_USER || $this->getTitle()->getNamespace() == NS_USER_TALK ) { @@ -958,13 +996,13 @@ class Article extends Page { $ip = User::isIP( $rootPart ); if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist - $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", + $outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) ); } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract( - $wgOut, + $outputPage, 'block', - $user->getUserPage()->getPrefixedText(), + $user->getUserPage(), '', array( 'lim' => 1, @@ -981,7 +1019,7 @@ class Article extends Page { wfRunHooks( 'ShowMissingArticle', array( $this ) ); # Show delete and move logs - LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->getTitle()->getPrefixedText(), '', + LogEventsList::showLogExtract( $outputPage, array( 'delete', 'move' ), $this->getTitle(), '', array( 'lim' => 10, 'conds' => array( "log_action != 'revision'" ), 'showIfEmpty' => false, @@ -991,7 +1029,7 @@ class Article extends Page { if ( !$this->mPage->hasViewableContent() && $wgSend404Code ) { // 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" ); + $this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" ); } $hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) ); @@ -1003,56 +1041,50 @@ class Article extends Page { # Show error message $oldid = $this->getOldID(); if ( $oldid ) { - $text = wfMsgNoTrans( 'missing-article', - $this->getTitle()->getPrefixedText(), - wfMsgNoTrans( 'missingarticle-rev', $oldid ) ); + $text = wfMessage( 'missing-revision', $oldid )->plain(); } elseif ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) { // Use the default message text $text = $this->getTitle()->getDefaultMessageText(); + } elseif ( $this->getTitle()->quickUserCan( 'create', $this->getContext()->getUser() ) + && $this->getTitle()->quickUserCan( 'edit', $this->getContext()->getUser() ) + ) { + $text = wfMessage( 'noarticletext' )->plain(); } else { - $createErrors = $this->getTitle()->getUserPermissionsErrors( 'create', $wgUser ); - $editErrors = $this->getTitle()->getUserPermissionsErrors( 'edit', $wgUser ); - $errors = array_merge( $createErrors, $editErrors ); - - if ( !count( $errors ) ) { - $text = wfMsgNoTrans( 'noarticletext' ); - } else { - $text = wfMsgNoTrans( 'noarticletext-nopermission' ); - } + $text = wfMessage( 'noarticletext-nopermission' )->plain(); } $text = "<div class='noarticletext'>\n$text\n</div>"; - $wgOut->addWikiText( $text ); + $outputPage->addWikiText( $text ); } /** * If the revision requested for view is deleted, check permissions. - * Send either an error message or a warning header to $wgOut. + * Send either an error message or a warning header to the output. * * @return boolean true if the view is allowed, false if not. */ public function showDeletedRevisionHeader() { - global $wgOut, $wgRequest; - if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { // Not deleted return true; } + $outputPage = $this->getContext()->getOutput(); + $user = $this->getContext()->getUser(); // If the user is not allowed to see it... - if ( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + if ( !$this->mRevision->userCan( Revision::DELETED_TEXT, $user ) ) { + $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' ); return false; // If the user needs to confirm that they want to see it... - } elseif ( $wgRequest->getInt( 'unhide' ) != 1 ) { + } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) { # Give explanation and add a link to view the revision... $oldid = intval( $this->getOldID() ); $link = $this->getTitle()->getFullUrl( "oldid={$oldid}&unhide=1" ); $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide'; - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) ); return false; @@ -1060,7 +1092,7 @@ class Article extends Page { } else { $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ? 'rev-suppressed-text-view' : 'rev-deleted-text-view'; - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg ); + $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg ); return true; } @@ -1072,30 +1104,36 @@ class Article extends Page { * Revision as of \<date\>; view current revision * \<- Previous version | Next Version -\> * - * @param $oldid String: revision ID of this article revision + * @param $oldid int: revision ID of this article revision */ public function setOldSubtitle( $oldid = 0 ) { - global $wgLang, $wgOut, $wgUser, $wgRequest; - if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { return; } - $unhide = $wgRequest->getInt( 'unhide' ) == 1; + $unhide = $this->getContext()->getRequest()->getInt( 'unhide' ) == 1; # Cascade unhide param in links for easy deletion browsing $extraParams = array(); - if ( $wgRequest->getVal( 'unhide' ) ) { + if ( $unhide ) { $extraParams['unhide'] = 1; } - $revision = Revision::newFromId( $oldid ); + if ( $this->mRevision && $this->mRevision->getId() === $oldid ) { + $revision = $this->mRevision; + } else { + $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 ); + $language = $this->getContext()->getLanguage(); + $user = $this->getContext()->getUser(); + + $td = $language->userTimeAndDate( $timestamp, $user ); + $tddate = $language->userDate( $timestamp, $user ); + $tdtime = $language->userTime( $timestamp, $user ); # Show user links if allowed to see them. If hidden, then show them only if requested... $userlinks = Linker::revUserTools( $revision, !$unhide ); @@ -1104,89 +1142,85 @@ class Article extends Page { ? 'revision-info-current' : 'revision-info'; - $wgOut->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg, + $outputPage = $this->getContext()->getOutput(); + $outputPage->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg, $td )->rawParams( $userlinks )->params( $revision->getID(), $tddate, $tdtime, $revision->getUser() )->parse() . "</div>" ); $lnk = $current - ? wfMsgHtml( 'currentrevisionlink' ) - : Linker::link( + ? wfMessage( 'currentrevisionlink' )->escaped() + : Linker::linkKnown( $this->getTitle(), - wfMsgHtml( 'currentrevisionlink' ), + wfMessage( 'currentrevisionlink' )->escaped(), array(), - $extraParams, - array( 'known', 'noclasses' ) + $extraParams ); $curdiff = $current - ? wfMsgHtml( 'diff' ) - : Linker::link( + ? wfMessage( 'diff' )->escaped() + : Linker::linkKnown( $this->getTitle(), - wfMsgHtml( 'diff' ), + wfMessage( 'diff' )->escaped(), array(), array( 'diff' => 'cur', 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) + ) + $extraParams ); $prev = $this->getTitle()->getPreviousRevisionID( $oldid ) ; $prevlink = $prev - ? Linker::link( + ? Linker::linkKnown( $this->getTitle(), - wfMsgHtml( 'previousrevision' ), + wfMessage( 'previousrevision' )->escaped(), array(), array( 'direction' => 'prev', 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) + ) + $extraParams ) - : wfMsgHtml( 'previousrevision' ); + : wfMessage( 'previousrevision' )->escaped(); $prevdiff = $prev - ? Linker::link( + ? Linker::linkKnown( $this->getTitle(), - wfMsgHtml( 'diff' ), + wfMessage( 'diff' )->escaped(), array(), array( 'diff' => 'prev', 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) + ) + $extraParams ) - : wfMsgHtml( 'diff' ); + : wfMessage( 'diff' )->escaped(); $nextlink = $current - ? wfMsgHtml( 'nextrevision' ) - : Linker::link( + ? wfMessage( 'nextrevision' )->escaped() + : Linker::linkKnown( $this->getTitle(), - wfMsgHtml( 'nextrevision' ), + wfMessage( 'nextrevision' )->escaped(), array(), array( 'direction' => 'next', 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) + ) + $extraParams ); $nextdiff = $current - ? wfMsgHtml( 'diff' ) - : Linker::link( + ? wfMessage( 'diff' )->escaped() + : Linker::linkKnown( $this->getTitle(), - wfMsgHtml( 'diff' ), + wfMessage( 'diff' )->escaped(), array(), array( 'diff' => 'next', 'oldid' => $oldid - ) + $extraParams, - array( 'known', 'noclasses' ) + ) + $extraParams ); - $cdel = Linker::getRevDeleteLink( $wgUser, $revision, $this->getTitle() ); + $cdel = Linker::getRevDeleteLink( $user, $revision, $this->getTitle() ); if ( $cdel !== '' ) { $cdel .= ' '; } - $wgOut->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel . - wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ), - $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>" ); + $outputPage->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel . + wfMessage( 'revision-nav' )->rawParams( + $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff + )->escaped() . "</div>" ); } /** @@ -1198,7 +1232,7 @@ class Article extends Page { * @return string containing HMTL with redirect link */ public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { - global $wgOut, $wgStylePath; + global $wgStylePath; if ( !is_array( $target ) ) { $target = array( $target ); @@ -1208,7 +1242,8 @@ class Article extends Page { $imageDir = $lang->getDir(); if ( $appendSubtitle ) { - $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) ); + $out = $this->getContext()->getOutput(); + $out->addSubtitle( wfMessage( 'redirectpagesub' )->escaped() ); } // the loop prepends the arrow image before the link, so the first case needs to be outside @@ -1246,9 +1281,7 @@ class Article extends Page { * Handle action=render */ public function render() { - global $wgOut; - - $wgOut->setArticleBodyOnly( true ); + $this->getContext()->getOutput()->setArticleBodyOnly( true ); $this->view(); } @@ -1271,8 +1304,6 @@ class Article extends Page { * UI entry point for page deletion */ public function delete() { - global $wgOut, $wgRequest, $wgLang; - # This code desperately needs to be totally rewritten $title = $this->getTitle(); @@ -1290,48 +1321,54 @@ class Article extends Page { } # Better double-check that it hasn't been deleted yet! - $dbw = wfGetDB( DB_MASTER ); - $conds = $title->pageCond(); - $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); - if ( $latest === false ) { - $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) ); - $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", + $this->mPage->loadPageData( 'fromdbmaster' ); + if ( !$this->mPage->exists() ) { + $deleteLogPage = new LogPage( 'delete' ); + $outputPage = $this->getContext()->getOutput(); + $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) ); + $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", array( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ) ); - $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); + $outputPage->addHTML( + Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) + ); LogEventsList::showLogExtract( - $wgOut, + $outputPage, 'delete', - $title->getPrefixedText() + $title ); return; } - $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' ); - $deleteReason = $wgRequest->getText( 'wpReason' ); + $request = $this->getContext()->getRequest(); + $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' ); + $deleteReason = $request->getText( 'wpReason' ); if ( $deleteReasonList == 'other' ) { $reason = $deleteReason; } elseif ( $deleteReason != '' ) { // Entry from drop down menu + additional comment - $reason = $deleteReasonList . wfMsgForContent( 'colon-separator' ) . $deleteReason; + $colonseparator = wfMessage( 'colon-separator' )->inContentLanguage()->text(); + $reason = $deleteReasonList . $colonseparator . $deleteReason; } else { $reason = $deleteReasonList; } - if ( $wgRequest->wasPosted() && $user->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), + if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ), array( 'delete', $this->getTitle()->getPrefixedText() ) ) ) { # Flag to hide all contents of the archived revisions - $suppress = $wgRequest->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' ); + $suppress = $request->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' ); $this->doDelete( $reason, $suppress ); - if ( $wgRequest->getCheck( 'wpWatch' ) && $user->isLoggedIn() ) { - $this->doWatch(); - } elseif ( $title->userIsWatching() ) { - $this->doUnwatch(); + if ( $user->isLoggedIn() && $request->getCheck( 'wpWatch' ) != $user->isWatched( $title ) ) { + if ( $request->getCheck( 'wpWatch' ) ) { + WatchAction::doWatch( $title, $user ); + } else { + WatchAction::doUnwatch( $title, $user ); + } } return; @@ -1347,10 +1384,10 @@ class Article extends Page { if ( $hasHistory ) { $revisions = $this->mTitle->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' ) . Linker::link( $title, - wfMsgHtml( 'history' ), + $this->getContext()->getOutput()->addHTML( '<strong class="mw-delete-warning-revisions">' . + wfMessage( 'historywarning' )->numParams( $revisions )->parse() . + wfMessage( 'word-separator' )->plain() . Linker::linkKnown( $title, + wfMessage( 'history' )->escaped(), array( 'rel' => 'archives' ), array( 'action' => 'history' ) ) . '</strong>' @@ -1358,12 +1395,12 @@ class Article extends Page { if ( $this->mTitle->isBigDeletion() ) { global $wgDeleteRevisionsLimit; - $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", - array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); + $this->getContext()->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n", + array( 'delete-warning-toobig', $this->getContext()->getLanguage()->formatNum( $wgDeleteRevisionsLimit ) ) ); } } - return $this->confirmDelete( $reason ); + $this->confirmDelete( $reason ); } /** @@ -1372,16 +1409,15 @@ class Article extends Page { * @param $reason String: prefilled reason */ public function confirmDelete( $reason ) { - global $wgOut; - wfDebug( "Article::confirmDelete\n" ); - $wgOut->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); - $wgOut->addBacklinkSubtitle( $this->getTitle() ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->addWikiMsg( 'confirmdeletetext' ); + $outputPage = $this->getContext()->getOutput(); + $outputPage->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) ); + $outputPage->addBacklinkSubtitle( $this->getTitle() ); + $outputPage->setRobotPolicy( 'noindex,nofollow' ); + $outputPage->addWikiMsg( 'confirmdeletetext' ); - wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) ); + wfRunHooks( 'ArticleConfirmDelete', array( $this, $outputPage, &$reason ) ); $user = $this->getContext()->getUser(); @@ -1389,33 +1425,33 @@ class Article extends Page { $suppress = "<tr id=\"wpDeleteSuppressRow\"> <td></td> <td class='mw-input'><strong>" . - Xml::checkLabel( wfMsg( 'revdelete-suppress' ), + Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . "</strong></td> </tr>"; } else { $suppress = ''; } - $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching(); + $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $user->isWatched( $this->getTitle() ); $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) . Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . - Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) . + Xml::tags( 'legend', null, wfMessage( 'delete-legend' )->escaped() ) . Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) . "<tr id=\"wpDeleteReasonListRow\"> <td class='mw-label'>" . - Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) . + Xml::label( wfMessage( 'deletecomment' )->text(), 'wpDeleteReasonList' ) . "</td> <td class='mw-input'>" . Xml::listDropDown( 'wpDeleteReasonList', - wfMsgForContent( 'deletereason-dropdown' ), - wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) . + wfMessage( 'deletereason-dropdown' )->inContentLanguage()->text(), + wfMessage( 'deletereasonotherlist' )->inContentLanguage()->text(), '', 'wpReasonDropDown', 1 ) . "</td> </tr> <tr id=\"wpDeleteReasonRow\"> <td class='mw-label'>" . - Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) . + Xml::label( wfMessage( 'deleteotherreason' )->text(), 'wpReason' ) . "</td> <td class='mw-input'>" . Html::input( 'wpReason', $reason, 'text', array( @@ -1434,7 +1470,7 @@ class Article extends Page { <tr> <td></td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'watchthis' ), + Xml::checkLabel( wfMessage( 'watchthis' )->text(), 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . "</td> </tr>"; @@ -1445,7 +1481,7 @@ class Article extends Page { <tr> <td></td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'deletepage' ), + Xml::submitButton( wfMessage( 'deletepage' )->text(), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) . "</td> </tr>" . @@ -1458,17 +1494,19 @@ class Article extends Page { $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); $link = Linker::link( $title, - wfMsgHtml( 'delete-edit-reasonlist' ), + wfMessage( 'delete-edit-reasonlist' )->escaped(), array(), array( 'action' => 'edit' ) ); $form .= '<p class="mw-delete-editreasons">' . $link . '</p>'; } - $wgOut->addHTML( $form ); - $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); - LogEventsList::showLogExtract( $wgOut, 'delete', - $this->getTitle()->getPrefixedText() + $outputPage->addHTML( $form ); + + $deleteLogPage = new LogPage( 'delete' ); + $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); + LogEventsList::showLogExtract( $outputPage, 'delete', + $this->getTitle() ); } @@ -1478,34 +1516,35 @@ class Article extends Page { * @param $suppress bool */ public function doDelete( $reason, $suppress = false ) { - global $wgOut; - $error = ''; - if ( $this->mPage->doDeleteArticle( $reason, $suppress, 0, true, $error ) ) { + $outputPage = $this->getContext()->getOutput(); + $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error ); + if ( $status->isGood() ) { $deleted = $this->getTitle()->getPrefixedText(); - $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) ); + $outputPage->setRobotPolicy( 'noindex,nofollow' ); - $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'; + $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]'; - $wgOut->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink ); - $wgOut->returnToMain( false ); + $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink ); + $outputPage->returnToMain( false ); } else { - $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) ); + $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) ); if ( $error == '' ) { - $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>", - array( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ) + $outputPage->addWikiText( + "<div class=\"error mw-error-cannotdelete\">\n" . $status->getWikiText() . "\n</div>" ); - $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); + $deleteLogPage = new LogPage( 'delete' ); + $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) ); LogEventsList::showLogExtract( - $wgOut, + $outputPage, 'delete', - $this->getTitle()->getPrefixedText() + $this->getTitle() ); } else { - $wgOut->addHTML( $error ); + $outputPage->addHTML( $error ); } } } @@ -1578,22 +1617,22 @@ class Article extends Page { * @return ParserOutput or false if the given revsion ID is not found */ public function getParserOutput( $oldid = null, User $user = null ) { - global $wgUser; - - $user = is_null( $user ) ? $wgUser : $user; - $parserOptions = $this->mPage->makeParserOptions( $user ); + if ( $user === null ) { + $parserOptions = $this->getParserOptions(); + } else { + $parserOptions = $this->mPage->makeParserOptions( $user ); + } return $this->mPage->getParserOutput( $parserOptions, $oldid ); } /** * Get parser options suitable for rendering the primary article wikitext - * @return ParserOptions|false + * @return ParserOptions */ public function getParserOptions() { - global $wgUser; if ( !$this->mParserOptions ) { - $this->mParserOptions = $this->mPage->makeParserOptions( $wgUser ); + $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext() ); } // Clone to allow modifications of the return value without affecting cache return clone $this->mParserOptions; @@ -1645,6 +1684,7 @@ class Article extends Page { /** * Handle action=purge * @deprecated since 1.19 + * @return Action|bool|null false if the action is disabled, null if it is not recognised */ public function purge() { return Action::factory( 'purge', $this )->show(); @@ -1679,7 +1719,7 @@ class Article extends Page { } /** - * Add this page to $wgUser's watchlist + * Add this page to the current user's watchlist * * This is safe to be called multiple times * @@ -1687,9 +1727,8 @@ class Article extends Page { * @deprecated since 1.18 */ public function doWatch() { - global $wgUser; wfDeprecated( __METHOD__, '1.18' ); - return WatchAction::doWatch( $this->getTitle(), $wgUser ); + return WatchAction::doWatch( $this->getTitle(), $this->getContext()->getUser() ); } /** @@ -1708,24 +1747,21 @@ class Article extends Page { * @deprecated since 1.18 */ public function doUnwatch() { - global $wgUser; wfDeprecated( __METHOD__, '1.18' ); - return WatchAction::doUnwatch( $this->getTitle(), $wgUser ); + return WatchAction::doUnwatch( $this->getTitle(), $this->getContext()->getUser() ); } /** * Output a redirect back to the article. * This is typically used after an edit. * - * @deprecated in 1.18; call $wgOut->redirect() directly + * @deprecated in 1.18; call OutputPage::redirect() directly * @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 = '' ) { wfDeprecated( __METHOD__, '1.18' ); - global $wgOut; - if ( $noRedir ) { $query = 'redirect=no'; if ( $extraQuery ) @@ -1734,7 +1770,7 @@ class Article extends Page { $query = $extraQuery; } - $wgOut->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor ); + $this->getContext()->getOutput()->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor ); } /** @@ -1776,6 +1812,7 @@ class Article extends Page { * * @param $fname String Name of called method * @param $args Array Arguments to the method + * @return mixed */ public function __call( $fname, $args ) { if ( is_callable( array( $this->mPage, $fname ) ) ) { @@ -1832,8 +1869,7 @@ class Article extends Page { * @return array */ public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) { - global $wgUser; - $user = is_null( $user ) ? $wgUser : $user; + $user = is_null( $user ) ? $this->getContext()->getUser() : $user; return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user ); } @@ -1846,8 +1882,7 @@ class Article extends Page { * @return array */ public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) { - global $wgUser; - $guser = is_null( $guser ) ? $wgUser : $guser; + $guser = is_null( $guser ) ? $this->getContext()->getUser() : $guser; return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser ); } diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index e8bab859..2e42439c 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -34,6 +34,12 @@ * someone logs in who can be authenticated externally. */ class AuthPlugin { + + /** + * @var string + */ + protected $domain; + /** * Check whether there exists a user account with the given name. * The name will be normalized to MediaWiki's requirements, so @@ -84,6 +90,19 @@ class AuthPlugin { } /** + * Get the user's domain + * + * @return string + */ + public function getDomain() { + if ( isset( $this->domain ) ) { + return $this->domain; + } else { + return 'invaliddomain'; + } + } + + /** * Check to see if the specific domain is a valid domain. * * @param $domain String: authentication domain. @@ -103,6 +122,7 @@ class AuthPlugin { * forget the & on your function declaration. * * @param $user User object + * @return bool */ public function updateUser( &$user ) { # Override this and do something @@ -256,6 +276,8 @@ class AuthPlugin { /** * If you want to munge the case of an account name before the final * check, now is your chance. + * @param $username string + * @return string */ public function getCanonicalName( $username ) { return $username; diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 93fac45f..d3a2c548 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -2,6 +2,21 @@ /** * This defines autoloading handler for whole MediaWiki framework * + * 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 */ @@ -27,6 +42,7 @@ $wgAutoloadLocalClasses = array( 'BadTitleError' => 'includes/Exception.php', 'BaseTemplate' => 'includes/SkinTemplate.php', 'Block' => 'includes/Block.php', + 'CacheHelper' => 'includes/CacheHelper.php', 'Category' => 'includes/Category.php', 'Categoryfinder' => 'includes/Categoryfinder.php', 'CategoryPage' => 'includes/CategoryPage.php', @@ -53,9 +69,11 @@ $wgAutoloadLocalClasses = array( 'CurlHttpRequest' => 'includes/HttpFunctions.php', 'DeferrableUpdate' => 'includes/DeferredUpdates.php', 'DeferredUpdates' => 'includes/DeferredUpdates.php', + 'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php', 'DerivativeRequest' => 'includes/WebRequest.php', + 'DeviceDetection' => 'includes/mobile/DeviceDetection.php', + 'DeviceProperties' => 'includes/mobile/DeviceDetection.php', 'DiffHistoryBlob' => 'includes/HistoryBlob.php', - 'DoubleReplacer' => 'includes/StringUtils.php', 'DummyLinker' => 'includes/Linker.php', 'Dump7ZipOutput' => 'includes/Export.php', @@ -92,6 +110,7 @@ $wgAutoloadLocalClasses = array( 'FormAction' => 'includes/Action.php', 'FormOptions' => 'includes/FormOptions.php', 'FormSpecialPage' => 'includes/SpecialPage.php', + 'GitInfo' => 'includes/GitInfo.php', 'HashtableReplacer' => 'includes/StringUtils.php', 'HistoryBlob' => 'includes/HistoryBlob.php', 'HistoryBlobCurStub' => 'includes/HistoryBlob.php', @@ -117,7 +136,10 @@ $wgAutoloadLocalClasses = array( 'Http' => 'includes/HttpFunctions.php', 'HttpError' => 'includes/Exception.php', 'HttpRequest' => 'includes/HttpFunctions.old.php', + 'ICacheHelper' => 'includes/CacheHelper.php', 'IcuCollation' => 'includes/Collation.php', + 'IDeviceProperties' => 'includes/mobile/DeviceDetection.php', + 'IDeviceDetector' => 'includes/mobile/DeviceDetection.php', 'IdentityCollation' => 'includes/Collation.php', 'ImageGallery' => 'includes/ImageGallery.php', 'ImageHistoryList' => 'includes/ImagePage.php', @@ -140,6 +162,7 @@ $wgAutoloadLocalClasses = array( 'Linker' => 'includes/Linker.php', 'LinkFilter' => 'includes/LinkFilter.php', 'LinksUpdate' => 'includes/LinksUpdate.php', + 'LinksDeletionUpdate' => 'includes/LinksUpdate.php', 'LocalisationCache' => 'includes/LocalisationCache.php', 'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php', 'MagicWord' => 'includes/MagicWord.php', @@ -153,6 +176,7 @@ $wgAutoloadLocalClasses = array( 'MWException' => 'includes/Exception.php', 'MWExceptionHandler' => 'includes/Exception.php', 'MWFunction' => 'includes/MWFunction.php', + 'MWHookException' => 'includes/Hooks.php', 'MWHttpRequest' => 'includes/HttpFunctions.php', 'MWInit' => 'includes/Init.php', 'MWNamespace' => 'includes/Namespace.php', @@ -185,12 +209,16 @@ $wgAutoloadLocalClasses = array( 'ReplacementArray' => 'includes/StringUtils.php', 'Replacer' => 'includes/StringUtils.php', 'ReverseChronologicalPager' => 'includes/Pager.php', + 'RevisionItem' => 'includes/RevisionList.php', 'RevisionItemBase' => 'includes/RevisionList.php', 'RevisionListBase' => 'includes/RevisionList.php', 'Revision' => 'includes/Revision.php', 'RevisionList' => 'includes/RevisionList.php', 'RSSFeed' => 'includes/Feed.php', 'Sanitizer' => 'includes/Sanitizer.php', + 'DataUpdate' => 'includes/DataUpdate.php', + 'SqlDataUpdate' => 'includes/SqlDataUpdate.php', + 'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php', 'SiteConfiguration' => 'includes/SiteConfiguration.php', 'SiteStats' => 'includes/SiteStats.php', 'SiteStatsInit' => 'includes/SiteStats.php', @@ -217,16 +245,20 @@ $wgAutoloadLocalClasses = array( 'StubObject' => 'includes/StubObject.php', 'StubUserLang' => 'includes/StubObject.php', 'TablePager' => 'includes/Pager.php', + 'MWTimestamp' => 'includes/Timestamp.php', 'Title' => 'includes/Title.php', 'TitleArray' => 'includes/TitleArray.php', 'TitleArrayFromResult' => 'includes/TitleArray.php', 'ThrottledError' => 'includes/Exception.php', 'UnlistedSpecialPage' => 'includes/SpecialPage.php', + 'UploadSourceAdapter' => 'includes/Import.php', 'UppercaseCollation' => 'includes/Collation.php', 'User' => 'includes/User.php', 'UserArray' => 'includes/UserArray.php', 'UserArrayFromResult' => 'includes/UserArray.php', 'UserBlockedError' => 'includes/Exception.php', + 'UserNotLoggedIn' => 'includes/Exception.php', + 'UserCache' => 'includes/cache/UserCache.php', 'UserMailer' => 'includes/UserMailer.php', 'UserRightsProxy' => 'includes/UserRightsProxy.php', 'ViewCountUpdate' => 'includes/ViewCountUpdate.php', @@ -249,12 +281,15 @@ $wgAutoloadLocalClasses = array( 'Xml' => 'includes/Xml.php', 'XmlDumpWriter' => 'includes/Export.php', 'XmlJsCode' => 'includes/Xml.php', + 'XMLReader2' => 'includes/Import.php', 'XmlSelect' => 'includes/Xml.php', 'XmlTypeCheck' => 'includes/XmlTypeCheck.php', 'ZhClient' => 'includes/ZhClient.php', 'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php', + 'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php', # includes/actions + 'CachedAction' => 'includes/actions/CachedAction.php', 'CreditsAction' => 'includes/actions/CreditsAction.php', 'DeleteAction' => 'includes/actions/DeleteAction.php', 'EditAction' => 'includes/actions/EditAction.php', @@ -310,6 +345,7 @@ $wgAutoloadLocalClasses = array( 'ApiMain' => 'includes/api/ApiMain.php', 'ApiMove' => 'includes/api/ApiMove.php', 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', + 'ApiOptions' => 'includes/api/ApiOptions.php', 'ApiPageSet' => 'includes/api/ApiPageSet.php', 'ApiParamInfo' => 'includes/api/ApiParamInfo.php', 'ApiParse' => 'includes/api/ApiParse.php', @@ -318,10 +354,10 @@ $wgAutoloadLocalClasses = array( 'ApiPurge' => 'includes/api/ApiPurge.php', 'ApiQuery' => 'includes/api/ApiQuery.php', 'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php', - 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php', + 'ApiQueryAllImages' => 'includes/api/ApiQueryAllImages.php', 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php', - 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php', - 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.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', @@ -363,11 +399,14 @@ $wgAutoloadLocalClasses = array( 'ApiResult' => 'includes/api/ApiResult.php', 'ApiRollback' => 'includes/api/ApiRollback.php', 'ApiRsd' => 'includes/api/ApiRsd.php', + 'ApiSetNotificationTimestamp' => 'includes/api/ApiSetNotificationTimestamp.php', + 'ApiTokens' => 'includes/api/ApiTokens.php', 'ApiUnblock' => 'includes/api/ApiUnblock.php', 'ApiUndelete' => 'includes/api/ApiUndelete.php', 'ApiUpload' => 'includes/api/ApiUpload.php', 'ApiUserrights' => 'includes/api/ApiUserrights.php', 'ApiWatch' => 'includes/api/ApiWatch.php', + 'UsageException' => 'includes/api/ApiMain.php', # includes/cache 'CacheDependency' => 'includes/cache/CacheDependency.php', @@ -384,19 +423,21 @@ $wgAutoloadLocalClasses = array( 'LinkCache' => 'includes/cache/LinkCache.php', 'MessageCache' => 'includes/cache/MessageCache.php', 'ObjectFileCache' => 'includes/cache/ObjectFileCache.php', + 'ProcessCacheLRU' => 'includes/cache/ProcessCacheLRU.php', 'ResourceFileCache' => 'includes/cache/ResourceFileCache.php', 'SquidUpdate' => 'includes/cache/SquidUpdate.php', 'TitleDependency' => 'includes/cache/CacheDependency.php', 'TitleListDependency' => 'includes/cache/CacheDependency.php', - 'UsageException' => 'includes/api/ApiMain.php', - # includes/context 'ContextSource' => 'includes/context/ContextSource.php', 'DerivativeContext' => 'includes/context/DerivativeContext.php', 'IContextSource' => 'includes/context/IContextSource.php', 'RequestContext' => 'includes/context/RequestContext.php', + # includes/dao + 'IDBAccessObject' => 'includes/dao/IDBAccessObject.php', + # includes/db 'Blob' => 'includes/db/DatabaseUtility.php', 'ChronologyProtector' => 'includes/db/LBFactory.php', @@ -411,9 +452,12 @@ $wgAutoloadLocalClasses = array( 'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php', 'DatabaseSqliteStandalone' => 'includes/db/DatabaseSqlite.php', 'DatabaseType' => 'includes/db/Database.php', + 'DBAccessError' => 'includes/db/LBFactory.php', 'DBConnectionError' => 'includes/db/DatabaseError.php', 'DBError' => 'includes/db/DatabaseError.php', 'DBObject' => 'includes/db/DatabaseUtility.php', + 'IORMRow' => 'includes/db/IORMRow.php', + 'IORMTable' => 'includes/db/IORMTable.php', 'DBMasterPos' => 'includes/db/DatabaseUtility.php', 'DBQueryError' => 'includes/db/DatabaseError.php', 'DBUnexpectedError' => 'includes/db/DatabaseError.php', @@ -421,7 +465,10 @@ $wgAutoloadLocalClasses = array( 'Field' => 'includes/db/DatabaseUtility.php', 'IBM_DB2Blob' => 'includes/db/DatabaseIbm_db2.php', 'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php', + 'IBM_DB2Helper' => 'includes/db/DatabaseIbm_db2.php', + 'IBM_DB2Result' => 'includes/db/DatabaseIbm_db2.php', 'LBFactory' => 'includes/db/LBFactory.php', + 'LBFactory_Fake' => 'includes/db/LBFactory.php', 'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php', 'LBFactory_Simple' => 'includes/db/LBFactory.php', 'LBFactory_Single' => 'includes/db/LBFactory_Single.php', @@ -431,12 +478,20 @@ $wgAutoloadLocalClasses = array( 'LoadMonitor' => 'includes/db/LoadMonitor.php', 'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php', 'LoadMonitor_Null' => 'includes/db/LoadMonitor.php', + 'MssqlField' => 'includes/db/DatabaseMssql.php', + 'MssqlResult' => 'includes/db/DatabaseMssql.php', 'MySQLField' => 'includes/db/DatabaseMysql.php', 'MySQLMasterPos' => 'includes/db/DatabaseMysql.php', 'ORAField' => 'includes/db/DatabaseOracle.php', 'ORAResult' => 'includes/db/DatabaseOracle.php', + 'ORMIterator' => 'includes/db/ORMIterator.php', + 'ORMResult' => 'includes/db/ORMResult.php', + 'ORMRow' => 'includes/db/ORMRow.php', + 'ORMTable' => 'includes/db/ORMTable.php', 'PostgresField' => 'includes/db/DatabasePostgres.php', + 'PostgresTransactionState' => 'includes/db/DatabasePostgres.php', 'ResultWrapper' => 'includes/db/DatabaseUtility.php', + 'SavepointPostgres' => 'includes/db/DatabasePostgres.php', 'SQLiteField' => 'includes/db/DatabaseSqlite.php', # includes/debug @@ -466,6 +521,50 @@ $wgAutoloadLocalClasses = array( 'ExternalUser_MediaWiki' => 'includes/extauth/MediaWiki.php', 'ExternalUser_vB' => 'includes/extauth/vB.php', + # includes/filebackend + 'FileBackendGroup' => 'includes/filebackend/FileBackendGroup.php', + 'FileBackend' => 'includes/filebackend/FileBackend.php', + 'FileBackendStore' => 'includes/filebackend/FileBackendStore.php', + 'FileBackendStoreShardListIterator' => 'includes/filebackend/FileBackendStore.php', + 'FileBackendStoreShardDirIterator' => 'includes/filebackend/FileBackendStore.php', + 'FileBackendStoreShardFileIterator' => 'includes/filebackend/FileBackendStore.php', + 'FileBackendMultiWrite' => 'includes/filebackend/FileBackendMultiWrite.php', + 'FileBackendStoreOpHandle' => 'includes/filebackend/FileBackendStore.php', + 'FSFile' => 'includes/filebackend/FSFile.php', + 'FSFileBackend' => 'includes/filebackend/FSFileBackend.php', + 'FSFileBackendList' => 'includes/filebackend/FSFileBackend.php', + 'FSFileBackendDirList' => 'includes/filebackend/FSFileBackend.php', + 'FSFileBackendFileList' => 'includes/filebackend/FSFileBackend.php', + 'FSFileOpHandle' => 'includes/filebackend/FSFileBackend.php', + 'SwiftFileBackend' => 'includes/filebackend/SwiftFileBackend.php', + 'SwiftFileBackendList' => 'includes/filebackend/SwiftFileBackend.php', + 'SwiftFileBackendDirList' => 'includes/filebackend/SwiftFileBackend.php', + 'SwiftFileBackendFileList' => 'includes/filebackend/SwiftFileBackend.php', + 'SwiftFileOpHandle' => 'includes/filebackend/SwiftFileBackend.php', + 'TempFSFile' => 'includes/filebackend/TempFSFile.php', + 'FileJournal' => 'includes/filebackend/filejournal/FileJournal.php', + 'DBFileJournal' => 'includes/filebackend/filejournal/DBFileJournal.php', + 'NullFileJournal' => 'includes/filebackend/filejournal/FileJournal.php', + 'LockManagerGroup' => 'includes/filebackend/lockmanager/LockManagerGroup.php', + 'LockManager' => 'includes/filebackend/lockmanager/LockManager.php', + 'ScopedLock' => 'includes/filebackend/lockmanager/LockManager.php', + 'FSLockManager' => 'includes/filebackend/lockmanager/FSLockManager.php', + 'DBLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php', + 'LSLockManager' => 'includes/filebackend/lockmanager/LSLockManager.php', + 'MemcLockManager' => 'includes/filebackend/lockmanager/MemcLockManager.php', + 'QuorumLockManager' => 'includes/filebackend/lockmanager/LockManager.php', + 'MySqlLockManager'=> 'includes/filebackend/lockmanager/DBLockManager.php', + 'NullLockManager' => 'includes/filebackend/lockmanager/LockManager.php', + 'FileOp' => 'includes/filebackend/FileOp.php', + 'FileOpBatch' => 'includes/filebackend/FileOpBatch.php', + 'StoreFileOp' => 'includes/filebackend/FileOp.php', + 'CopyFileOp' => 'includes/filebackend/FileOp.php', + 'MoveFileOp' => 'includes/filebackend/FileOp.php', + 'DeleteFileOp' => 'includes/filebackend/FileOp.php', + 'ConcatenateFileOp' => 'includes/filebackend/FileOp.php', + 'CreateFileOp' => 'includes/filebackend/FileOp.php', + 'NullFileOp' => 'includes/filebackend/FileOp.php', + # includes/filerepo 'FileRepo' => 'includes/filerepo/FileRepo.php', 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php', @@ -476,6 +575,7 @@ $wgAutoloadLocalClasses = array( 'LocalRepo' => 'includes/filerepo/LocalRepo.php', 'NullRepo' => 'includes/filerepo/NullRepo.php', 'RepoGroup' => 'includes/filerepo/RepoGroup.php', + 'TempFileRepo' => 'includes/filerepo/FileRepo.php', # includes/filerepo/file 'ArchivedFile' => 'includes/filerepo/file/ArchivedFile.php', @@ -488,36 +588,6 @@ $wgAutoloadLocalClasses = array( 'LocalFileRestoreBatch' => 'includes/filerepo/file/LocalFile.php', 'OldLocalFile' => 'includes/filerepo/file/OldLocalFile.php', 'UnregisteredLocalFile' => 'includes/filerepo/file/UnregisteredLocalFile.php', - 'FSFile' => 'includes/filerepo/backend/FSFile.php', - 'TempFSFile' => 'includes/filerepo/backend/TempFSFile.php', - - # includes/filerepo/backend - 'FileBackendGroup' => 'includes/filerepo/backend/FileBackendGroup.php', - 'FileBackend' => 'includes/filerepo/backend/FileBackend.php', - 'FileBackendStore' => 'includes/filerepo/backend/FileBackend.php', - 'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php', - 'FileBackendStoreShardListIterator' => 'includes/filerepo/backend/FileBackend.php', - 'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php', - 'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php', - 'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php', - 'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php', - 'LockManagerGroup' => 'includes/filerepo/backend/lockmanager/LockManagerGroup.php', - 'LockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', - 'ScopedLock' => 'includes/filerepo/backend/lockmanager/LockManager.php', - 'FSLockManager' => 'includes/filerepo/backend/lockmanager/FSLockManager.php', - 'DBLockManager' => 'includes/filerepo/backend/lockmanager/DBLockManager.php', - 'LSLockManager' => 'includes/filerepo/backend/lockmanager/LSLockManager.php', - 'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php', - 'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', - 'FileOp' => 'includes/filerepo/backend/FileOp.php', - 'FileOpScopedPHPTimeout' => 'includes/filerepo/backend/FileOp.php', - 'StoreFileOp' => 'includes/filerepo/backend/FileOp.php', - 'CopyFileOp' => 'includes/filerepo/backend/FileOp.php', - 'MoveFileOp' => 'includes/filerepo/backend/FileOp.php', - 'DeleteFileOp' => 'includes/filerepo/backend/FileOp.php', - 'ConcatenateFileOp' => 'includes/filerepo/backend/FileOp.php', - 'CreateFileOp' => 'includes/filerepo/backend/FileOp.php', - 'NullFileOp' => 'includes/filerepo/backend/FileOp.php', # includes/installer 'CliInstaller' => 'includes/installer/CliInstaller.php', @@ -563,7 +633,7 @@ $wgAutoloadLocalClasses = array( 'DoubleRedirectJob' => 'includes/job/DoubleRedirectJob.php', 'EmaillingJob' => 'includes/job/EmaillingJob.php', 'EnotifNotifyJob' => 'includes/job/EnotifNotifyJob.php', - 'Job' => 'includes/job/JobQueue.php', + 'Job' => 'includes/job/Job.php', 'RefreshLinksJob' => 'includes/job/RefreshLinksJob.php', 'RefreshLinksJob2' => 'includes/job/RefreshLinksJob.php', 'UploadFromUrlJob' => 'includes/job/UploadFromUrlJob.php', @@ -575,13 +645,19 @@ $wgAutoloadLocalClasses = array( # includes/libs 'CSSJanus' => 'includes/libs/CSSJanus.php', + 'CSSJanus_Tokenizer' => 'includes/libs/CSSJanus.php', 'CSSMin' => 'includes/libs/CSSMin.php', + 'GenericArrayObject' => 'includes/libs/GenericArrayObject.php', 'HttpStatus' => 'includes/libs/HttpStatus.php', 'IEContentAnalyzer' => 'includes/libs/IEContentAnalyzer.php', 'IEUrlExtension' => 'includes/libs/IEUrlExtension.php', 'JavaScriptMinifier' => 'includes/libs/JavaScriptMinifier.php', + 'JSCompilerContext' => 'includes/libs/jsminplus.php', 'JSMinPlus' => 'includes/libs/jsminplus.php', + 'JSNode' => 'includes/libs/jsminplus.php', 'JSParser' => 'includes/libs/jsminplus.php', + 'JSToken' => 'includes/libs/jsminplus.php', + 'JSTokenizer' => 'includes/libs/jsminplus.php', # includes/logging 'DatabaseLogEntry' => 'includes/logging/LogEntry.php', @@ -613,17 +689,18 @@ $wgAutoloadLocalClasses = array( 'FormatMetadata' => 'includes/media/FormatMetadata.php', 'GIFHandler' => 'includes/media/GIF.php', 'GIFMetadataExtractor' => 'includes/media/GIFMetadataExtractor.php', - 'ImageHandler' => 'includes/media/Generic.php', + 'ImageHandler' => 'includes/media/ImageHandler.php', 'IPTC' => 'includes/media/IPTC.php', 'JpegHandler' => 'includes/media/Jpeg.php', 'JpegMetadataExtractor' => 'includes/media/JpegMetadataExtractor.php', - 'MediaHandler' => 'includes/media/Generic.php', + 'MediaHandler' => 'includes/media/MediaHandler.php', 'MediaTransformError' => 'includes/media/MediaTransformOutput.php', 'MediaTransformOutput' => 'includes/media/MediaTransformOutput.php', 'PNGHandler' => 'includes/media/PNG.php', 'PNGMetadataExtractor' => 'includes/media/PNGMetadataExtractor.php', 'SvgHandler' => 'includes/media/SVG.php', 'SVGMetadataExtractor' => 'includes/media/SVGMetadataExtractor.php', + 'SVGReader' => 'includes/media/SVGMetadataExtractor.php', 'ThumbnailImage' => 'includes/media/MediaTransformOutput.php', 'TiffHandler' => 'includes/media/Tiff.php', 'TransformParameterError' => 'includes/media/MediaTransformOutput.php', @@ -645,16 +722,20 @@ $wgAutoloadLocalClasses = array( 'HashBagOStuff' => 'includes/objectcache/HashBagOStuff.php', 'MediaWikiBagOStuff' => 'includes/objectcache/SqlBagOStuff.php', 'MemCachedClientforWiki' => 'includes/objectcache/MemcachedClient.php', + 'MemcachedBagOStuff' => 'includes/objectcache/MemcachedBagOStuff.php', + 'MemcachedPeclBagOStuff' => 'includes/objectcache/MemcachedPeclBagOStuff.php', 'MemcachedPhpBagOStuff' => 'includes/objectcache/MemcachedPhpBagOStuff.php', 'MultiWriteBagOStuff' => 'includes/objectcache/MultiWriteBagOStuff.php', 'MWMemcached' => 'includes/objectcache/MemcachedClient.php', 'ObjectCache' => 'includes/objectcache/ObjectCache.php', + 'ObjectCacheSessionHandler' => 'includes/objectcache/ObjectCacheSessionHandler.php', + 'RedisBagOStuff' => 'includes/objectcache/RedisBagOStuff.php', 'SqlBagOStuff' => 'includes/objectcache/SqlBagOStuff.php', 'WinCacheBagOStuff' => 'includes/objectcache/WinCacheBagOStuff.php', 'XCacheBagOStuff' => 'includes/objectcache/XCacheBagOStuff.php', # includes/parser - 'CacheTime' => 'includes/parser/ParserOutput.php', + 'CacheTime' => 'includes/parser/CacheTime.php', 'CoreLinkFunctions' => 'includes/parser/CoreLinkFunctions.php', 'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php', 'CoreTagHooks' => 'includes/parser/CoreTagHooks.php', @@ -662,6 +743,7 @@ $wgAutoloadLocalClasses = array( 'LinkHolderArray' => 'includes/parser/LinkHolderArray.php', 'LinkMarkerReplacer' => 'includes/parser/Parser_LinkHooks.php', 'MWTidy' => 'includes/parser/Tidy.php', + 'MWTidyWrapper' => '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', @@ -727,11 +809,13 @@ $wgAutoloadLocalClasses = array( 'ResourceLoaderUserModule' => 'includes/resourceloader/ResourceLoaderUserModule.php', 'ResourceLoaderUserOptionsModule' => 'includes/resourceloader/ResourceLoaderUserOptionsModule.php', 'ResourceLoaderUserTokensModule' => 'includes/resourceloader/ResourceLoaderUserTokensModule.php', + 'ResourceLoaderLanguageDataModule' => 'includes/resourceloader/ResourceLoaderLanguageDataModule.php', 'ResourceLoaderWikiModule' => 'includes/resourceloader/ResourceLoaderWikiModule.php', # includes/revisiondelete 'RevDel_ArchivedFileItem' => 'includes/revisiondelete/RevisionDelete.php', 'RevDel_ArchivedFileList' => 'includes/revisiondelete/RevisionDelete.php', + 'RevDel_ArchivedRevisionItem' => 'includes/revisiondelete/RevisionDelete.php', 'RevDel_ArchiveItem' => 'includes/revisiondelete/RevisionDelete.php', 'RevDel_ArchiveList' => 'includes/revisiondelete/RevisionDelete.php', 'RevDel_FileItem' => 'includes/revisiondelete/RevisionDelete.php', @@ -747,6 +831,7 @@ $wgAutoloadLocalClasses = array( 'RevisionDeleteUser' => 'includes/revisiondelete/RevisionDeleteUser.php', # includes/search + 'MssqlSearchResultSet' => 'includes/search/SearchMssql.php', 'MySQLSearchResultSet' => 'includes/search/SearchMySQL.php', 'PostgresSearchResult' => 'includes/search/SearchPostgres.php', 'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php', @@ -756,6 +841,7 @@ $wgAutoloadLocalClasses = array( 'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php', 'SearchMssql' => 'includes/search/SearchMssql.php', 'SearchMySQL' => 'includes/search/SearchMySQL.php', + 'SearchNearMatchResultSet' => 'includes/search/SearchEngine.php', 'SearchOracle' => 'includes/search/SearchOracle.php', 'SearchPostgres' => 'includes/search/SearchPostgres.php', 'SearchResult' => 'includes/search/SearchEngine.php', @@ -773,6 +859,7 @@ $wgAutoloadLocalClasses = array( 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', 'BlockListPager' => 'includes/specials/SpecialBlockList.php', 'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php', + 'CategoryPager' => 'includes/specials/SpecialCategories.php', 'ContribsPager' => 'includes/specials/SpecialContributions.php', 'DBLockForm' => 'includes/specials/SpecialLockdb.php', 'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php', @@ -781,11 +868,14 @@ $wgAutoloadLocalClasses = array( 'DeletedContributionsPage' => 'includes/specials/SpecialDeletedContributions.php', 'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php', 'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php', + 'EditWatchlistCheckboxSeriesField' => 'includes/specials/SpecialEditWatchlist.php', + 'EditWatchlistNormalHTMLForm' => 'includes/specials/SpecialEditWatchlist.php', 'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php', 'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php', 'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php', 'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php', 'HTMLBlockedUsersItemSelect' => 'includes/specials/SpecialBlockList.php', + 'ImageListPager' => 'includes/specials/SpecialListfiles.php', 'ImportReporter' => 'includes/specials/SpecialImport.php', 'IPBlockForm' => 'includes/specials/SpecialBlock.php', 'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php', @@ -793,17 +883,22 @@ $wgAutoloadLocalClasses = array( 'LoginForm' => 'includes/specials/SpecialUserlogin.php', 'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php', 'LongPagesPage' => 'includes/specials/SpecialLongpages.php', + 'MergeHistoryPager' => 'includes/specials/SpecialMergeHistory.php', 'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php', 'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php', 'MostimagesPage' => 'includes/specials/SpecialMostimages.php', + 'MostinterwikisPage' => 'includes/specials/SpecialMostinterwikis.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', + 'NewFilesPager' => 'includes/specials/SpecialNewimages.php', 'NewPagesPager' => 'includes/specials/SpecialNewpages.php', 'PageArchive' => 'includes/specials/SpecialUndelete.php', 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', + 'ProtectedPagesPager' => 'includes/specials/SpecialProtectedpages.php', + 'ProtectedTitlesPager' => 'includes/specials/SpecialProtectedtitles.php', 'RandomPage' => 'includes/specials/SpecialRandompage.php', 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', 'SpecialActiveUsers' => 'includes/specials/SpecialActiveusers.php', @@ -814,6 +909,7 @@ $wgAutoloadLocalClasses = array( 'SpecialBlockList' => 'includes/specials/SpecialBlockList.php', 'SpecialBlockme' => 'includes/specials/SpecialBlockme.php', 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', + 'SpecialCachedPage' => 'includes/specials/SpecialCachedPage.php', 'SpecialCategories' => 'includes/specials/SpecialCategories.php', 'SpecialChangeEmail' => 'includes/specials/SpecialChangeEmail.php', 'SpecialChangePassword' => 'includes/specials/SpecialChangePassword.php', @@ -853,10 +949,11 @@ $wgAutoloadLocalClasses = array( 'SpecialUnlockdb' => 'includes/specials/SpecialUnlockdb.php', 'SpecialUpload' => 'includes/specials/SpecialUpload.php', 'SpecialUploadStash' => 'includes/specials/SpecialUploadStash.php', + 'SpecialUploadStashTooLargeException' => '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', @@ -865,6 +962,8 @@ $wgAutoloadLocalClasses = array( 'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php', 'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php', 'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php', + 'UploadChunkFileException' => 'includes/upload/UploadFromChunks.php', + 'UploadChunkZeroLengthFileException' => 'includes/upload/UploadFromChunks.php', 'UploadForm' => 'includes/specials/SpecialUpload.php', 'UploadSourceField' => 'includes/specials/SpecialUpload.php', 'UserrightsPage' => 'includes/specials/SpecialUserrights.php', @@ -899,9 +998,12 @@ $wgAutoloadLocalClasses = array( 'UploadStashNoSuchKeyException' => 'includes/upload/UploadStash.php', # languages + 'ConverterRule' => 'languages/LanguageConverter.php', 'FakeConverter' => 'languages/Language.php', 'Language' => 'languages/Language.php', 'LanguageConverter' => 'languages/LanguageConverter.php', + 'CLDRPluralRuleEvaluator' => 'languages/utils/CLDRPluralRuleEvaluator.php', + 'CLDRPluralRuleError' => 'languages/utils/CLDRPluralRuleEvaluator.php', # maintenance 'ConvertLinks' => 'maintenance/convertLinks.php', @@ -928,6 +1030,7 @@ $wgAutoloadLocalClasses = array( # maintenance/language 'csvStatsOutput' => 'maintenance/language/StatOutputs.php', + 'extensionLanguages' => 'maintenance/language/languages.inc', 'languages' => 'maintenance/language/languages.inc', 'MessageWriter' => 'maintenance/language/writeMessagesArray.inc', 'statsOutput' => 'maintenance/language/StatOutputs.php', @@ -938,16 +1041,26 @@ $wgAutoloadLocalClasses = array( 'AnsiTermColorer' => 'maintenance/term/MWTerm.php', 'DummyTermColorer' => 'maintenance/term/MWTerm.php', + # mw-config + 'InstallerOverrides' => 'mw-config/overrides.php', + 'MyLocalSettingsGenerator' => 'mw-config/overrides.php', + # tests 'DbTestPreviewer' => 'tests/testHelpers.inc', 'DbTestRecorder' => 'tests/testHelpers.inc', + 'DelayedParserTest' => 'tests/testHelpers.inc', 'TestFileIterator' => 'tests/testHelpers.inc', 'TestRecorder' => 'tests/testHelpers.inc', + # tests/phpunit/includes + 'GenericArrayObjectTest' => 'tests/phpunit/includes/libs/GenericArrayObjectTest.php', + + # tests/phpunit/includes/db + 'ORMRowTest' => 'tests/phpunit/includes/db/ORMRowTest.php', + # tests/parser 'ParserTest' => 'tests/parser/parserTest.inc', 'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php', - 'ParserTestStaticParserHook' => 'tests/parser/parserTestsStaticParserHook.php', # tests/selenium 'Selenium' => 'tests/selenium/Selenium.php', @@ -958,6 +1071,23 @@ $wgAutoloadLocalClasses = array( 'SeleniumTestListener' => 'tests/selenium/SeleniumTestListener.php', 'SeleniumTestSuite' => 'tests/selenium/SeleniumTestSuite.php', 'SeleniumConfig' => 'tests/selenium/SeleniumConfig.php', + + # skins + 'CologneBlueTemplate' => 'skins/CologneBlue.php', + 'ModernTemplate' => 'skins/Modern.php', + 'MonoBookTemplate' => 'skins/MonoBook.php', + 'NostalgiaTemplate' => 'skins/Nostalgia.php', + 'SkinChick' => 'skins/Chick.php', + 'SkinCologneBlue' => 'skins/CologneBlue.php', + 'SkinModern' => 'skins/Modern.php', + 'SkinMonoBook' => 'skins/MonoBook.php', + 'SkinMySkin' => 'skins/MySkin.php', + 'SkinNostalgia' => 'skins/Nostalgia.php', + 'SkinSimple' => 'skins/Simple.php', + 'SkinStandard' => 'skins/Standard.php', + 'SkinVector' => 'skins/Vector.php', + 'StandardTemplate' => 'skins/Standard.php', + 'VectorTemplate' => 'skins/Vector.php', ); class AutoLoader { @@ -972,6 +1102,14 @@ class AutoLoader { static function autoload( $className ) { global $wgAutoloadClasses, $wgAutoloadLocalClasses; + // Workaround for PHP bug <https://bugs.php.net/bug.php?id=49143> (5.3.2. is broken, it's fixed in 5.3.6). + // Strip leading backslashes from class names. When namespaces are used, leading backslashes are used to indicate + // the top-level namespace, e.g. \foo\Bar. When used like this in the code, the leading backslash isn't passed to + // the auto-loader ($className would be 'foo\Bar'). However, if a class is accessed using a string instead of a + // class literal (e.g. $class = '\foo\Bar'; new $class()), then some versions of PHP do not strip the leading + // backlash in this case, causing autoloading to fail. + $className = ltrim( $className, '\\' ); + if ( isset( $wgAutoloadLocalClasses[$className] ) ) { $filename = $wgAutoloadLocalClasses[$className]; } elseif ( isset( $wgAutoloadClasses[$className] ) ) { @@ -1014,6 +1152,7 @@ class AutoLoader { * Sanitizer that have define()s outside of their class definition. Of course * this wouldn't be necessary if everything in MediaWiki was class-based. Sigh. * + * @param $class string * @return Boolean Return the results of class_exists() so we know if we were successful */ static function loadClass( $class ) { diff --git a/includes/Autopromote.php b/includes/Autopromote.php index a2336030..9c77855d 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -1,9 +1,30 @@ <?php /** + * Automatic user rights promotion based on conditions specified + * in $wgAutopromote. + * + * 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 + */ + +/** * This class checks if user can get extra rights * because of conditions specified in $wgAutopromote */ - class Autopromote { /** * Get the groups for the given user based on $wgAutopromote. @@ -32,7 +53,7 @@ class Autopromote { * * Does not return groups the user already belongs to or has once belonged. * - * @param $user The user to get the groups for + * @param $user 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. diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php index d17104f8..05bf3186 100644 --- a/includes/BacklinkCache.php +++ b/includes/BacklinkCache.php @@ -1,7 +1,28 @@ <?php /** - * File for BacklinkCache class + * Class for fetching backlink lists, approximate backlink counts and + * partitions. + * + * 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 + * @author Tim Starling + * @copyright © 2009, Tim Starling, Domas Mituzas + * @copyright © 2010, Max Sem + * @copyright © 2011, Antoine Musso */ /** @@ -18,13 +39,10 @@ * 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, Antoine Musso */ class BacklinkCache { + /** @var ProcessCacheLRU */ + protected static $cache; /** * Multi dimensions array representing batches. Keys are: @@ -65,13 +83,33 @@ class BacklinkCache { /** * Create a new BacklinkCache - * @param Title $title : Title object to create a backlink cache for. + * + * @param Title $title : Title object to create a backlink cache for */ - function __construct( $title ) { + public function __construct( Title $title ) { $this->title = $title; } /** + * Create a new BacklinkCache or reuse any existing one. + * Currently, only one cache instance can exist; callers that + * need multiple backlink cache objects should keep them in scope. + * + * @param Title $title : Title object to get a backlink cache for + * @return BacklinkCache + */ + public static function get( Title $title ) { + if ( !self::$cache ) { // init cache + self::$cache = new ProcessCacheLRU( 1 ); + } + $dbKey = $title->getPrefixedDBkey(); + if ( !self::$cache->has( $dbKey, 'obj' ) ) { + self::$cache->set( $dbKey, 'obj', new self( $title ) ); + } + return self::$cache->get( $dbKey, 'obj' ); + } + + /** * Serialization handler, diasallows to serialize the database to prevent * failures after this class is deserialized from cache with dead DB * connection. @@ -103,7 +141,7 @@ class BacklinkCache { /** * Get the slave connection to the database * When non existing, will initialize the connection. - * @return Database object + * @return DatabaseBase object */ protected function getDB() { if ( !isset( $this->db ) ) { @@ -179,6 +217,7 @@ class BacklinkCache { /** * Get the field name prefix for a given table * @param $table String + * @return null|string */ protected function getPrefix( $table ) { static $prefixes = array( @@ -206,6 +245,7 @@ class BacklinkCache { * Get the SQL condition array for selecting backlinks, with a join * on the page table. * @param $table String + * @return array|null */ protected function getConditions( $table ) { $prefix = $this->getPrefix( $table ); @@ -285,7 +325,7 @@ class BacklinkCache { */ public function partition( $table, $batchSize ) { - // 1) try partition cache ... + // 1) try partition cache ... if ( isset( $this->partitionCache[$table][$batchSize] ) ) { wfDebug( __METHOD__ . ": got from partition cache\n" ); @@ -340,7 +380,7 @@ class BacklinkCache { * Partition a DB result with backlinks in it into batches * @param $res ResultWrapper database result * @param $batchSize integer - * @return array @see + * @return array @see */ protected function partitionResult( $res, $batchSize ) { $batches = array(); diff --git a/includes/Block.php b/includes/Block.php index d80edb5e..732699dc 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -28,11 +28,15 @@ class Block { $mBlockEmail, $mDisableUsertalk, - $mCreateAccount; + $mCreateAccount, + $mParentBlockId; /// @var User|String protected $target; + // @var Integer Hack for foreign blocking (CentralAuth) + protected $forcedTargetID; + /// @var Block::TYPE_ constant. Can only be USER, IP or RANGE internally protected $type; @@ -72,7 +76,7 @@ class Block { $this->setTarget( $address ); if ( $this->target instanceof User && $user ) { - $this->target->setId( $user ); // needed for foreign users + $this->forcedTargetID = $user; // needed for foreign users } if ( $by ) { // local user $this->setBlocker( User::newFromID( $by ) ); @@ -122,18 +126,43 @@ class Block { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->selectRow( 'ipblocks', - '*', + self::selectFields(), array( 'ipb_id' => $id ), __METHOD__ ); if ( $res ) { - return Block::newFromRow( $res ); + return self::newFromRow( $res ); } else { return null; } } /** + * Return the list of ipblocks fields that should be selected to create + * a new block. + * @return array + */ + public static function selectFields() { + return array( + 'ipb_id', + 'ipb_address', + 'ipb_by', + 'ipb_by_text', + 'ipb_reason', + 'ipb_timestamp', + 'ipb_auto', + 'ipb_anon_only', + 'ipb_create_account', + 'ipb_enable_autoblock', + 'ipb_expiry', + 'ipb_deleted', + 'ipb_block_email', + 'ipb_allow_usertalk', + 'ipb_parent_block_id', + ); + } + + /** * 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 * * @@ -243,7 +272,7 @@ class Block { } } - $res = $db->select( 'ipblocks', '*', $conds, __METHOD__ ); + $res = $db->select( 'ipblocks', self::selectFields(), $conds, __METHOD__ ); # 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. @@ -256,7 +285,7 @@ class Block { $bestBlockPreventsEdit = null; foreach( $res as $row ){ - $block = Block::newFromRow( $row ); + $block = self::newFromRow( $row ); # Don't use expired blocks if( $block->deleteIfExpired() ){ @@ -365,6 +394,7 @@ class Block { $this->mAuto = $row->ipb_auto; $this->mHideName = $row->ipb_deleted; $this->mId = $row->ipb_id; + $this->mParentBlockId = $row->ipb_parent_block_id; // I wish I didn't have to do this $db = wfGetDB( DB_SLAVE ); @@ -408,6 +438,7 @@ class Block { } $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', array( 'ipb_parent_block_id' => $this->getId() ), __METHOD__ ); $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->getId() ), __METHOD__ ); return $dbw->affectedRows() > 0; @@ -483,9 +514,15 @@ class Block { } $expiry = $db->encodeExpiry( $this->mExpiry ); + if ( $this->forcedTargetID ) { + $uid = $this->forcedTargetID; + } else { + $uid = $this->target instanceof User ? $this->target->getID() : 0; + } + $a = array( 'ipb_address' => (string)$this->target, - 'ipb_user' => $this->target instanceof User ? $this->target->getID() : 0, + 'ipb_user' => $uid, 'ipb_by' => $this->getBy(), 'ipb_by_text' => $this->getByName(), 'ipb_reason' => $this->mReason, @@ -499,7 +536,8 @@ class Block { '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' ) + 'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ), + 'ipb_parent_block_id' => $this->mParentBlockId ); return $a; @@ -575,7 +613,7 @@ class Block { $key = wfMemcKey( 'ipb', 'autoblock', 'whitelist' ); $lines = $wgMemc->get( $key ); if ( !$lines ) { - $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) ); + $lines = explode( "\n", wfMessage( 'autoblock_whitelist' )->inContentLanguage()->plain() ); $wgMemc->set( $key, $lines, 3600 * 24 ); } @@ -649,7 +687,7 @@ class Block { wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" ); $autoblock->setTarget( $autoblockIP ); $autoblock->setBlocker( $this->getBlocker() ); - $autoblock->mReason = wfMsgForContent( 'autoblocker', $this->getTarget(), $this->mReason ); + $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason )->inContentLanguage()->text(); $timestamp = wfTimestampNow(); $autoblock->mTimestamp = $timestamp; $autoblock->mAuto = 1; @@ -657,6 +695,7 @@ class Block { # Continue suppressing the name if needed $autoblock->mHideName = $this->mHideName; $autoblock->prevents( 'editownusertalk', $this->prevents( 'editownusertalk' ) ); + $autoblock->mParentBlockId = $this->mId; if ( $this->mExpiry == 'infinity' ) { # Original block was indefinite, start an autoblock now @@ -896,7 +935,7 @@ class Block { * Encode expiry for DB * * @param $expiry String: timestamp for expiry, or - * @param $db Database object + * @param $db DatabaseBase object * @return String * @deprecated since 1.18; use $dbw->encodeExpiry() instead */ @@ -964,41 +1003,6 @@ class Block { } /** - * Convert a DB-encoded expiry into a real string that humans can read. - * - * @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 ) { - wfDeprecated( __METHOD__, '1.18' ); - - global $wgContLang; - static $msg = null; - - if ( is_null( $msg ) ) { - $msg = array(); - $keys = array( 'infiniteblock', 'expiringblock' ); - - foreach ( $keys as $key ) { - $msg[$key] = wfMsgHtml( $key ); - } - } - - $expiry = $wgContLang->formatExpiry( $encoded_expiry, TS_MW ); - if ( $expiry == wfGetDB( DB_SLAVE )->getInfinity() ) { - $expirystr = $msg['infiniteblock']; - } else { - global $wgLang; - $expiredatestr = htmlspecialchars( $wgLang->date( $expiry, true ) ); - $expiretimestr = htmlspecialchars( $wgLang->time( $expiry, true ) ); - $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array( $expiredatestr, $expiretimestr ) ); - } - - return $expirystr; - } - - /** * 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 @@ -1066,8 +1070,6 @@ class Block { * @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() ) ){ @@ -1079,6 +1081,8 @@ class Block { return array( null, null ); } + $target = trim( $target ); + 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) diff --git a/includes/CacheHelper.php b/includes/CacheHelper.php new file mode 100644 index 00000000..ac46fc42 --- /dev/null +++ b/includes/CacheHelper.php @@ -0,0 +1,392 @@ +<?php +/** + * Cache of various elements in a single cache entry. + * + * 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 + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +/** + * Interface for all classes implementing CacheHelper functionality. + * + * @since 1.20 + */ +interface ICacheHelper { + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + function setCacheEnabled( $cacheEnabled ); + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + function startCache( $cacheExpiry = null, $cacheEnabled = null ); + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + function getCachedValue( $computeFunction, $args = array(), $key = null ); + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + function saveCache(); + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp + * indicating the point of expiry... + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + function setExpiry( $cacheExpiry ); + +} + +/** + * Helper class for caching various elements in a single cache entry. + * + * To get a cached value or compute it, use getCachedValue like this: + * $this->getCachedValue( $callback ); + * + * To add HTML that should be cached, use addCachedHTML like this: + * $this->addCachedHTML( $callback ); + * + * The callback function is only called when needed, so do all your expensive + * computations here. This function should returns the HTML to be cached. + * It should not add anything to the PageOutput object! + * + * Before the first addCachedHTML call, you should call $this->startCache(); + * After adding the last HTML that should be cached, call $this->saveCache(); + * + * @since 1.20 + */ +class CacheHelper implements ICacheHelper { + + /** + * The time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. + * + * @since 1.20 + * @var integer + */ + protected $cacheExpiry = 3600; + + /** + * List of HTML chunks to be cached (if !hasCached) or that where cached (of hasCached). + * If not cached already, then the newly computed chunks are added here, + * if it as cached already, chunks are removed from this list as they are needed. + * + * @since 1.20 + * @var array + */ + protected $cachedChunks; + + /** + * Indicates if the to be cached content was already cached. + * Null if this information is not available yet. + * + * @since 1.20 + * @var boolean|null + */ + protected $hasCached = null; + + /** + * If the cache is enabled or not. + * + * @since 1.20 + * @var boolean + */ + protected $cacheEnabled = true; + + /** + * Function that gets called when initialization is done. + * + * @since 1.20 + * @var callable + */ + protected $onInitHandler = false; + + /** + * Elements to build a cache key with. + * + * @since 1.20 + * @var array + */ + protected $cacheKey = array(); + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + public function setCacheEnabled( $cacheEnabled ) { + $this->cacheEnabled = $cacheEnabled; + } + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { + if ( is_null( $this->hasCached ) ) { + if ( !is_null( $cacheExpiry ) ) { + $this->cacheExpiry = $cacheExpiry; + } + + if ( !is_null( $cacheEnabled ) ) { + $this->setCacheEnabled( $cacheEnabled ); + } + + $this->initCaching(); + } + } + + /** + * Returns a message that notifies the user he/she is looking at + * a cached version of the page, including a refresh link. + * + * @since 1.20 + * + * @param IContextSource $context + * @param boolean $includePurgeLink + * + * @return string + */ + public function getCachedNotice( IContextSource $context, $includePurgeLink = true ) { + if ( $this->cacheExpiry < 86400 * 3650 ) { + $message = $context->msg( + 'cachedspecial-viewing-cached-ttl', + $context->getLanguage()->formatDuration( $this->cacheExpiry ) + )->escaped(); + } + else { + $message = $context->msg( + 'cachedspecial-viewing-cached-ts' + )->escaped(); + } + + if ( $includePurgeLink ) { + $refreshArgs = $context->getRequest()->getQueryValues(); + unset( $refreshArgs['title'] ); + $refreshArgs['action'] = 'purge'; + + $subPage = $context->getTitle()->getFullText(); + $subPage = explode( '/', $subPage, 2 ); + $subPage = count( $subPage ) > 1 ? $subPage[1] : false; + + $message .= ' ' . Linker::link( + $context->getTitle( $subPage ), + $context->msg( 'cachedspecial-refresh-now' )->escaped(), + array(), + $refreshArgs + ); + } + + return $message; + } + + /** + * Initializes the caching if not already done so. + * Should be called before any of the caching functionality is used. + * + * @since 1.20 + */ + protected function initCaching() { + if ( $this->cacheEnabled && is_null( $this->hasCached ) ) { + $cachedChunks = wfGetCache( CACHE_ANYTHING )->get( $this->getCacheKeyString() ); + + $this->hasCached = is_array( $cachedChunks ); + $this->cachedChunks = $this->hasCached ? $cachedChunks : array(); + + if ( $this->onInitHandler !== false ) { + call_user_func( $this->onInitHandler, $this->hasCached ); + } + } + } + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + public function getCachedValue( $computeFunction, $args = array(), $key = null ) { + $this->initCaching(); + + if ( $this->cacheEnabled && $this->hasCached ) { + $value = null; + + if ( is_null( $key ) ) { + $itemKey = array_keys( array_slice( $this->cachedChunks, 0, 1 ) ); + $itemKey = array_shift( $itemKey ); + + if ( !is_integer( $itemKey ) ) { + wfWarn( "Attempted to get item with non-numeric key while the next item in the queue has a key ($itemKey) in " . __METHOD__ ); + } + elseif ( is_null( $itemKey ) ) { + wfWarn( "Attempted to get an item while the queue is empty in " . __METHOD__ ); + } + else { + $value = array_shift( $this->cachedChunks ); + } + } + else { + if ( array_key_exists( $key, $this->cachedChunks ) ) { + $value = $this->cachedChunks[$key]; + unset( $this->cachedChunks[$key] ); + } + else { + wfWarn( "There is no item with key '$key' in this->cachedChunks in " . __METHOD__ ); + } + } + } + else { + if ( !is_array( $args ) ) { + $args = array( $args ); + } + + $value = call_user_func_array( $computeFunction, $args ); + + if ( $this->cacheEnabled ) { + if ( is_null( $key ) ) { + $this->cachedChunks[] = $value; + } + else { + $this->cachedChunks[$key] = $value; + } + } + } + + return $value; + } + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + public function saveCache() { + if ( $this->cacheEnabled && $this->hasCached === false && !empty( $this->cachedChunks ) ) { + wfGetCache( CACHE_ANYTHING )->set( $this->getCacheKeyString(), $this->cachedChunks, $this->cacheExpiry ); + } + } + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp + * indicating the point of expiry... + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + public function setExpiry( $cacheExpiry ) { + $this->cacheExpiry = $cacheExpiry; + } + + /** + * Returns the cache key to use to cache this page's HTML output. + * Is constructed from the special page name and language code. + * + * @since 1.20 + * + * @return string + * @throws MWException + */ + protected function getCacheKeyString() { + if ( $this->cacheKey === array() ) { + throw new MWException( 'No cache key set, so cannot obtain or save the CacheHelper values.' ); + } + + return call_user_func_array( 'wfMemcKey', $this->cacheKey ); + } + + /** + * Sets the cache key that should be used. + * + * @since 1.20 + * + * @param array $cacheKey + */ + public function setCacheKey( array $cacheKey ) { + $this->cacheKey = $cacheKey; + } + + /** + * Rebuild the content, even if it's already cached. + * This effectively has the same effect as purging the cache, + * since it will be overridden with the new value on the next request. + * + * @since 1.20 + */ + public function rebuildOnDemand() { + $this->hasCached = false; + } + + /** + * Sets a function that gets called when initialization of the cache is done. + * + * @since 1.20 + * + * @param $handlerFunction + */ + public function setOnInitializedHandler( $handlerFunction ) { + $this->onInitHandler = $handlerFunction; + } + +} diff --git a/includes/Category.php b/includes/Category.php index 9d9b5a67..b7b12e8a 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -1,14 +1,33 @@ <?php /** + * Representation for a category. + * + * 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 + * @author Simetrical + */ + +/** * Category objects are immutable, strictly speaking. If you call methods that change the database, * like to refresh link counts, the objects will be appropriately reinitialized. * Member variables are lazy-initialized. * * TODO: Move some stuff from CategoryPage.php to here, and use that. - * - * @author Simetrical */ - class Category { /** Name of the category, normalized to DB-key form */ private $mName = null; @@ -103,7 +122,7 @@ class Category { * Factory function. * * @param $title Title for the category page - * @return category|false on a totally invalid name + * @return Category|bool on a totally invalid name */ public static function newFromTitle( $title ) { $cat = new self(); @@ -185,7 +204,7 @@ class Category { public function getFileCount() { return $this->getX( 'mFiles' ); } /** - * @return Title|false Title for this category, or false on failure. + * @return Title|bool Title for this category, or false on failure. */ public function getTitle() { if ( $this->mTitle ) return $this->mTitle; @@ -231,7 +250,10 @@ class Category { ); } - /** Generic accessor */ + /** + * Generic accessor + * @return bool + */ private function getX( $key ) { if ( !$this->initialize() ) { return false; @@ -257,7 +279,7 @@ class Category { } $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); + $dbw->begin( __METHOD__ ); # Insert the row if it doesn't exist yet (e.g., this is being run via # update.php from a pre-1.16 schema). TODO: This will cause lots and @@ -275,13 +297,13 @@ class Category { 'IGNORE' ); - $cond1 = $dbw->conditional( 'page_namespace=' . NS_CATEGORY, 1, 'NULL' ); - $cond2 = $dbw->conditional( 'page_namespace=' . NS_FILE, 1, 'NULL' ); + $cond1 = $dbw->conditional( array( 'page_namespace' => NS_CATEGORY ), 1, 'NULL' ); + $cond2 = $dbw->conditional( array( 'page_namespace' => NS_FILE ), 1, 'NULL' ); $result = $dbw->selectRow( array( 'categorylinks', 'page' ), - array( 'COUNT(*) AS pages', - "COUNT($cond1) AS subcats", - "COUNT($cond2) AS files" + array( 'pages' => 'COUNT(*)', + 'subcats' => "COUNT($cond1)", + 'files' => "COUNT($cond2)" ), array( 'cl_to' => $this->mName, 'page_id = cl_from' ), __METHOD__, @@ -297,7 +319,7 @@ class Category { array( 'cat_title' => $this->mName ), __METHOD__ ); - $dbw->commit(); + $dbw->commit( __METHOD__ ); # Now we should update our local counts. $this->mPages = $result->pages; diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index eab7a356..32e270e8 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -1,14 +1,26 @@ <?php /** - * Class for viewing MediaWiki category description pages. + * Special handling for category description pages. * Modelled after ImagePage.php. * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file */ -if ( !defined( 'MEDIAWIKI' ) ) - die( 1 ); - /** * Special handling for category description pages, showing pages, * subcategories and file that belong to the category @@ -29,6 +41,7 @@ class CategoryPage extends Article { /** * Constructor from a page id * @param $id Int article ID to load + * @return CategoryPage|null */ public static function newFromID( $id ) { $t = Title::newFromID( $id ); diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index e8e91423..3bb2bc9b 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -1,7 +1,24 @@ <?php - -if ( !defined( 'MEDIAWIKI' ) ) - die( 1 ); +/** + * List and paging of category members. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ class CategoryViewer extends ContextSource { var $limit, $from, $until, @@ -105,7 +122,7 @@ class CategoryViewer extends ContextSource { // Give a proper message if category is empty if ( $r == '' ) { - $r = wfMsgExt( 'category-empty', array( 'parse' ) ); + $r = $this->msg( 'category-empty' )->parseAsBlock(); } $lang = $this->getLanguage(); @@ -172,7 +189,8 @@ class CategoryViewer extends ContextSource { * * @param Title $title * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever). - */ + * @return string + */ function getSubcategorySortChar( $title, $sortkey ) { global $wgContLang; @@ -351,7 +369,7 @@ class CategoryViewer extends ContextSource { if ( $rescnt > 0 ) { # Showing subcategories $r .= "<div id=\"mw-subcategories\">\n"; - $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; + $r .= '<h2>' . $this->msg( 'subcategories' )->text() . "</h2>\n"; $r .= $countmsg; $r .= $this->getSectionPagingLinks( 'subcat' ); $r .= $this->formatList( $this->children, $this->children_start_char ); @@ -365,7 +383,7 @@ class CategoryViewer extends ContextSource { * @return string */ function getPagesSection() { - $ti = htmlspecialchars( $this->title->getText() ); + $ti = wfEscapeWikiText( $this->title->getText() ); # Don't show articles section if there are none. $r = ''; @@ -380,7 +398,7 @@ class CategoryViewer extends ContextSource { if ( $rescnt > 0 ) { $r = "<div id=\"mw-pages\">\n"; - $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; + $r .= '<h2>' . $this->msg( 'category_header', $ti )->text() . "</h2>\n"; $r .= $countmsg; $r .= $this->getSectionPagingLinks( 'page' ); $r .= $this->formatList( $this->articles, $this->articles_start_char ); @@ -401,7 +419,7 @@ class CategoryViewer extends ContextSource { $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); $r .= "<div id=\"mw-category-media\">\n"; - $r .= '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n"; + $r .= '<h2>' . $this->msg( 'category-media-header', wfEscapeWikiText( $this->title->getText() ) )->text() . "</h2>\n"; $r .= $countmsg; $r .= $this->getSectionPagingLinks( 'file' ); if ( $this->showGallery ) { @@ -486,11 +504,11 @@ class CategoryViewer extends ContextSource { # Split into three columns $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ ); - $ret = '<table width="100%"><tr valign="top">'; + $ret = '<table style="width: 100%;"><tr style="vertical-align: top;">'; $prevchar = null; foreach ( $columns as $column ) { - $ret .= '<td width="33.3%">'; + $ret .= '<td style="width: 33.3%;">'; $colContents = array(); # Kind of like array_flip() here, but we keep duplicates in an @@ -508,7 +526,7 @@ class CategoryViewer extends ContextSource { if ( $first && $char === $prevchar ) { # We're continuing a previous chunk at the top of a new # column, so add " cont." after the letter. - $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' ); + $ret .= ' ' . wfMessage( 'listingcontinuesabbrev' )->escaped(); } $ret .= "</h3>\n"; @@ -558,7 +576,7 @@ class CategoryViewer extends ContextSource { * @return String HTML */ private function pagingLinks( $first, $last, $type = '' ) { - $prevLink = wfMessage( 'prevn' )->numParams( $this->limit )->escaped(); + $prevLink = $this->msg( 'prevn' )->numParams( $this->limit )->escaped(); if ( $first != '' ) { $prevQuery = $this->query; @@ -572,7 +590,7 @@ class CategoryViewer extends ContextSource { ); } - $nextLink = wfMessage( 'nextn' )->numParams( $this->limit )->escaped(); + $nextLink = $this->msg( 'nextn' )->numParams( $this->limit )->escaped(); if ( $last != '' ) { $lastQuery = $this->query; @@ -586,7 +604,7 @@ class CategoryViewer extends ContextSource { ); } - return "($prevLink) ($nextLink)"; + return $this->msg('categoryviewer-pagedlinks')->rawParams($prevLink, $nextLink)->escaped(); } /** @@ -670,8 +688,8 @@ class CategoryViewer extends ContextSource { $this->cat->refreshCounts(); } else { # Case 3: hopeless. Don't give a total count at all. - return wfMessage( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock(); + return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock(); } - return wfMessage( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock(); + return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock(); } } diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index 4a8ed709..e2b6a0ca 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -1,5 +1,26 @@ <?php /** + * Recent changes filtering by category. + * + * 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 + */ + +/** * The "Categoryfinder" class takes a list of articles, creates an internal * representation of all their parent categories (as well as parents of * parents etc.). From this representation, it determines which of these diff --git a/includes/Cdb.php b/includes/Cdb.php index 94aa1925..ae2e5b18 100644 --- a/includes/Cdb.php +++ b/includes/Cdb.php @@ -1,6 +1,21 @@ <?php /** - * Native CDB file reader and writer + * Native CDB file reader and writer. + * + * 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 */ diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php index 53175272..02be65f3 100644 --- a/includes/Cdb_PHP.php +++ b/includes/Cdb_PHP.php @@ -6,6 +6,21 @@ * * Exception thrown if sizes or offsets are between 2GB and 4GB * * Some variables renamed * + * 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 */ @@ -51,7 +66,7 @@ class CdbFunctions { /** * The CDB hash function. * - * @param $s + * @param $s string * * @return */ diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index 63d37327..0ebc926f 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -1,9 +1,25 @@ <?php /** - * Functions related to change tags. + * Recent changes tagging. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html * * @file */ + class ChangeTags { /** @@ -19,6 +35,8 @@ class ChangeTags { * */ static function formatSummaryRow( $tags, $page ) { + global $wgLang; + if( !$tags ) return array( '', array() ); @@ -35,7 +53,7 @@ class ChangeTags { ); $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" ); } - $markers = '(' . implode( ', ', $displayTags ) . ')'; + $markers = wfMessage( 'parentheses' )->rawParams( $wgLang->commaList( $displayTags ) )->text(); $markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers ); return array( $markers, $classes ); @@ -209,17 +227,17 @@ class ChangeTags { if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) return $fullForm ? '' : array(); - $data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMsgExt( 'tag-filter', 'parseinline' ) ), - Xml::input( 'tagfilter', 20, $selected ) ); + $data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMessage( 'tag-filter' )->parse() ), + Xml::input( 'tagfilter', 20, $selected, array( 'class' => 'mw-tagfilter-input' ) ) ); if ( !$fullForm ) { return $data; } $html = implode( ' ', $data ); - $html .= "\n" . Xml::element( 'input', array( 'type' => 'submit', 'value' => wfMsg( 'tag-filter-submit' ) ) ); + $html .= "\n" . Xml::element( 'input', array( 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() ) ); $html .= "\n" . Html::hidden( 'title', $title->getPrefixedText() ); - $html = Xml::tags( 'form', array( 'action' => $title->getLocalURL(), 'method' => 'get' ), $html ); + $html = Xml::tags( 'form', array( 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ), $html ); return $html; } diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index bcedf2f3..ee4c2d64 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -1,4 +1,24 @@ <?php +/** + * Feed for list of changes. + * + * 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 + */ /** * Feed to Special:RecentChanges and Special:RecentChangesLiked @@ -51,13 +71,13 @@ class ChangesFeed { * @param $rows ResultWrapper object with rows in recentchanges table * @param $lastmod Integer: timestamp of the last item in the recentchanges table (only used for the cache key) * @param $opts FormOptions as in SpecialRecentChanges::getDefaultOptions() - * @return null or true + * @return null|bool True or null */ public function execute( $feed, $rows, $lastmod, $opts ) { global $wgLang, $wgRenderHashAppend; if ( !FeedUtils::checkFeedOutput( $this->format ) ) { - return; + return null; } $optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend; @@ -107,7 +127,7 @@ class ChangesFeed { * @param $lastmod Integer: timestamp of the last item in the recentchanges table * @param $timekey String: memcached key of the last modification * @param $key String: memcached key of the content - * @return feed's content on cache hit or false on cache miss + * @return string|bool feed's content on cache hit or false on cache miss */ public function loadFromCache( $lastmod, $timekey, $key ) { global $wgFeedCacheTimeout, $wgOut, $messageMemc; @@ -186,7 +206,7 @@ class ChangesFeed { FeedUtils::formatDiff( $obj ), $url, $obj->rc_timestamp, - ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text, + ( $obj->rc_deleted & Revision::DELETED_USER ) ? wfMessage( 'rev-deleted-user' )->escaped() : $obj->rc_user_text, $talkpage ); $feed->outItem( $item ); diff --git a/includes/ChangesList.php b/includes/ChangesList.php index fd97e0cb..84677124 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -1,10 +1,27 @@ <?php /** - * Classes to show various lists of changes: + * Classes to show lists of changes. + * + * These can be: * - watchlist * - related changes * - recent changes * + * 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 */ @@ -63,7 +80,7 @@ class ChangesList extends ContextSource { * This first argument used to be an User object. * * @deprecated in 1.18; use newFromContext() instead - * @param $unused Unused + * @param $unused string|User Unused * @return ChangesList|EnhancedChangesList|OldChangesList derivative */ public static function newFromUser( $unused ) { @@ -91,7 +108,7 @@ class ChangesList extends ContextSource { } /** - * Sets the list to use a <li class="watchlist-(namespace)-(page)"> tag + * Sets the list to use a "<li class='watchlist-(namespace)-(page)'>" tag * @param $value Boolean */ public function setWatchlistDivs( $value = true ) { @@ -106,7 +123,7 @@ class ChangesList extends ContextSource { if( !isset( $this->message ) ) { foreach ( explode( ' ', 'cur diff hist last blocklink history ' . 'semicolon-separator pipe-separator' ) as $msg ) { - $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); + $this->message[$msg] = $this->msg( $msg )->escaped(); } } } @@ -128,7 +145,7 @@ class ChangesList extends ContextSource { } /** - * Provide the <abbr> element appropriate to a given abbreviated flag, + * Provide the "<abbr>" element appropriate to a given abbreviated flag, * namely the flag indicating a new page, a minor edit, a bot edit, or an * unpatrolled edit. By default in English it will contain "N", "m", "b", * "!" respectively, plus it will have an appropriate title and class. @@ -146,8 +163,8 @@ class ChangesList extends ContextSource { 'unpatrolled' => array( 'unpatrolledletter', 'recentchanges-label-unpatrolled' ), ); foreach( $messages as &$value ) { - $value[0] = wfMsgExt( $value[0], 'escapenoentities' ); - $value[1] = wfMsgExt( $value[1], 'escapenoentities' ); + $value[0] = wfMessage( $value[0] )->escaped(); + $value[1] = wfMessage( $value[1] )->escaped(); } } @@ -175,6 +192,7 @@ class ChangesList extends ContextSource { $this->rcCacheIndex = 0; $this->lastdate = ''; $this->rclistOpen = false; + $this->getOutput()->addModuleStyles( 'mediawiki.special.changeslist' ); return ''; } @@ -182,22 +200,31 @@ class ChangesList extends ContextSource { * Show formatted char difference * @param $old Integer: bytes * @param $new Integer: bytes + * @param $context IContextSource context to use * @return String */ - public static function showCharacterDifference( $old, $new ) { - global $wgRCChangedSizeThreshold, $wgLang, $wgMiserMode; + public static function showCharacterDifference( $old, $new, IContextSource $context = null ) { + global $wgRCChangedSizeThreshold, $wgMiserMode; + + if ( !$context ) { + $context = RequestContext::getMain(); + } + + $new = (int)$new; + $old = (int)$old; $szdiff = $new - $old; - $code = $wgLang->getCode(); + $lang = $context->getLanguage(); + $code = $lang->getCode(); static $fastCharDiff = array(); if ( !isset($fastCharDiff[$code]) ) { - $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1'; + $fastCharDiff[$code] = $wgMiserMode || $context->msg( 'rc-change-size' )->plain() === '$1'; } - $formattedSize = $wgLang->formatNum($szdiff); + $formattedSize = $lang->formatNum( $szdiff ); if ( !$fastCharDiff[$code] ) { - $formattedSize = wfMsgExt( 'rc-change-size', array( 'parsemag' ), $formattedSize ); + $formattedSize = $context->msg( 'rc-change-size', $formattedSize )->text(); } if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { @@ -217,11 +244,34 @@ class ChangesList extends ContextSource { $formattedSizeClass = 'mw-plusminus-neg'; } - $formattedTotalSize = wfMsgExt( 'rc-change-size-new', 'parsemag', $wgLang->formatNum( $new ) ); + $formattedTotalSize = $context->msg( 'rc-change-size-new' )->numParams( $new )->text(); return Html::element( $tag, array( 'dir' => 'ltr', 'class' => $formattedSizeClass, 'title' => $formattedTotalSize ), - wfMessage( 'parentheses', $formattedSize )->plain() ) . $wgLang->getDirMark(); + $context->msg( 'parentheses', $formattedSize )->plain() ) . $lang->getDirMark(); + } + + /** + * Format the character difference of one or several changes. + * + * @param $old RecentChange + * @param $new RecentChange last change to use, if not provided, $old will be used + * @return string HTML fragment + */ + public function formatCharacterDifference( RecentChange $old, RecentChange $new = null ) { + $oldlen = $old->mAttribs['rc_old_len']; + + if ( $new ) { + $newlen = $new->mAttribs['rc_new_len']; + } else { + $newlen = $old->mAttribs['rc_new_len']; + } + + if( $oldlen === null || $newlen === null ) { + return ''; + } + + return self::showCharacterDifference( $oldlen, $newlen, $this->getContext() ); } /** @@ -238,7 +288,7 @@ class ChangesList extends ContextSource { public function insertDateHeader( &$s, $rc_timestamp ) { # Make date header if necessary - $date = $this->getLanguage()->date( $rc_timestamp, true, true ); + $date = $this->getLanguage()->userDate( $rc_timestamp, $this->getUser() ); if( $date != $this->lastdate ) { if( $this->lastdate != '' ) { $s .= "</ul>\n"; @@ -252,7 +302,7 @@ class ChangesList extends ContextSource { public function insertLog( &$s, $title, $logtype ) { $page = new LogPage( $logtype ); $logname = $page->getName()->escaped(); - $s .= '(' . Linker::linkKnown( $title, $logname ) . ')'; + $s .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $title, $logname ) )->escaped(); } /** @@ -284,9 +334,9 @@ class ChangesList extends ContextSource { $query ); } - $s .= '(' . $diffLink . $this->message['pipe-separator']; + $diffhist = $diffLink . $this->message['pipe-separator']; # History link - $s .= Linker::linkKnown( + $diffhist .= Linker::linkKnown( $rc->getTitle(), $this->message['hist'], array(), @@ -295,7 +345,7 @@ class ChangesList extends ContextSource { 'action' => 'history' ) ); - $s .= ') . . '; + $s .= $this->msg( 'parentheses' )->rawParams( $diffhist )->escaped() . ' <span class="mw-changeslist-separator">. .</span> '; } /** @@ -316,16 +366,14 @@ class ChangesList extends ContextSource { $articlelink = Linker::linkKnown( $rc->getTitle(), null, - array(), + array( 'class' => 'mw-changeslist-title' ), $params ); if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) { $articlelink = '<span class="history-deleted">' . $articlelink . '</span>'; } - # Bolden pages watched by this user - if( $watched ) { - $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>"; - } + # To allow for boldening pages watched by this user + $articlelink = "<span class=\"mw-title\">{$articlelink}</span>"; # RTL/LTR marker $articlelink .= $this->getLanguage()->getDirMark(); @@ -340,8 +388,8 @@ class ChangesList extends ContextSource { * @param $rc RecentChange */ public function insertTimestamp( &$s, $rc ) { - $s .= $this->message['semicolon-separator'] . - $this->getLanguage()->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; + $s .= $this->message['semicolon-separator'] . '<span class="mw-changeslist-date">' . + $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() ) . '</span> <span class="mw-changeslist-separator">. .</span> '; } /** @@ -352,7 +400,7 @@ class ChangesList extends ContextSource { */ 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">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>'; } else { $s .= $this->getLanguage()->getDirMark() . Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); @@ -364,22 +412,25 @@ class ChangesList extends ContextSource { * Insert a formatted action * * @param $rc RecentChange + * @return string */ public function insertLogEntry( $rc ) { $formatter = LogFormatter::newFromRow( $rc->mAttribs ); + $formatter->setContext( $this->getContext() ); $formatter->setShowUserToolLinks( true ); $mark = $this->getLanguage()->getDirMark(); return $formatter->getActionText() . " $mark" . $formatter->getComment(); } - /** + /** * Insert a formatted comment * @param $rc RecentChange + * @return string */ public function insertComment( $rc ) { if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) { - return ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>'; + return ' <span class="history-deleted">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span>'; } else { return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); } @@ -397,13 +448,13 @@ class ChangesList extends ContextSource { /** * Returns the string which indicates the number of watching users + * @return string */ protected function numberofWatchingusers( $count ) { static $cache = array(); if( $count > 0 ) { if( !isset( $cache[$count] ) ) { - $cache[$count] = wfMsgExt( 'number_of_watching_users_RCview', - array('parsemag', 'escape' ), $this->getLanguage()->formatNum( $count ) ); + $cache[$count] = $this->msg( 'number_of_watching_users_RCview' )->numParams( $count )->escaped(); } return $cache[$count]; } else { @@ -456,7 +507,7 @@ class ChangesList extends ContextSource { * @param $rc RecentChange */ public function insertRollback( &$s, &$rc ) { - if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) { + if( $rc->mAttribs['rc_type'] != 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 */ @@ -497,7 +548,7 @@ class ChangesList extends ContextSource { if ( !$rc->mAttribs['rc_patrolled'] ) { if ( $this->getUser()->useRCPatrol() ) { $unpatrolled = true; - } elseif ( $this->getUser()->useNPPatrol() && $rc->mAttribs['rc_new'] ) { + } elseif ( $this->getUser()->useNPPatrol() && $rc->mAttribs['rc_type'] == RC_NEW ) { $unpatrolled = true; } } @@ -513,7 +564,10 @@ class OldChangesList extends ChangesList { /** * Format a line using the old system (aka without any javascript). * - * @param $rc RecentChange + * @param $rc RecentChange, passed by reference + * @param $watched Bool (default false) + * @param $linenumber Int (default null) + * @return string */ public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { global $wgRCShowChangedSize; @@ -537,11 +591,15 @@ class OldChangesList extends ChangesList { } } + // Indicate watched status on the line to allow for more + // comprehensive styling. + $classes[] = $watched ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched'; + // Moved pages (very very old, not supported anymore) if( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) { // Log entries } elseif( $rc->mAttribs['rc_log_type'] ) { - $logtitle = Title::newFromText( 'Log/'.$rc->mAttribs['rc_log_type'], NS_SPECIAL ); + $logtitle = SpecialPage::getTitleFor( 'Log', $rc->mAttribs['rc_log_type'] ); $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 ) { @@ -555,7 +613,7 @@ class OldChangesList extends ChangesList { # M, N, b and ! (minor, new, bot and unpatrolled) $s .= $this->recentChangesFlags( array( - 'newpage' => $rc->mAttribs['rc_new'], + 'newpage' => $rc->mAttribs['rc_type'] == RC_NEW, 'minor' => $rc->mAttribs['rc_minor'], 'unpatrolled' => $unpatrolled, 'bot' => $rc->mAttribs['rc_bot'] @@ -567,10 +625,10 @@ class OldChangesList extends ChangesList { # Edit/log timestamp $this->insertTimestamp( $s, $rc ); # Bytes added or removed - if( $wgRCShowChangedSize ) { - $cd = $rc->getCharacterDifference(); - if( $cd != '' ) { - $s .= "$cd . . "; + if ( $wgRCShowChangedSize ) { + $cd = $this->formatCharacterDifference( $rc ); + if ( $cd !== '' ) { + $s .= $cd . ' <span class="mw-changeslist-separator">. .</span> '; } } @@ -593,8 +651,7 @@ class OldChangesList extends ChangesList { # How many users watch this page if( $rc->numberofWatchingusers > 0 ) { - $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', - array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $rc->numberofWatchingusers ) ); + $s .= ' ' . $this->numberofWatchingusers( $rc->numberofWatchingusers ); } if( $this->watchlist ) { @@ -646,7 +703,7 @@ class EnhancedChangesList extends ChangesList { $curIdEq = array( 'curid' => $rc->mAttribs['rc_cur_id'] ); # If it's a new day, add the headline and flush the cache - $date = $this->getLanguage()->date( $rc->mAttribs['rc_timestamp'], true ); + $date = $this->getLanguage()->userDate( $rc->mAttribs['rc_timestamp'], $this->getUser() ); $ret = ''; if( $date != $this->lastdate ) { # Process current cache @@ -675,7 +732,7 @@ class EnhancedChangesList extends ChangesList { $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); $logpage = new LogPage( $logType ); $logname = $logpage->getName()->escaped(); - $clink = '(' . Linker::linkKnown( $logtitle, $logname ) . ')'; + $clink = $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $logtitle, $logname ) )->escaped(); } else { $clink = Linker::link( $rc->getTitle() ); } @@ -694,7 +751,7 @@ class EnhancedChangesList extends ChangesList { $showdifflinks = false; } - $time = $this->getLanguage()->time( $rc->mAttribs['rc_timestamp'], true, true ); + $time = $this->getLanguage()->userTime( $rc->mAttribs['rc_timestamp'], $this->getUser() ); $rc->watched = $watched; $rc->link = $clink; $rc->timestamp = $time; @@ -743,7 +800,7 @@ class EnhancedChangesList extends ChangesList { # Make user links if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) { - $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; + $rc->userlink = ' <span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>'; } else { $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'] ); @@ -779,6 +836,7 @@ class EnhancedChangesList extends ChangesList { /** * Enhanced RC group + * @return string */ protected function recentChangesBlockGroup( $block ) { global $wgRCShowChangedSize; @@ -786,14 +844,16 @@ class EnhancedChangesList extends ChangesList { wfProfileIn( __METHOD__ ); # Add the namespace and title of the block as part of the class + $classes = array( 'mw-collapsible', 'mw-collapsed', 'mw-enhanced-rc' ); if ( $block[0]->mAttribs['rc_log_type'] ) { # Log entry - $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' + $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] ); } else { - $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' + $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); } + $classes[] = $block[0]->watched ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched'; $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); @@ -808,7 +868,7 @@ class EnhancedChangesList extends ChangesList { $allLogs = true; foreach( $block as $rcObj ) { $oldid = $rcObj->mAttribs['rc_last_oldid']; - if( $rcObj->mAttribs['rc_new'] ) { + if( $rcObj->mAttribs['rc_type'] == RC_NEW ) { $isnew = true; } // If all log actions to this page were hidden, then don't @@ -847,24 +907,17 @@ class EnhancedChangesList extends ChangesList { $text = $userlink; $text .= $this->getLanguage()->getDirMark(); if( $count > 1 ) { - $text .= ' (' . $this->getLanguage()->formatNum( $count ) . '×)'; + $text .= ' ' . $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->formatNum( $count ) . '×' )->escaped(); } array_push( $users, $text ); } - $users = ' <span class="changedby">[' . - implode( $this->message['semicolon-separator'], $users ) . ']</span>'; + $users = ' <span class="changedby">' + . $this->msg( 'brackets' )->rawParams( + implode( $this->message['semicolon-separator'], $users ) + )->escaped() . '</span>'; - # Title for <a> tags - $expandTitle = htmlspecialchars( wfMsg( 'rc-enhanced-expand' ) ); - $closeTitle = htmlspecialchars( wfMsg( 'rc-enhanced-hide' ) ); - - $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>"; + $tl = '<span class="mw-collapsible-toggle mw-enhancedchanges-arrow"></span>'; $r .= "<td>$tl</td>"; # Main line @@ -880,7 +933,7 @@ class EnhancedChangesList extends ChangesList { # Article link if( $namehidden ) { - $r .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>'; + $r .= ' <span class="history-deleted">' . $this->msg( 'rev-deleted-event' )->escaped() . '</span>'; } elseif( $allLogs ) { $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); } else { @@ -894,22 +947,22 @@ class EnhancedChangesList extends ChangesList { $n = count($block); static $nchanges = array(); if ( !isset( $nchanges[$n] ) ) { - $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $n ) ); + $nchanges[$n] = $this->msg( 'nchanges' )->numParams( $n )->escaped(); } # Total change link $r .= ' '; + $logtext = ''; if( !$allLogs ) { - $r .= '('; if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) { - $r .= $nchanges[$n]; + $logtext .= $nchanges[$n]; } elseif( $isnew ) { - $r .= $nchanges[$n]; + $logtext .= $nchanges[$n]; } else { $params = $queryParams; $params['diff'] = $currentRevision; $params['oldid'] = $oldid; - $r .= Linker::link( + $logtext .= Linker::link( $block[0]->getTitle(), $nchanges[$n], array(), @@ -923,20 +976,25 @@ class EnhancedChangesList extends ChangesList { if( $allLogs ) { // don't show history link for logs } elseif( $namehidden || !$block[0]->getTitle()->exists() ) { - $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')'; + $logtext .= $this->message['pipe-separator'] . $this->message['hist']; } else { $params = $queryParams; $params['action'] = 'history'; - $r .= $this->message['pipe-separator'] . + $logtext .= $this->message['pipe-separator'] . Linker::linkKnown( $block[0]->getTitle(), $this->message['hist'], array(), $params - ) . ')'; + ); } - $r .= ' . . '; + + if( $logtext !== '' ) { + $r .= $this->msg( 'parentheses' )->rawParams( $logtext )->escaped(); + } + + $r .= ' <span class="mw-changeslist-separator">. .</span> '; # Character difference (does not apply if only log items) if( $wgRCShowChangedSize && !$allLogs ) { @@ -950,13 +1008,12 @@ class EnhancedChangesList extends ChangesList { $first--; } # Get net change - $chardiff = $rcObj->getCharacterDifference( $block[$first]->mAttribs['rc_old_len'], - $block[$last]->mAttribs['rc_new_len'] ); + $chardiff = $this->formatCharacterDifference( $block[$first], $block[$last] ); if( $chardiff == '' ) { $r .= ' '; } else { - $r .= ' ' . $chardiff. ' . . '; + $r .= ' ' . $chardiff. ' <span class="mw-changeslist-separator">. .</span> '; } } @@ -969,10 +1026,9 @@ class EnhancedChangesList extends ChangesList { $classes = array(); $type = $rcObj->mAttribs['rc_type']; - #$r .= '<tr><td valign="top">'.$this->spacerArrow(); $r .= '<tr><td></td><td class="mw-enhanced-rc">'; $r .= $this->recentChangesFlags( array( - 'newpage' => $rcObj->mAttribs['rc_new'], + 'newpage' => $type == RC_NEW, 'minor' => $rcObj->mAttribs['rc_minor'], 'unpatrolled' => $rcObj->unpatrolled, 'bot' => $rcObj->mAttribs['rc_bot'], @@ -1008,17 +1064,16 @@ class EnhancedChangesList extends ChangesList { $r .= $link . '</span>'; if ( !$type == RC_LOG || $type == RC_NEW ) { - $r .= ' ('; - $r .= $rcObj->curlink; - $r .= $this->message['pipe-separator']; - $r .= $rcObj->lastlink; - $r .= ')'; + $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->curlink . $this->message['pipe-separator'] . $rcObj->lastlink )->escaped(); } - $r .= ' . . '; + $r .= ' <span class="mw-changeslist-separator">. .</span> '; # Character diff - if( $wgRCShowChangedSize && $rcObj->getCharacterDifference() ) { - $r .= $rcObj->getCharacterDifference() . ' . . ' ; + if ( $wgRCShowChangedSize ) { + $cd = $this->formatCharacterDifference( $rcObj ); + if ( $cd !== '' ) { + $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> '; + } } if ( $rcObj->mAttribs['rc_type'] == RC_LOG ) { @@ -1051,7 +1106,7 @@ class EnhancedChangesList extends ChangesList { * @param $dir String: one of '', 'd', 'l', 'r' * @param $alt String: text * @param $title String: text - * @return String: HTML <img> tag + * @return String: HTML "<img>" tag */ protected function arrow( $dir, $alt='', $title='' ) { global $wgStylePath; @@ -1064,26 +1119,25 @@ class EnhancedChangesList extends ChangesList { /** * Generate HTML for a right- or left-facing arrow, * depending on language direction. - * @return String: HTML <img> tag + * @return String: HTML "<img>" tag */ protected function sideArrow() { - global $wgLang; - $dir = $wgLang->isRTL() ? 'l' : 'r'; - return $this->arrow( $dir, '+', wfMsg( 'rc-enhanced-expand' ) ); + $dir = $this->getLanguage()->isRTL() ? 'l' : 'r'; + return $this->arrow( $dir, '+', $this->msg( 'rc-enhanced-expand' )->text() ); } /** * Generate HTML for a down-facing arrow * depending on language direction. - * @return String: HTML <img> tag + * @return String: HTML "<img>" tag */ protected function downArrow() { - return $this->arrow( 'd', '-', wfMsg( 'rc-enhanced-hide' ) ); + return $this->arrow( 'd', '-', $this->msg( 'rc-enhanced-hide' )->text() ); } /** * Generate HTML for a spacer image - * @return String: HTML <img> tag + * @return String: HTML "<img>" tag */ protected function spacerArrow() { return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space @@ -1103,18 +1157,20 @@ class EnhancedChangesList extends ChangesList { $type = $rcObj->mAttribs['rc_type']; $logType = $rcObj->mAttribs['rc_log_type']; + $classes = array( 'mw-enhanced-rc' ); if( $logType ) { # Log entry - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' + $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType . '-' . $rcObj->mAttribs['rc_title'] ); } else { - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . + $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' . $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] ); } + $classes[] = $rcObj->watched ? 'mw-changeslist-line-watched' : 'mw-changeslist-line-not-watched'; $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); - $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow(); + $r .= '<td class="mw-enhanced-rc"><span class="mw-enhancedchanges-arrow mw-enhancedchanges-arrow-space"></span>'; # Flag and Timestamp if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { $r .= '    '; // 4 flags -> 4 spaces @@ -1129,39 +1185,41 @@ class EnhancedChangesList extends ChangesList { $r .= ' '.$rcObj->timestamp.' </td><td>'; # Article or log link if( $logType ) { - $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); - $logname = LogPage::logName( $logType ); - $r .= '(' . Linker::linkKnown( $logtitle, htmlspecialchars( $logname ) ) . ')'; + $logPage = new LogPage( $logType ); + $logTitle = SpecialPage::getTitleFor( 'Log', $logType ); + $logName = $logPage->getName()->escaped(); + $r .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $logTitle, $logName ) )->escaped(); } else { $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched ); } # Diff and hist links if ( $type != RC_LOG ) { - $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator']; $query['action'] = 'history'; - $r .= Linker::linkKnown( + $r .= ' ' . $this->msg( 'parentheses' )->rawParams( $rcObj->difflink . $this->message['pipe-separator'] . Linker::linkKnown( $rcObj->getTitle(), $this->message['hist'], array(), $query - ) . ')'; + ) )->escaped(); } - $r .= ' . . '; + $r .= ' <span class="mw-changeslist-separator">. .</span> '; # Character diff - if( $wgRCShowChangedSize && ($cd = $rcObj->getCharacterDifference()) ) { - $r .= "$cd . . "; + if ( $wgRCShowChangedSize ) { + $cd = $this->formatCharacterDifference( $rcObj ); + if ( $cd !== '' ) { + $r .= $cd . ' <span class="mw-changeslist-separator">. .</span> '; + } } if ( $type == RC_LOG ) { $r .= $this->insertLogEntry( $rcObj ); - } else { + } else { $r .= ' '.$rcObj->userlink . $rcObj->usertalklink; $r .= $this->insertComment( $rcObj ); - $r .= $this->insertRollback( $r, $rcObj ); + $this->insertRollback( $r, $rcObj ); } # Tags - $classes = explode( ' ', $classes ); $this->insertTags( $r, $rcObj, $classes ); # Show how many people are watching this if enabled $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers); diff --git a/includes/Collation.php b/includes/Collation.php index 0c510b78..ad2b94b1 100644 --- a/includes/Collation.php +++ b/includes/Collation.php @@ -1,4 +1,24 @@ <?php +/** + * Database row sorting. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ abstract class Collation { static $instance; @@ -311,7 +331,7 @@ class IcuCollation extends Collation { * -1, 0 or 1 in the style of strcmp(). * @param $target string The target value to find. * - * @return The item index of the lower bound, or false if the target value + * @return int|bool The item index of the lower bound, or false if the target value * sorts before all items. */ function findLowerBound( $valueCallback, $valueCount, $comparisonCallback, $target ) { diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php index 42a7173d..b68fc762 100644 --- a/includes/ConfEditor.php +++ b/includes/ConfEditor.php @@ -1,4 +1,24 @@ <?php +/** + * Configuration file editor. + * + * 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 + */ /** * This is a state machine style parser with two internal stacks: @@ -139,6 +159,7 @@ class ConfEditor { * insert * Insert a new element at the start of the array. * + * @return string */ public function edit( $ops ) { $this->parse(); @@ -371,6 +392,7 @@ 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 * break, and any comments in between. + * @return array */ function findDeletionRegion( $pathName ) { if ( !isset( $this->pathInfo[$pathName] ) ) { @@ -428,6 +450,7 @@ class ConfEditor { * or semicolon. * * The end position is the past-the-end (end + 1) value as per convention. + * @return array */ function findValueRegion( $pathName ) { if ( !isset( $this->pathInfo[$pathName] ) ) { @@ -444,6 +467,7 @@ class ConfEditor { * Find the path name of the last 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. + * @return bool|int|string */ function findLastArrayElement( $path ) { // Try for a real element @@ -480,6 +504,7 @@ class ConfEditor { * 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. + * @return bool|int|string */ function findFirstArrayElement( $path ) { // Try for an ordinary element @@ -504,6 +529,7 @@ class ConfEditor { /** * Get the indent string which sits after a given start position. * Returns false if the position is not at the start of the line. + * @return array */ function getIndent( $pos, $key = false, $arrowPos = false ) { $arrowIndent = ' '; @@ -725,6 +751,7 @@ class ConfEditor { /** * Create a ConfEditorToken from an element of token_get_all() + * @return ConfEditorToken */ function newTokenObj( $internalToken ) { if ( is_array( $internalToken ) ) { @@ -776,6 +803,7 @@ class ConfEditor { /** * Get the token $offset steps ahead of the current position. * $offset may be negative, to get tokens behind the current position. + * @return ConfEditorToken */ function getTokenAhead( $offset ) { $pos = $this->pos + $offset; @@ -821,6 +849,7 @@ class ConfEditor { /** * Pop a state from the state stack. + * @return mixed */ function popState() { return array_pop( $this->stateStack ); @@ -829,6 +858,7 @@ class ConfEditor { /** * Returns true if the user input path is valid. * This exists to allow "/" and "@" to be reserved for string path keys + * @return bool */ function validatePath( $path ) { return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; @@ -949,6 +979,7 @@ class ConfEditor { /** * Get a readable name for the given token type. + * @return string */ function getTypeName( $type ) { if ( is_int( $type ) ) { @@ -962,6 +993,7 @@ 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 * whitespace. + * @return bool */ function isAhead( $type, $offset = 0 ) { $ahead = $offset; diff --git a/includes/Cookie.php b/includes/Cookie.php index 76739ccc..7984d63e 100644 --- a/includes/Cookie.php +++ b/includes/Cookie.php @@ -1,6 +1,24 @@ <?php /** - * @defgroup HTTP HTTP + * Cookie for HTTP requests. + * + * 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 HTTP */ class Cookie { @@ -62,8 +80,8 @@ class Cookie { * 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) + * @todo fixme fails to detect 3-letter top-level domains + * @todo 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 @@ -193,6 +211,7 @@ class CookieJar { /** * @see Cookie::serializeToHttpRequest + * @return string */ public function serializeToHttpRequest( $path, $domain ) { $cookies = array(); @@ -213,6 +232,7 @@ class CookieJar { * * @param $cookie String * @param $domain String: cookie's domain + * @return null */ public function parseCookieResponseHeader ( $cookie, $domain ) { $len = strlen( 'Set-Cookie:' ); diff --git a/includes/CryptRand.php b/includes/CryptRand.php index e4be1b37..858eebf2 100644 --- a/includes/CryptRand.php +++ b/includes/CryptRand.php @@ -5,6 +5,21 @@ * This is based in part on Drupal code as well as what we used in our own code * prior to introduction of this class. * + * 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 + * * @author Daniel Friesen * @file */ @@ -54,7 +69,7 @@ class MWCryptRand { // It'll also vary slightly across different machines $state = serialize( $_SERVER ); - // To try and vary the system information of the state a bit more + // To try vary the system information of the state a bit more // by including the system's hostname into the state $state .= wfHostname(); @@ -63,13 +78,22 @@ class MWCryptRand { // Include some information about the filesystem's current state in the random state $files = array(); + // We know this file is here so grab some info about ourself $files[] = __FILE__; + + // We must also have a parent folder, and with the usual file structure, a grandparent + $files[] = __DIR__; + $files[] = dirname( __DIR__ ); + // The config file is likely the most often edited file we know should be around - // so if the constant with it's location is defined include it's stat info into the state + // so include its stat info into the state. + // The constant with its location will almost always be defined, as WebStart.php defines + // MW_CONFIG_FILE to $IP/LocalSettings.php unless being configured with MW_CONFIG_CALLBACK (eg. the installer) if ( defined( 'MW_CONFIG_FILE' ) ) { $files[] = MW_CONFIG_FILE; } + foreach ( $files as $file ) { wfSuppressWarnings(); $stat = stat( $file ); @@ -275,7 +299,7 @@ class MWCryptRand { if ( strlen( $buffer ) < $bytes ) { // If available make use of mcrypt_create_iv URANDOM source to generate randomness // On unix-like systems this reads from /dev/urandom but does it without any buffering - // and bypasses openbasdir restrictions so it's preferable to reading directly + // and bypasses openbasedir restrictions, so it's preferable to reading directly // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate // entropy so this is also preferable to just trying to read urandom because it may work // on Windows systems as well. @@ -294,9 +318,10 @@ class MWCryptRand { } if ( strlen( $buffer ) < $bytes ) { - // If available make use of openssl's random_pesudo_bytes method to attempt to generate randomness. + // If available make use of openssl's random_pseudo_bytes method to attempt to generate randomness. // However don't do this on Windows with PHP < 5.3.4 due to a bug: // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php + // http://git.php.net/?p=php-src.git;a=commitdiff;h=cd62a70863c261b07f6dadedad9464f7e213cad5 if ( function_exists( 'openssl_random_pseudo_bytes' ) && ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) ) ) { diff --git a/includes/DataUpdate.php b/includes/DataUpdate.php new file mode 100644 index 00000000..377b64c0 --- /dev/null +++ b/includes/DataUpdate.php @@ -0,0 +1,124 @@ +<?php +/** + * Base code for update jobs that do something with some secondary + * data extracted from article. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Abstract base class for update jobs that do something with some secondary + * data extracted from article. + * + * @note: subclasses should NOT start or commit transactions in their doUpdate() method, + * a transaction will automatically be wrapped around the update. If need be, + * subclasses can override the beginTransaction() and commitTransaction() methods. + */ +abstract class DataUpdate implements DeferrableUpdate { + + /** + * Constructor + */ + public function __construct( ) { + # noop + } + + /** + * Begin an appropriate transaction, if any. + * This default implementation does nothing. + */ + public function beginTransaction() { + //noop + } + + /** + * Commit the transaction started via beginTransaction, if any. + * This default implementation does nothing. + */ + public function commitTransaction() { + //noop + } + + /** + * Abort / roll back the transaction started via beginTransaction, if any. + * This default implementation does nothing. + */ + public function rollbackTransaction() { + //noop + } + + /** + * Convenience method, calls doUpdate() on every DataUpdate in the array. + * + * This methods supports transactions logic by first calling beginTransaction() + * on all updates in the array, then calling doUpdate() on each, and, if all goes well, + * then calling commitTransaction() on each update. If an error occurrs, + * rollbackTransaction() will be called on any update object that had beginTranscation() + * called but not yet commitTransaction(). + * + * This allows for limited transactional logic across multiple backends for storing + * secondary data. + * + * @static + * @param $updates array a list of DataUpdate instances + */ + public static function runUpdates( $updates ) { + if ( empty( $updates ) ) return; # nothing to do + + $open_transactions = array(); + $exception = null; + + /** + * @var $update DataUpdate + * @var $trans DataUpdate + */ + + try { + // begin transactions + foreach ( $updates as $update ) { + $update->beginTransaction(); + $open_transactions[] = $update; + } + + // do work + foreach ( $updates as $update ) { + $update->doUpdate(); + } + + // commit transactions + while ( count( $open_transactions ) > 0 ) { + $trans = array_pop( $open_transactions ); + $trans->commitTransaction(); + } + } catch ( Exception $ex ) { + $exception = $ex; + wfDebug( "Caught exception, will rethrow after rollback: " . $ex->getMessage() ); + } + + // rollback remaining transactions + while ( count( $open_transactions ) > 0 ) { + $trans = array_pop( $open_transactions ); + $trans->rollbackTransaction(); + } + + if ( $exception ) { + throw $exception; // rethrow after cleanup + } + } + +} diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index ef1ef402..8216beb8 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1,6 +1,7 @@ <?php /** - * @file + * Default values for MediaWiki configuration settings. + * * * NEVER EDIT THIS FILE * @@ -15,25 +16,50 @@ * * Documentation is in the source and on: * http://www.mediawiki.org/wiki/Manual:Configuration_settings + * + * @warning Note: this (and other things) will break if the autoloader is not + * enabled. Please include includes/AutoLoader.php before including this file. + * + * 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 + */ + +/** + * @defgroup Globalsettings Global settings */ /** * @cond file_level_code - * This is not a valid entry point, perform no further processing unless MEDIAWIKI is defined + * This is not a valid entry point, perform no further processing unless + * MEDIAWIKI is defined */ if( !defined( 'MEDIAWIKI' ) ) { echo "This file is part of MediaWiki and is not a valid entry point\n"; die( 1 ); } -# 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 hold the site configuration. + * Not used for much in a default install. + */ $wgConf = new SiteConfiguration; -/** @endcond */ /** MediaWiki version number */ -$wgVersion = '1.19.3'; +$wgVersion = '1.20.2'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -41,10 +67,10 @@ $wgSitename = 'MediaWiki'; /** * URL of the server. * - * Example: - * <code> + * @par Example: + * @code * $wgServer = 'http://example.com'; - * </code> + * @endcode * * This is usually detected correctly by MediaWiki. If MediaWiki detects the * wrong server, it will redirect incorrectly after you save a page. In that @@ -110,28 +136,6 @@ $wgUsePathInfo = */ $wgScriptExtension = '.php'; -/** - * The URL path to index.php. - * - * Will default to "{$wgScriptPath}/index{$wgScriptExtension}" in Setup.php - */ -$wgScript = false; - -/** - * The URL path to redirect.php. This is a script that is used by the Nostalgia - * skin. - * - * Will default to "{$wgScriptPath}/redirect{$wgScriptExtension}" in Setup.php - */ -$wgRedirectScript = false; - -/** - * The URL path to load.php. - * - * Defaults to "{$wgScriptPath}/load{$wgScriptExtension}". - */ -$wgLoadScript = false; - /**@}*/ @@ -154,7 +158,30 @@ $wgLoadScript = false; */ /** - * The URL path of the skins directory. Will default to "{$wgScriptPath}/skins" in Setup.php + * The URL path to index.php. + * + * Defaults to "{$wgScriptPath}/index{$wgScriptExtension}". + */ +$wgScript = false; + +/** + * The URL path to redirect.php. This is a script that is used by the Nostalgia + * skin. + * + * Defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}". + */ +$wgRedirectScript = false; + +/** + * The URL path to load.php. + * + * Defaults to "{$wgScriptPath}/load{$wgScriptExtension}". + */ +$wgLoadScript = false; + +/** + * The URL path of the skins directory. + * Defaults to "{$wgScriptPath}/skins". */ $wgStylePath = false; $wgStyleSheetPath = &$wgStylePath; @@ -173,7 +200,8 @@ $wgLocalStylePath = false; $wgExtensionAssetsPath = false; /** - * Filesystem stylesheets directory. Will default to "{$IP}/skins" in Setup.php + * Filesystem stylesheets directory. + * Defaults to "{$IP}/skins". */ $wgStyleDirectory = false; @@ -181,29 +209,31 @@ $wgStyleDirectory = false; * The URL path for primary article page views. This path should contain $1, * which is replaced by the article title. * - * Will default to "{$wgScript}/$1" or "{$wgScript}?title=$1" in Setup.php, + * Defaults to "{$wgScript}/$1" or "{$wgScript}?title=$1", * depending on $wgUsePathInfo. */ $wgArticlePath = false; /** - * The URL path for the images directory. Will default to "{$wgScriptPath}/images" in Setup.php + * The URL path for the images directory. + * Defaults to "{$wgScriptPath}/images". */ $wgUploadPath = false; /** - * The maximum age of temporary (incomplete) uploaded files + * The filesystem path of the images directory. Defaults to "{$IP}/images". */ -$wgUploadStashMaxAge = 6 * 3600; // 6 hours +$wgUploadDirectory = false; /** - * The filesystem path of the images directory. Defaults to "{$IP}/images". + * Directory where the cached page will be saved. + * Defaults to "{$wgUploadDirectory}/cache". */ -$wgUploadDirectory = false; +$wgFileCacheDirectory = false; /** * The URL path of the wiki logo. The logo size should be 135x135 pixels. - * Will default to "{$wgStylePath}/common/images/wiki.png" in Setup.php + * Defaults to "{$wgStylePath}/common/images/wiki.png". */ $wgLogo = false; @@ -222,7 +252,16 @@ $wgAppleTouchIcon = false; * The local filesystem path to a temporary directory. This is not required to * be web accessible. * - * Will default to "{$wgUploadDirectory}/tmp" in Setup.php + * When this setting is set to false, its value will be set through a call + * to wfTempDir(). See that methods implementation for the actual detection + * logic. + * + * Developers should use the global function wfTempDir() instead of this + * variable. + * + * @see wfTempDir() + * @note Default changed to false in MediaWiki 1.20. + * */ $wgTmpDirectory = false; @@ -242,11 +281,16 @@ $wgUploadStashScalerBaseUrl = false; /** * To set 'pretty' URL paths for actions other than - * plain page views, add to this array. For instance: + * plain page views, add to this array. + * + * @par Example: + * Set pretty URL for the edit action: + * @code * 'edit' => "$wgScriptPath/edit/$1" + * @endcode * - * There must be an appropriate script or rewrite rule - * in place to handle these URLs. + * There must be an appropriate script or rewrite rule in place to handle these + * URLs. */ $wgActionPaths = array(); @@ -260,11 +304,16 @@ $wgActionPaths = array(); /** Uploads have to be specially set up to be secure */ $wgEnableUploads = false; +/** + * The maximum age of temporary (incomplete) uploaded files + */ +$wgUploadStashMaxAge = 6 * 3600; // 6 hours + /** Allows to move images and other media files */ $wgAllowImageMoving = true; /** - * These are additional characters that should be replaced with '-' in file names + * These are additional characters that should be replaced with '-' in filenames */ $wgIllegalFileChars = ":"; @@ -274,9 +323,10 @@ $wgIllegalFileChars = ":"; $wgFileStore = array(); /** - * What directory to place deleted uploads in + * What directory to place deleted uploads in. + * Defaults to "{$wgUploadDirectory}/deleted". */ -$wgDeletedDirectory = false; // Defaults to $wgUploadDirectory/deleted +$wgDeletedDirectory = false; /** * Set this to true if you use img_auth and want the user to see details on why access failed. @@ -308,11 +358,15 @@ $wgImgAuthPublicTest = true; * * For most core repos: * - zones Associative array of zone names that each map to an array with: - * container : backend container name the zone is in - * directory : root path within container for the zone - * Zones default to using <repo name>-<zone> as the - * container name and the container root as the zone directory. - * - url Base public URL + * container : backend container name the zone is in + * directory : root path within container for the zone + * url : base URL to the root of the zone + * handlerUrl : base script handled URL to the root of the zone + * (see FileRepo::getZoneHandlerUrl() function) + * Zones default to using "<repo name>-<zone name>" as the container name + * and default to using the container root as the zone's root directory. + * Nesting of zone locations within other zones should be avoided. + * - url Public zone URL. The 'zones' settings take precedence. * - hashLevels The number of directory levels for hash-based division of files * - thumbScriptUrl The URL for thumb.php (optional, not recommended) * - transformVia404 Whether to skip media file transformation on parse and rely on a 404 @@ -329,9 +383,11 @@ $wgImgAuthPublicTest = true; * is 0644. * - directory The local filesystem directory where public files are stored. Not used for * some remote repos. - * - thumbDir The base thumbnail directory. Defaults to <directory>/thumb. - * - thumbUrl The base thumbnail URL. Defaults to <url>/thumb. - * + * - thumbDir The base thumbnail directory. Defaults to "<directory>/thumb". + * - thumbUrl The base thumbnail URL. Defaults to "<url>/thumb". + * - isPrivate Set this if measures should always be taken to keep the files private. + * One should not trust this to assure that the files are not web readable; + * the server configuration should be done manually depending on the backend. * * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored * for local repositories: @@ -343,7 +399,9 @@ $wgImgAuthPublicTest = true; * * - articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1 * - fetchDescription Fetch the text of the remote file description page. Equivalent to - * $wgFetchCommonsDescriptions. + * $wgFetchCommonsDescriptions. + * - abbrvThreshold File names over this size will use the short form of thumbnail names. + * Short thumbnail names only have the width, parameters, and the extension. * * ForeignDBRepo: * - dbType, dbServer, dbUser, dbPassword, dbName, dbFlags @@ -380,10 +438,11 @@ $wgUseInstantCommons = false; * File backend structure configuration. * This is an array of file backend configuration arrays. * Each backend configuration has the following parameters: - * 'name' : A unique name for the backend - * 'class' : The file backend class to use - * 'wikiId' : A unique string that identifies the wiki (container prefix) - * 'lockManager' : The name of a lock manager (see $wgLockManagers) + * - 'name' : A unique name for the backend + * - 'class' : The file backend class to use + * - 'wikiId' : A unique string that identifies the wiki (container prefix) + * - 'lockManager' : The name of a lock manager (see $wgLockManagers) + * * Additional parameters are specific to the class used. */ $wgFileBackends = array(); @@ -391,8 +450,8 @@ $wgFileBackends = array(); /** * Array of configuration arrays for each lock manager. * Each backend configuration has the following parameters: - * 'name' : A unique name for the lock manger - * 'class' : The lock manger class to use + * - 'name' : A unique name for the lock manager + * - 'class' : The lock manger class to use * Additional parameters are specific to the class used. */ $wgLockManagers = array(); @@ -401,12 +460,13 @@ $wgLockManagers = array(); * Show EXIF data, on by default if available. * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php * - * NOTE FOR WINDOWS USERS: - * To enable EXIF functions, add the following lines to the - * "Windows extensions" section of php.ini: - * + * @note FOR WINDOWS USERS: + * To enable EXIF functions, add the following lines to the "Windows + * extensions" section of php.ini: + * @code{.ini} * extension=extensions/php_mbstring.dll * extension=extensions/php_exif.dll + * @endcode */ $wgShowEXIF = function_exists( 'exif_read_data' ); @@ -431,23 +491,32 @@ $wgUpdateCompatibleMetadata = false; * $wgForeignFileRepos variable. */ $wgUseSharedUploads = false; + /** Full path on the web server where shared uploads can be found */ $wgSharedUploadPath = "http://commons.wikimedia.org/shared/images"; + /** Fetch commons image description pages and display them on the local wiki? */ $wgFetchCommonsDescriptions = false; + /** Path on the file system where shared uploads can be found. */ $wgSharedUploadDirectory = "/var/www/wiki3/images"; + /** DB name with metadata about shared directory. Set this to false if the uploads do not come from a wiki. */ $wgSharedUploadDBname = false; + /** Optional table prefix used in database. */ $wgSharedUploadDBprefix = ''; + /** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */ $wgCacheSharedUploads = true; + /** -* Allow for upload to be copied from an URL. Requires Special:Upload?source=web -* The timeout for copy uploads is set by $wgHTTPTimeout. -*/ + * Allow for upload to be copied from an URL. + * The timeout for copy uploads is set by $wgHTTPTimeout. + * You have to assign the user right 'upload_by_url' to a user group, to use this. + */ $wgAllowCopyUploads = false; + /** * Allow asynchronous copy uploads. * This feature is experimental and broken as of r81612. @@ -455,16 +524,31 @@ $wgAllowCopyUploads = false; $wgAllowAsyncCopyUploads = false; /** + * A list of domains copy uploads can come from + * + * @since 1.20 + */ +$wgCopyUploadsDomains = array(); + +/** + * Proxy to use for copy upload requests. + * @since 1.20 + */ +$wgCopyUploadProxy = false; + +/** * 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: + * @par Example: + * @code * $wgMaxUploadSize = array( * '*' => 250 * 1024, * 'url' => 500 * 1024, * ); + * @endcode * Sets the maximum for all uploads to 250 kB except for upload-by-url, which * will have a maximum of 500 kB. * @@ -474,27 +558,37 @@ $wgMaxUploadSize = 1024*1024*100; # 100MB /** * Point the upload navigation link to an external URL * Useful if you want to use a shared repository by default - * without disabling local uploads (use $wgEnableUploads = false for that) - * e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload'; + * without disabling local uploads (use $wgEnableUploads = false for that). + * + * @par Example: + * @code + * $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload'; + * @endcode */ $wgUploadNavigationUrl = false; /** * Point the upload link for missing files to an external URL, as with - * $wgUploadNavigationUrl. The URL will get (?|&)wpDestFile=<filename> + * $wgUploadNavigationUrl. The URL will get "(?|&)wpDestFile=<filename>" * appended to it as appropriate. */ $wgUploadMissingFileUrl = false; /** - * Give a path here to use thumb.php for thumbnail generation on client request, instead of - * generating them on render and outputting a static URL. This is necessary if some of your - * apache servers don't have read/write access to the thumbnail path. + * Give a path here to use thumb.php for thumbnail generation on client + * request, instead of generating them on render and outputting a static URL. + * This is necessary if some of your apache servers don't have read/write + * access to the thumbnail path. * - * Example: + * @par Example: + * @code * $wgThumbnailScriptPath = "{$wgScriptPath}/thumb{$wgScriptExtension}"; + * @endcode */ $wgThumbnailScriptPath = false; +/** + * @see $wgThumbnailScriptPath + */ $wgSharedThumbnailScriptPath = false; /** @@ -507,7 +601,8 @@ $wgSharedThumbnailScriptPath = false; * maintenance/rebuildImages.php to register them in the database. This is no * longer recommended, use maintenance/importImages.php instead. * - * Note that this variable may be ignored if $wgLocalFileRepo is set. + * @note That this variable may be ignored if $wgLocalFileRepo is set. + * @todo Deprecate the setting and ultimately remove it from Core. */ $wgHashedUploadDirectory = true; @@ -532,13 +627,17 @@ $wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/File:"; * This is the list of preferred extensions for uploading files. Uploading files * with extensions not in this list will trigger a warning. * - * WARNING: If you add any OpenOffice or Microsoft Office file formats here, + * @warning If you add any OpenOffice or Microsoft Office file formats here, * such as odt or doc, and untrusted users are allowed to upload files, then * your wiki will be vulnerable to cross-site request forgery (CSRF). */ $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' ); -/** Files with these extensions will never be allowed as uploads. */ +/** + * Files with these extensions will never be allowed as uploads. + * An array of file extensions to blacklist. You should append to this array + * if you want to blacklist additional files. + * */ $wgFileBlacklist = array( # HTML may contain cookie-stealing JavaScript and web bugs 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', @@ -576,7 +675,7 @@ $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. + * @warning Setting this to false is insecure for public wikis. */ $wgCheckFileExtensions = true; @@ -584,18 +683,21 @@ $wgCheckFileExtensions = true; * If this is turned off, users may override the warning for files not covered * by $wgFileExtensions. * - * WARNING: setting this to false is insecure for public wikis. + * @warning Setting this to false is insecure for public wikis. */ $wgStrictFileExtensions = true; /** * Setting this to true will disable the upload system's checks for HTML/JavaScript. - * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions - * TO RESTRICT UPLOADING to only those that you trust + * + * @warning THIS IS VERY DANGEROUS on a publicly editable site, so USE + * $wgGroupPermissions TO RESTRICT UPLOADING to only those that you trust */ $wgDisableUploadScriptChecks = false; -/** Warn if uploaded files are larger than this (in bytes), or false to disable*/ +/** + * Warn if uploaded files are larger than this (in bytes), or false to disable + */ $wgUploadSizeWarning = false; /** @@ -622,18 +724,18 @@ $wgTrustedMediaFormats = array( * Each entry in the array maps a MIME type to a class name */ $wgMediaHandlers = array( - 'image/jpeg' => 'JpegHandler', - 'image/png' => 'PNGHandler', - 'image/gif' => 'GIFHandler', - 'image/tiff' => 'TiffHandler', + 'image/jpeg' => 'JpegHandler', + 'image/png' => 'PNGHandler', + 'image/gif' => 'GIFHandler', + 'image/tiff' => 'TiffHandler', 'image/x-ms-bmp' => 'BmpHandler', - 'image/x-bmp' => 'BmpHandler', - 'image/x-xcf' => 'XCFHandler', - 'image/svg+xml' => 'SvgHandler', // official - 'image/svg' => 'SvgHandler', // compat + 'image/x-bmp' => 'BmpHandler', + 'image/x-xcf' => 'XCFHandler', + 'image/svg+xml' => 'SvgHandler', // official + 'image/svg' => 'SvgHandler', // compat 'image/vnd.djvu' => 'DjVuHandler', // official - 'image/x.djvu' => 'DjVuHandler', // compat - 'image/x-djvu' => 'DjVuHandler', // compat + 'image/x.djvu' => 'DjVuHandler', // compat + 'image/x-djvu' => 'DjVuHandler', // compat ); /** @@ -667,17 +769,18 @@ $wgImageMagickTempDir = false; * %s will be replaced with the source path, %d with the destination * %w and %h will be replaced with the width and height. * - * Example for GraphicMagick: - * <code> + * @par Example for GraphicMagick: + * @code * $wgCustomConvertCommand = "gm convert %s -resize %wx%h %d" - * </code> + * @endcode * * Leave as false to skip this. */ $wgCustomConvertCommand = false; /** - * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some image formats. + * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some + * image formats. */ $wgExiv2Command = '/usr/bin/exiv2'; @@ -699,22 +802,31 @@ $wgSVGConverters = array( 'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output', 'ImagickExt' => array( 'SvgHandler::rasterizeImagickExt' ), ); + /** Pick a converter defined in $wgSVGConverters */ $wgSVGConverter = 'ImageMagick'; + /** If not in the executable PATH, specify the SVG converter path. */ $wgSVGConverterPath = ''; + /** Don't scale a SVG larger than this */ $wgSVGMaxSize = 2048; + /** Don't read SVG metadata beyond this point. - * Default is 1024*256 bytes */ + * Default is 1024*256 bytes + */ $wgSVGMetadataCutoff = 262144; /** - * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't - * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading - * crap files as images. When this directive is on, <title> will be allowed in files with - * an "image/svg+xml" MIME type. You should leave this disabled if your web server is misconfigured - * and doesn't send appropriate MIME types for SVG images. + * Disallow <title> element in SVG files. + * + * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic + * browsers which can not perform basic stuff like MIME detection and which are + * vulnerable to further idiots uploading crap files as images. + * + * When this directive is on, "<title>" will be allowed in files with an + * "image/svg+xml" MIME type. You should leave this disabled if your web server + * is misconfigured and doesn't send appropriate MIME types for SVG images. */ $wgAllowTitlesInSVG = false; @@ -744,13 +856,13 @@ $wgMaxAnimatedGifArea = 1.25e7; * For inline display, we need to convert to PNG or JPEG. * Note scaling should work with ImageMagick, but may not with GD scaling. * - * Example: - * <code> + * @par Example: + * @code * // PNG is lossless, but inefficient for photos * $wgTiffThumbnailType = array( 'png', 'image/png' ); * // JPEG is good for photos, but has no transparency support. Bad for diagrams. * $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' ); - * </code> + * @endcode */ $wgTiffThumbnailType = false; @@ -763,7 +875,7 @@ $wgMaxAnimatedGifArea = 1.25e7; $wgThumbnailEpoch = '20030516000000'; /** - * If set, inline scaled images will still produce <img> tags ready for + * If set, inline scaled images will still produce "<img>" tags ready for * output instead of showing an error message. * * This may be useful if errors are transitory, especially if the site @@ -855,20 +967,6 @@ $wgAntivirusSetup = array( 'messagepattern' => '/.*?:(.*)/sim', ), - - #setup for f-prot - 'f-prot' => array ( - 'command' => "f-prot ", - - 'codemap' => array ( - "0" => AV_NO_VIRUS, # no virus - "3" => AV_VIRUS_FOUND, # virus found - "6" => AV_VIRUS_FOUND, # virus found - "*" => AV_SCAN_FAILED, # else scan failed - ), - - 'messagepattern' => '/.*?Infection:(.*)$/m', - ), ); @@ -898,10 +996,11 @@ $wgLoadFileinfoExtension = false; * the mime type to standard output. * The name of the file to process will be appended to the command given here. * If not set or NULL, mime_content_type will be used if available. - * Example: - * <code> + * + * @par Example: + * @code * #$wgMimeDetectorCommand = "file -bi"; # use external mime detector (Linux) - * </code> + * @endcode */ $wgMimeDetectorCommand = null; @@ -937,8 +1036,7 @@ $wgImageLimits = array( array( 640, 480 ), array( 800, 600 ), array( 1024, 768 ), - array( 1280, 1024 ), - array( 10000, 10000 ) + array( 1280, 1024 ) ); /** @@ -956,7 +1054,7 @@ $wgThumbLimits = array( ); /** - * Default parameters for the <gallery> tag + * Default parameters for the "<gallery>" tag */ $wgGalleryOptions = array ( 'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize @@ -979,7 +1077,10 @@ $wgThumbUpright = 0.75; $wgDirectoryMode = 0777; /** - * DJVU settings + * @name DJVU settings + * @{ + */ +/** * Path of the djvudump executable * Enable this and $wgDjvuRenderer to enable djvu rendering */ @@ -1004,15 +1105,18 @@ $wgDjvuTxt = null; * Path of the djvutoxml executable * This works like djvudump except much, much slower as of version 3.5. * - * For now I recommend you use djvudump instead. The djvuxml output is + * For now we recommend you use djvudump instead. The djvuxml output is * probably more stable, so we'll switch back to it as soon as they fix * the efficiency problem. * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 + * + * @par Example: + * @code + * $wgDjvuToXML = 'djvutoxml'; + * @endcode */ -# $wgDjvuToXML = 'djvutoxml'; $wgDjvuToXML = null; - /** * Shell command for the DJVU post processor * Default: pnmtopng, since ddjvu generates ppm output @@ -1023,6 +1127,7 @@ $wgDjvuPostProcessor = 'pnmtojpeg'; * File extension for the DJVU post processor output */ $wgDjvuOutputExtension = 'jpg'; +/** @} */ # end of DJvu } /** @} */ # end of file uploads } @@ -1099,17 +1204,21 @@ $wgNewPasswordExpiry = 3600 * 24 * 7; $wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60; /** - * SMTP Mode + * SMTP Mode. + * * For using a direct (authenticated) SMTP server connection. * Default to false or fill an array : - * <code> - * "host" => 'SMTP domain', - * "IDHost" => 'domain for MessageID', - * "port" => "25", - * "auth" => true/false, - * "username" => user, - * "password" => password - * </code> + * + * @code + * $wgSMTP = array( + * 'host' => 'SMTP domain', + * 'IDHost' => 'domain for MessageID', + * 'port' => '25', + * 'auth' => [true|false], + * 'username' => [SMTP username], + * 'password' => [SMTP password], + * ); + * @endcode */ $wgSMTP = false; @@ -1131,9 +1240,9 @@ $wgEnotifFromEditor = false; # It call this to be a "user-preferences-option (UPO)" /** - * Require email authentication before sending mail to an email addres. This is - * highly recommended. It prevents MediaWiki from being used as an open spam - * relay. + * Require email authentication before sending mail to an email address. + * This is highly recommended. It prevents MediaWiki from being used as an open + * spam relay. */ $wgEmailAuthentication = true; @@ -1211,6 +1320,10 @@ $wgDBuser = 'wikiuser'; $wgDBpassword = ''; /** Database type */ $wgDBtype = 'mysql'; +/** Whether to use SSL in DB connection. */ +$wgDBssl = false; +/** Whether to use compression in DB connection. */ +$wgDBcompress = false; /** Separate username for maintenance tasks. Leave as null to use the default. */ $wgDBadminuser = null; @@ -1295,6 +1408,9 @@ $wgSharedTables = array( 'user', 'user_properties' ); * - DBO_TRX -- wrap entire request in a transaction * - DBO_IGNORE -- ignore errors (not useful in LocalSettings.php) * - DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php) + * - DBO_PERSISTENT -- enables persistent database connections + * - DBO_SSL -- uses SSL/TLS encryption in database connections, if available + * - DBO_COMPRESS -- uses internal compression in database connections, if available * * - max lag: (optional) Maximum replication lag before a slave will taken out of rotation * - max threads: (optional) Maximum number of running threads @@ -1311,9 +1427,9 @@ $wgSharedTables = array( 'user', 'user_properties' ); * accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your * slaves in my.cnf. You can set read_only mode at runtime using: * - * <code> + * @code * SET @@read_only=1; - * </code> + * @endcode * * Since the effect of writing to a slave is so damaging and difficult to clean * up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even @@ -1339,23 +1455,41 @@ $wgMasterWaitTimeout = 10; /** File to log database errors to */ $wgDBerrorLog = false; +/** + * Timezone to use in the error log. + * Defaults to the wiki timezone ($wgLocaltimezone). + * + * A list of useable timezones can found at: + * http://php.net/manual/en/timezones.php + * + * @par Examples: + * @code + * $wgLocaltimezone = 'UTC'; + * $wgLocaltimezone = 'GMT'; + * $wgLocaltimezone = 'PST8PDT'; + * $wgLocaltimezone = 'Europe/Sweden'; + * $wgLocaltimezone = 'CET'; + * @endcode + * + * @since 1.20 + */ +$wgDBerrorLogTZ = false; + /** When to give an error message */ $wgDBClusterTimeout = 10; /** - * Scale load balancer polling time so that under overload conditions, the database server - * receives a SHOW STATUS query at an average interval of this many microseconds + * Scale load balancer polling time so that under overload conditions, the + * database server receives a SHOW STATUS query at an average interval of this + * many microseconds */ $wgDBAvgStatusPoll = 2000; -/** Set to true if using InnoDB tables */ -$wgDBtransactions = false; - /** * Set to true to engage MySQL 4.1/5.0 charset-related features; * for now will just cause sending of 'SET NAMES=utf8' on connect. * - * WARNING: THIS IS EXPERIMENTAL! + * @warning THIS IS EXPERIMENTAL! * * May break if you're not using the table defs from mysql5/tables.sql. * May break if you're upgrading an existing wiki if set differently. @@ -1408,19 +1542,30 @@ $wgCompressRevisions = false; /** * External stores allow including content - * from non database sources following URL links + * from non database sources following URL links. * * Short names of ExternalStore classes may be specified in an array here: + * @code * $wgExternalStores = array("http","file","custom")... + * @endcode * * CAUTION: Access to database might lead to code execution */ $wgExternalStores = false; /** - * An array of external mysql servers, e.g. - * $wgExternalServers = array( 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) ); - * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to another class. + * An array of external MySQL servers. + * + * @par Example: + * Create a cluster named 'cluster1' containing three servers: + * @code + * $wgExternalServers = array( + * 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) + * ); + * @endcode + * + * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to + * another class. */ $wgExternalServers = array(); @@ -1429,9 +1574,12 @@ $wgExternalServers = array(); * Part of a URL, e.g. DB://cluster1 * * Can be an array instead of a single string, to enable data distribution. Keys - * must be consecutive integers, starting at zero. Example: + * must be consecutive integers, starting at zero. * + * @par Example: + * @code * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' ); + * @endcode * * @var array */ @@ -1471,17 +1619,10 @@ $wgUseDumbLinkUpdate = false; /** * Anti-lock flags - bitfield - * - ALF_PRELOAD_LINKS: - * Preload links during link update for save - * - ALF_PRELOAD_EXISTENCE: - * Preload cur_id during replaceLinkHolders * - ALF_NO_LINK_LOCK: * Don't use locking reads when updating the link table. This is * necessary for wikis with a high edit rate for performance * reasons, but may cause link table inconsistency - * - ALF_NO_BLOCK_LOCK: - * As for ALF_LINK_LOCK, this flag is a necessity for high-traffic - * wikis. */ $wgAntiLockFlags = 0; @@ -1552,11 +1693,29 @@ $wgMessageCacheType = CACHE_ANYTHING; $wgParserCacheType = CACHE_ANYTHING; /** + * The cache type for storing session data. Used if $wgSessionsInObjectCache is true. + * + * For available types see $wgMainCacheType. + */ +$wgSessionCacheType = CACHE_ANYTHING; + +/** + * The cache type for storing language conversion tables, + * which are used when parsing certain text and interface messages. + * + * For available types see $wgMainCacheType. + * + * @since 1.20 + */ +$wgLanguageConverterCacheType = 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. + * referenced from $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, + * or $wgLanguageConverterCacheType. * * 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 @@ -1580,28 +1739,44 @@ $wgObjectCaches = array( 'xcache' => array( 'class' => 'XCacheBagOStuff' ), 'wincache' => array( 'class' => 'WinCacheBagOStuff' ), 'memcached-php' => array( 'class' => 'MemcachedPhpBagOStuff' ), + 'memcached-pecl' => array( 'class' => 'MemcachedPeclBagOStuff' ), 'hash' => array( 'class' => 'HashBagOStuff' ), ); /** - * The expiry time for the parser cache, in seconds. The default is 86.4k - * seconds, otherwise known as a day. + * The expiry time for the parser cache, in seconds. + * The default is 86400 (one day). */ $wgParserCacheExpireTime = 86400; /** - * Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php> to use as CACHE_DBA backend + * Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php> + * to use as CACHE_DBA backend. */ $wgDBAhandler = 'db3'; /** - * Store sessions in MemCached. This can be useful to improve performance, or to - * avoid the locking behaviour of PHP's default session handler, which tends to - * prevent multiple requests for the same user from acting concurrently. + * Deprecated alias for $wgSessionsInObjectCache. + * + * @deprecated Use $wgSessionsInObjectCache */ $wgSessionsInMemcached = false; /** + * Store sessions in an object cache, configured by $wgSessionCacheType. This + * can be useful to improve performance, or to avoid the locking behaviour of + * PHP's default session handler, which tends to prevent multiple requests for + * the same user from acting concurrently. + */ +$wgSessionsInObjectCache = false; + +/** + * The expiry time to use for session storage when $wgSessionsInObjectCache is + * enabled, in seconds. + */ +$wgObjectCacheSessionExpiry = 3600; + +/** * This is used for setting php's session.save_handler. In practice, you will * almost never need to change this ever. Other options might be 'user' or * 'session_mysql.' Setting to null skips setting this entirely (which might be @@ -1624,7 +1799,7 @@ $wgMemCachedPersistent = false; /** * Read/write timeout for MemCached server communication, in microseconds. */ -$wgMemCachedTimeout = 100000; +$wgMemCachedTimeout = 500000; /** * Set this to true to make a local copy of the message cache, for use in @@ -1633,9 +1808,9 @@ $wgMemCachedTimeout = 100000; $wgUseLocalMessageCache = false; /** - * Defines format of local cache - * true - Serialized object - * false - PHP source file (Warning - security risk) + * Defines format of local cache. + * - true: Serialized object + * - false: PHP source file (Warning - security risk) */ $wgLocalMessageCacheSerialized = true; @@ -1648,23 +1823,23 @@ $wgAdaptiveMessageCache = false; /** * Localisation cache configuration. Associative array with keys: - * class: The class to use. May be overridden by extensions. + * class: The class to use. May be overridden by extensions. * - * store: The location to store cache data. May be 'files', 'db' or - * 'detect'. If set to "files", data will be in CDB files. If set - * to "db", data will be stored to the database. If set to - * "detect", files will be used if $wgCacheDirectory is set, - * otherwise the database will be used. + * store: The location to store cache data. May be 'files', 'db' or + * 'detect'. If set to "files", data will be in CDB files. If set + * to "db", data will be stored to the database. If set to + * "detect", files will be used if $wgCacheDirectory is set, + * otherwise the database will be used. * - * storeClass: The class name for the underlying storage. If set to a class - * name, it overrides the "store" setting. + * storeClass: The class name for the underlying storage. If set to a class + * name, it overrides the "store" setting. * - * storeDirectory: If the store class puts its data in files, this is the - * directory it will use. If this is false, $wgCacheDirectory - * will be used. + * storeDirectory: If the store class puts its data in files, this is the + * directory it will use. If this is false, $wgCacheDirectory + * will be used. * - * manualRecache: Set this to true to disable cache updates on web requests. - * Use maintenance/rebuildLocalisationCache.php instead. + * manualRecache: Set this to true to disable cache updates on web requests. + * Use maintenance/rebuildLocalisationCache.php instead. */ $wgLocalisationCacheConf = array( 'class' => 'LocalisationCache', @@ -1679,14 +1854,17 @@ $wgCachePages = true; /** * Set this to current time to invalidate all prior cached pages. Affects both - * client- and server-side caching. + * client-side and server-side caching. * You can get the current date on your server by using the command: + * @verbatim * date +%Y%m%d%H%M%S + * @endverbatim */ $wgCacheEpoch = '20030516000000'; /** * Bump this number when changing the global style sheets and JavaScript. + * * It should be appended in the query string of static CSS and JS includes, * to ensure that client-side caches do not keep obsolete copies of global * styles. @@ -1703,12 +1881,6 @@ $wgStyleVersion = '303'; $wgUseFileCache = false; /** - * Directory where the cached page will be saved. - * Will default to "{$wgUploadDirectory}/cache" in Setup.php - */ -$wgFileCacheDirectory = false; - -/** * Depth of the subdirectory hierarchy to be created under * $wgFileCacheDirectory. The subdirectories will be named based on * the MD5 hash of the title. A value of 0 means all cache files will @@ -1752,8 +1924,6 @@ $wgSidebarCacheExpiry = 86400; /** * When using the file cache, we can store the cached HTML gzipped to save disk * space. Pages will then also be served compressed to clients that support it. - * THIS IS NOT COMPATIBLE with ob_gzhandler which is now enabled if supported in - * the default LocalSettings.php! If you enable this, remove that setting first. * * Requires zlib support enabled in PHP. */ @@ -1820,10 +1990,12 @@ $wgUseXVO = false; $wgVaryOnXFP = false; /** - * Internal server name as known to Squid, if different. Example: - * <code> + * Internal server name as known to Squid, if different. + * + * @par Example: + * @code * $wgInternalServer = 'http://yourinternal.tld:8000'; - * </code> + * @endcode */ $wgInternalServer = false; @@ -1860,22 +2032,61 @@ $wgSquidServersNoPurge = array(); $wgMaxSquidPurgeTitles = 400; /** + * Routing configuration for HTCP multicast purging. Add elements here to + * enable HTCP and determine which purges are sent where. If set to an empty + * array, HTCP is disabled. + * + * Each key in this array is a regular expression to match against the purged + * URL, or an empty string to match all URLs. The purged URL is matched against + * the regexes in the order specified, and the first rule whose regex matches + * is used. + * + * Example configuration to send purges for upload.wikimedia.org to one + * multicast group and all other purges to another: + * @code + * $wgHTCPMulticastRouting = array( + * '|^https?://upload\.wikimedia\.org|' => array( + * 'host' => '239.128.0.113', + * 'port' => 4827, + * ), + * '' => array( + * 'host' => '239.128.0.112', + * 'port' => 4827, + * ), + * ); + * @endcode + * + * @since 1.20 + * + * @see $wgHTCPMulticastTTL + */ +$wgHTCPMulticastRouting = array(); + +/** * HTCP multicast address. Set this to a multicast IP address to enable HTCP. * * Note that MediaWiki uses the old non-RFC compliant HTCP format, which was * present in the earliest Squid implementations of the protocol. + * + * This setting is DEPRECATED in favor of $wgHTCPMulticastRouting , and kept + * for backwards compatibility only. If $wgHTCPMulticastRouting is set, this + * setting is ignored. If $wgHTCPMulticastRouting is not set and this setting + * is, it is used to populate $wgHTCPMulticastRouting. + * + * @deprecated in favor of $wgHTCPMulticastRouting */ $wgHTCPMulticastAddress = false; /** * HTCP multicast port. + * @deprecated in favor of $wgHTCPMulticastRouting * @see $wgHTCPMulticastAddress */ $wgHTCPPort = 4827; /** * HTCP multicast TTL. - * @see $wgHTCPMulticastAddress + * @see $wgHTCPMulticastRouting */ $wgHTCPMulticastTTL = 1; @@ -1894,11 +2105,12 @@ $wgLanguageCode = 'en'; /** * Some languages need different word forms, usually for different cases. - * Used in Language::convertGrammar(). Example: + * Used in Language::convertGrammar(). * - * <code> + * @par Example: + * @code * $wgGrammarForms['en']['genitive']['car'] = 'car\'s'; - * </code> + * @endcode */ $wgGrammarForms = array(); @@ -1981,7 +2193,7 @@ $wgAllUnicodeFixes = false; * converting a wiki from MediaWiki 1.4 or earlier to UTF-8 without the * burdensome mass conversion of old text data. * - * NOTE! This DOES NOT touch any fields other than old_text.Titles, comments, + * @note This DOES NOT touch any fields other than old_text. Titles, comments, * user names, etc still must be converted en masse in the database before * continuing as a UTF-8 wiki. */ @@ -2090,28 +2302,27 @@ $wgCanonicalLanguageLinks = true; $wgDefaultLanguageVariant = false; /** - * Disabled variants array of language variant conversion. Example: - * <code> + * Disabled variants array of language variant conversion. + * + * @par Example: + * @code * $wgDisabledVariants[] = 'zh-mo'; * $wgDisabledVariants[] = 'zh-my'; - * </code> - * - * or: - * - * <code> - * $wgDisabledVariants = array('zh-mo', 'zh-my'); - * </code> + * @endcode */ $wgDisabledVariants = array(); /** * Like $wgArticlePath, but on multi-variant wikis, this provides a * path format that describes which parts of the URL contain the - * language variant. For Example: + * language variant. * - * $wgLanguageCode = 'sr'; - * $wgVariantArticlePath = '/$2/$1'; - * $wgArticlePath = '/wiki/$1'; + * @par Example: + * @code + * $wgLanguageCode = 'sr'; + * $wgVariantArticlePath = '/$2/$1'; + * $wgArticlePath = '/wiki/$1'; + * @endcode * * A link to /wiki/ would be redirected to /sr/Главна_страна * @@ -2128,19 +2339,23 @@ $wgVariantArticlePath = false; $wgLoginLanguageSelector = false; /** - * When translating messages with wfMsg(), it is not always clear what should - * be considered UI messages and what should be content messages. + * When translating messages with wfMessage(), it is not always clear what + * should be considered UI messages and what should be content messages. * * For example, for the English Wikipedia, there should be only one 'mainpage', * so when getting the link for 'mainpage', we should treat it as site content - * and call wfMsgForContent(), but for rendering the text of the link, we call - * wfMsg(). The code behaves this way by default. However, sites like the - * Wikimedia Commons do offer different versions of 'mainpage' and the like for - * different languages. This array provides a way to override the default - * behavior. For example, to allow language-specific main page and community - * portal, set - * - * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' ); + * and call ->inContentLanguage()->text(), but for rendering the text of the + * link, we call ->text(). The code behaves this way by default. However, + * sites like the Wikimedia Commons do offer different versions of 'mainpage' + * and the like for different languages. This array provides a way to override + * the default behavior. + * + * @par Example: + * To allow language-specific main page and community + * portal: + * @code + * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' ); + * @endcode */ $wgForceUIMsgAsContentMsg = array(); @@ -2155,13 +2370,17 @@ $wgForceUIMsgAsContentMsg = array(); * Timezones can be translated by editing MediaWiki messages of type * timezone-nameinlowercase like timezone-utc. * - * Examples: - * <code> + * A list of useable timezones can found at: + * http://php.net/manual/en/timezones.php + * + * @par Examples: + * @code + * $wgLocaltimezone = 'UTC'; * $wgLocaltimezone = 'GMT'; * $wgLocaltimezone = 'PST8PDT'; * $wgLocaltimezone = 'Europe/Sweden'; * $wgLocaltimezone = 'CET'; - * </code> + * @endcode */ $wgLocaltimezone = null; @@ -2182,7 +2401,7 @@ $wgLocalTZoffset = null; * language variant conversion is disabled in interface messages. Setting this * to true re-enables it. * - * This variable should be removed (implicitly false) in 1.20 or earlier. + * @todo This variable should be removed (implicitly false) in 1.20 or earlier. */ $wgBug34832TransitionalRollback = true; @@ -2255,11 +2474,6 @@ $wgAllowRdfaAttributes = false; $wgAllowMicrodataAttributes = false; /** - * Cleanup as much presentational html like valign -> css vertical-align as we can - */ -$wgCleanupPresentationalAttributes = true; - -/** * Should we try to make our HTML output well-formed XML? If set to false, * output will be a few bytes shorter, and the HTML will arguably be more * readable. If set to true, life will be much easier for the authors of @@ -2279,10 +2493,14 @@ $wgWellFormedXml = true; /** * Permit other namespaces in addition to the w3.org default. - * Use the prefix for the key and the namespace for the value. For - * example: + * + * Use the prefix for the key and the namespace for the value. + * + * @par Example: + * @code * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; - * Normally we wouldn't have to define this in the root <html> + * @endCode + * 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 @@ -2293,7 +2511,7 @@ $wgXhtmlNamespaces = array(); /** * Show IP address, for non-logged in users. It's necessary to switch this off * for some forms of caching. - * Will disable file cache. + * @warning Will disable file cache. */ $wgShowIPinHeader = true; @@ -2464,15 +2682,16 @@ $wgExperimentalHtmlIds = false; * 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 + * - 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 - * url: The url to use in the <a> arround the text or icon, if not set an <a> will not be outputted - * alt: This is the text form of the icon, it will be displayed without an image in + * - url: The url to use in the a element arround the text or icon, if not set an a element will not be outputted + * - alt: This is the text form of the icon, it will be displayed without an image in * skins like Modern or if src is not set, and will otherwise be used as * the alt="" for the image. This key is required. - * width and height: If the icon specified by src is not of the standard size + * - width and height: If the icon specified by src is not of the standard size * you can specify the size of image to use with these keys. * Otherwise they will default to the standard 88x31. + * @todo Reformat documentation. */ $wgFooterIcons = array( "copyright" => array( @@ -2488,23 +2707,24 @@ $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 + * 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; +$wgUseCombinedLoginLink = false; /** - * Search form behavior for Vector skin only - * true = use an icon search button - * false = use Go & Search buttons + * Search form look for Vector skin only. + * - true = use an icon search button + * - false = use Go & Search buttons */ $wgVectorUseSimpleSearch = false; /** - * Watch and unwatch as an icon rather than a link for Vector skin only - * true = use an icon watch/unwatch button - * false = use watch/unwatch text link + * Watch and unwatch as an icon rather than a link for Vector skin only. + * - true = use an icon watch/unwatch button + * - false = use watch/unwatch text link */ $wgVectorUseIconWatch = false; @@ -2534,6 +2754,16 @@ $wgBetterDirectionality = true; */ $wgSend404Code = true; + +/** + * The $wgShowRollbackEditCount variable is used to show how many edits will be + * rollback. The numeric value of the varible are the limit up to are counted. + * If the value is false or 0, the edits are not counted. + * + * @since 1.20 + */ +$wgShowRollbackEditCount = 10; + /** @} */ # End of output format settings } /*************************************************************************//** @@ -2542,17 +2772,21 @@ $wgSend404Code = true; */ /** - * Client-side resource modules. Extensions should add their module definitions - * here. + * Client-side resource modules. + * + * Extensions should add their resource loader module definitions + * to the $wgResourceModules variable. * - * Example: + * @par Example: + * @code * $wgResourceModules['ext.myExtension'] = array( * 'scripts' => 'myExtension.js', * 'styles' => 'myExtension.css', * 'dependencies' => array( 'jquery.cookie', 'jquery.tabIndex' ), - * 'localBasePath' => dirname( __FILE__ ), + * 'localBasePath' => __DIR__, * 'remoteExtPath' => 'MyExtension', * ); + * @endcode */ $wgResourceModules = array(); @@ -2561,22 +2795,26 @@ $wgResourceModules = array(); * built-in source that is not in this array, but defined by * ResourceLoader::__construct() so that it cannot be unset. * - * Example: + * @par Example: + * @code * $wgResourceLoaderSources['foo'] = array( * 'loadScript' => 'http://example.org/w/load.php', * 'apiScript' => 'http://example.org/w/api.php' * ); + * @endcode */ $wgResourceLoaderSources = array(); /** - * Default 'remoteBasePath' value for resource loader modules. + * Default 'remoteBasePath' value for instances of ResourceLoaderFileModule. * 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 + * Maximum time in seconds to cache resources served by the resource loader. + * + * @todo Document array structure */ $wgResourceLoaderMaxage = array( 'versioned' => array( @@ -2592,8 +2830,9 @@ $wgResourceLoaderMaxage = array( ); /** - * The default debug mode (on/off) for of ResourceLoader requests. This will still - * be overridden when the debug URL parameter is used. + * The default debug mode (on/off) for of ResourceLoader requests. + * + * This will still be overridden when the debug URL parameter is used. */ $wgResourceLoaderDebug = false; @@ -2619,33 +2858,54 @@ $wgResourceLoaderMinifierMaxLineLength = 1000; /** * Whether to include the mediawiki.legacy JS library (old wikibits.js), and its - * dependencies + * dependencies. */ $wgIncludeLegacyJavaScript = true; /** - * Whether to preload the mediawiki.util module as blocking module in the top queue. - * Before MediaWiki 1.19, modules used to load slower/less asynchronous which allowed - * modules to lack dependencies on 'popular' modules that were likely loaded already. + * Whether to preload the mediawiki.util module as blocking module in the top + * queue. + * + * Before MediaWiki 1.19, modules used to load slower/less asynchronous which + * allowed modules to lack dependencies on 'popular' modules that were likely + * loaded already. + * * This setting is to aid scripts during migration by providing mediawiki.util * unconditionally (which was the most commonly missed dependency). - * It doesn't cover all missing dependencies obviously but should fix most of them. + * It doesn't cover all missing dependencies obviously but should fix most of + * them. + * * This should be removed at some point after site/user scripts have been fixed. - * Enable this if your wiki has a large amount of user/site scripts that are lacking - * dependencies. + * Enable this if your wiki has a large amount of user/site scripts that are + * lacking dependencies. + * @todo Deprecate */ $wgPreloadJavaScriptMwUtil = false; /** - * 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 ) ..." + * Whether or not to assign configuration variables to the global window object. + * + * If this is set to false, old code using deprecated variables will no longer + * work. + * + * @par Example of legacy code: + * @code{,js} + * if ( window.wgRestrictionEdit ) { ... } + * @endcode * 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') )". + * @code{,js} + * if ( wgIsArticle ) { ... } + * @endcode + * + * Instead, one needs to use mw.config. + * @par Example using mw.config global configuration: + * @code{,js} + * if ( mw.config.exists('wgRestrictionEdit') ) { ... } + * @endcode + * or: + * @code{,js} + * if ( mw.config.get('wgIsArticle') ) { ... } + * @endcode */ $wgLegacyJavaScriptGlobals = true; @@ -2663,8 +2923,8 @@ $wgLegacyJavaScriptGlobals = true; $wgResourceLoaderMaxQueryLength = -1; /** - * If set to true, JavaScript modules loaded from wiki pages will be parsed prior - * to minification to validate it. + * 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. @@ -2682,7 +2942,7 @@ $wgResourceLoaderValidateJS = true; $wgResourceLoaderValidateStaticJS = false; /** - * If set to true, asynchronous loading of bottom-queue scripts in the <head> + * If set to true, asynchronous loading of bottom-queue scripts in the "<head>" * will be enabled. This is an experimental feature that's supposed to make * JavaScript load faster. */ @@ -2718,19 +2978,25 @@ $wgMetaNamespaceTalk = false; * names of existing namespaces. Extensions developers should use * $wgCanonicalNamespaceNames. * - * PLEASE NOTE: Once you delete a namespace, the pages in that namespace will + * @warning Once you delete a namespace, the pages in that namespace will * no longer be accessible. If you rename it, then you can access them through * the new namespace name. * * Custom namespaces should start at 100 to avoid conflicting with standard * namespaces, and should always follow the even/odd main/talk pattern. + * + * @par Example: + * @code + * $wgExtraNamespaces = array( + * 100 => "Hilfe", + * 101 => "Hilfe_Diskussion", + * 102 => "Aide", + * 103 => "Discussion_Aide" + * ); + * @endcode + * + * @todo Add a note about maintenance/namespaceDupes.php */ -# $wgExtraNamespaces = array( -# 100 => "Hilfe", -# 101 => "Hilfe_Diskussion", -# 102 => "Aide", -# 103 => "Discussion_Aide" -# ); $wgExtraNamespaces = array(); /** @@ -2742,18 +3008,22 @@ $wgExtraNamespaces = array(); $wgExtraGenderNamespaces = array(); /** - * Namespace aliases + * 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 * requested with such a prefix, the request will be redirected to the primary * name. * * Set this to a map from namespace names to IDs. - * Example: + * + * @par Example: + * @code * $wgNamespaceAliases = array( * 'Wikipedian' => NS_USER, * 'Help' => 100, * ); + * @endcode */ $wgNamespaceAliases = array(); @@ -2768,8 +3038,8 @@ $wgNamespaceAliases = array(); * - + Enabled by default, but doesn't work with path to query rewrite rules, corrupted by apache * - ? Enabled by default, but doesn't work with path to PATH_INFO rewrites * - * All three of these punctuation problems can be avoided by using an alias, instead of a - * rewrite rule of either variety. + * All three of these punctuation problems can be avoided by using an alias, + * instead of a rewrite rule of either variety. * * The problem with % is that when using a path to query rewrite rule, URLs are * double-unescaped: once by Apache's path conversion code, and again by PHP. So @@ -2795,33 +3065,47 @@ $wgLocalInterwiki = false; */ $wgInterwikiExpiry = 10800; -/** Interwiki caching settings. - $wgInterwikiCache specifies path to constant database file - This cdb database is generated by dumpInterwiki from maintenance - and has such key formats: - dbname:key - a simple key (e.g. enwiki:meta) - _sitename:key - site-scope key (e.g. wiktionary:meta) - __global:key - global-scope key (e.g. __global:meta) - __sites:dbname - site mapping (e.g. __sites:enwiki) - Sites mapping just specifies site name, other keys provide - "local url" data layout. - $wgInterwikiScopes specify number of domains to check for messages: - 1 - Just wiki(db)-level - 2 - wiki and global levels - 3 - site levels - $wgInterwikiFallbackSite - if unable to resolve from cache +/** + * @name Interwiki caching settings. + * @{ + */ +/** + *$wgInterwikiCache specifies path to constant database file. + * + * This cdb database is generated by dumpInterwiki from maintenance and has + * such key formats: + * - dbname:key - a simple key (e.g. enwiki:meta) + * - _sitename:key - site-scope key (e.g. wiktionary:meta) + * - __global:key - global-scope key (e.g. __global:meta) + * - __sites:dbname - site mapping (e.g. __sites:enwiki) + * + * Sites mapping just specifies site name, other keys provide "local url" + * data layout. */ $wgInterwikiCache = false; +/** + * Specify number of domains to check for messages. + * - 1: Just wiki(db)-level + * - 2: wiki and global levels + * - 3: site levels + */ $wgInterwikiScopes = 3; +/** + * $wgInterwikiFallbackSite - if unable to resolve from cache + */ $wgInterwikiFallbackSite = 'wiki'; +/** @} */ # end of Interwiki caching settings. /** * If local interwikis are set up which allow redirects, * set this regexp to restrict URLs which will be displayed * as 'redirected from' links. * + * @par Example: * It might look something like this: + * @code * $wgRedirectSources = '!^https?://[a-z-]+\.wikipedia\.org/!'; + * @endcode * * Leave at false to avoid displaying any incoming redirect markers. * This does not affect intra-wiki redirects, which don't change @@ -2831,7 +3115,8 @@ $wgRedirectSources = false; /** * Set this to false to avoid forcing the first letter of links to capitals. - * WARNING: may break links! This makes links COMPLETELY case-sensitive. Links + * + * @warning may break links! This makes links COMPLETELY case-sensitive. Links * appearing with a capital at the beginning of a sentence will *not* go to the * same place as links in the middle of a sentence using a lowercase initial. */ @@ -2845,7 +3130,11 @@ $wgCapitalLinks = true; * associated content namespaces, the values for those are ignored in favor of the * subject namespace's setting. Setting for NS_MEDIA is taken automatically from * NS_FILE. - * EX: $wgCapitalLinkOverrides[ NS_FILE ] = false; + * + * @par Example: + * @code + * $wgCapitalLinkOverrides[ NS_FILE ] = false; + * @endcode */ $wgCapitalLinkOverrides = array(); @@ -2930,11 +3219,19 @@ $wgParserConf = array( $wgMaxTocLevel = 999; /** - * A complexity limit on template expansion + * A complexity limit on template expansion: the maximum number of nodes visited + * by PPFrame::expand() */ $wgMaxPPNodeCount = 1000000; /** + * A complexity limit on template expansion: the maximum number of nodes + * generated by Preprocessor::preprocessToObj() + */ +$wgMaxGeneratedPPNodeCount = 1000000; + + +/** * Maximum recursion depth for templates within templates. * The current parser adds two levels to the PHP call stack for each template, * and xdebug limits the call stack to 100 by default. So this should hopefully @@ -2978,11 +3275,11 @@ $wgAllowExternalImages = false; * You can use this to set up a trusted, simple repository of images. * You may also specify an array of strings to allow multiple sites * - * Examples: - * <code> + * @par Examples: + * @code * $wgAllowExternalImagesFrom = 'http://127.0.0.1/'; * $wgAllowExternalImagesFrom = array( 'http://127.0.0.1/', 'http://example.com' ); - * </code> + * @endcode */ $wgAllowExternalImagesFrom = ''; @@ -2997,7 +3294,7 @@ $wgAllowExternalImagesFrom = ''; $wgEnableImageWhitelist = true; /** - * A different approach to the above: simply allow the <img> tag to be used. + * A different approach to the above: simply allow the "<img>" tag to be used. * This allows you to specify alt text and other attributes, copy-paste HTML to * your wiki more easily, etc. However, allowing external images in any manner * will allow anyone with editing rights to snoop on your visitors' IP @@ -3039,7 +3336,7 @@ $wgTidyInternal = extension_loaded( 'tidy' ); */ $wgDebugTidy = false; -/** Allow raw, unchecked HTML in <html>...</html> sections. +/** Allow raw, unchecked HTML in "<html>...</html>" sections. * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions * TO RESTRICT EDITING to only those that you trust */ @@ -3245,7 +3542,6 @@ $wgDefaultUserOptions = array( 'gender' => 'unknown', 'hideminor' => 0, 'hidepatrolled' => 0, - 'highlightbroken' => 1, 'imagesize' => 2, 'justify' => 0, 'math' => 1, @@ -3308,7 +3604,7 @@ $wgInvalidUsernameCharacters = '@'; /** * Character used as a delimiter when testing for interwiki userrights * (In Special:UserRights, it is possible to modify users on different - * databases if the delimiter is used, e.g. Someuser@enwiki). + * databases if the delimiter is used, e.g. "Someuser@enwiki"). * * It is recommended that you have this delimiter in * $wgInvalidUsernameCharacters above, or you will not be able to @@ -3405,12 +3701,19 @@ $wgSysopEmailBans = true; * Limits on the possible sizes of range blocks. * * CIDR notation is hard to understand, it's easy to mistakenly assume that a - * /1 is a small range and a /31 is a large range. Setting this to half the - * number of bits avoids such errors. + * /1 is a small range and a /31 is a large range. For IPv4, setting a limit of + * half the number of bits avoids such errors, and allows entire ISPs to be + * blocked using a small number of range blocks. + * + * For IPv6, RFC 3177 recommends that a /48 be allocated to every residential + * customer, so range blocks larger than /64 (half the number of bits) will + * plainly be required. RFC 4692 implies that a very large ISP may be + * allocated a /19 if a generous HD-Ratio of 0.8 is used, so we will use that + * as our limit. As of 2012, blocking the whole world would require a /4 range. */ $wgBlockCIDRLimit = array( 'IPv4' => 16, # Blocks larger than a /16 (64k addresses) will not be allowed - 'IPv6' => 64, # 2^64 = ~1.8x10^19 addresses + 'IPv6' => 19, ); /** @@ -3423,18 +3726,19 @@ $wgBlockCIDRLimit = array( $wgBlockDisablesLogin = false; /** - * Pages anonymous user may see as an array, e.g. + * Pages anonymous user may see, set as an array of pages titles. * - * <code> + * @par Example: + * @code * $wgWhitelistRead = array ( "Main Page", "Wikipedia:Help"); - * </code> + * @endcode * * Special:Userlogin and Special:ChangePassword are always whitelisted. * - * NOTE: This will only work if $wgGroupPermissions['*']['read'] is false -- + * @note This will only work if $wgGroupPermissions['*']['read'] is false -- * see below. Otherwise, ALL pages are accessible, regardless of this setting. * - * Also note that this will only protect _pages in the wiki_. Uploaded files + * @note Also that this will only protect _pages in the wiki_. Uploaded files * will remain readable. You can use img_auth.php to protect uploaded files, * see http://www.mediawiki.org/wiki/Manual:Image_Authorization */ @@ -3448,6 +3752,7 @@ $wgEmailConfirmToEdit = false; /** * Permission keys given to users in each group. + * * This is an array where the keys are all groups and each value is an * array of the format (right => boolean). * @@ -3538,7 +3843,6 @@ $wgGroupPermissions['sysop']['reupload'] = true; $wgGroupPermissions['sysop']['reupload-shared'] = true; $wgGroupPermissions['sysop']['unwatchedpages'] = true; $wgGroupPermissions['sysop']['autoconfirmed'] = true; -$wgGroupPermissions['sysop']['upload_by_url'] = true; $wgGroupPermissions['sysop']['ipblock-exempt'] = true; $wgGroupPermissions['sysop']['blockemail'] = true; $wgGroupPermissions['sysop']['markbotedits'] = true; @@ -3548,6 +3852,7 @@ $wgGroupPermissions['sysop']['noratelimit'] = true; $wgGroupPermissions['sysop']['movefile'] = true; $wgGroupPermissions['sysop']['unblockself'] = true; $wgGroupPermissions['sysop']['suppressredirect'] = true; +#$wgGroupPermissions['sysop']['upload_by_url'] = true; #$wgGroupPermissions['sysop']['mergehistory'] = true; // Permission to change users' group assignments @@ -3558,6 +3863,7 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true; // Permission to export pages including linked pages regardless of $wgExportMaxLinkDepth #$wgGroupPermissions['bureaucrat']['override-export-depth'] = true; +#$wgGroupPermissions['sysop']['deletelogentry'] = true; #$wgGroupPermissions['sysop']['deleterevision'] = true; // To hide usernames from users and Sysops #$wgGroupPermissions['suppress']['hideuser'] = true; @@ -3578,6 +3884,7 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true; /** * Permission keys revoked from users in each group. + * * This acts the same way as wgGroupPermissions above, except that * if the user is in a group here, the permission will be removed from them. * @@ -3595,16 +3902,20 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' ); * A map of group names that the user is in, to group names that those users * are allowed to add or revoke. * - * Setting the list of groups to add or revoke to true is equivalent to "any group". - * - * For example, to allow sysops to add themselves to the "bot" group: + * Setting the list of groups to add or revoke to true is equivalent to "any + * group". * + * @par Example: + * To allow sysops to add themselves to the "bot" group: + * @code * $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) ); + * @endcode * + * @par Example: * Implicit groups may be used for the source group, for instance: - * + * @code * $wgGroupsRemoveFromSelf = array( '*' => true ); - * + * @endcode * This allows users in the '*' group (i.e. any user) to remove themselves from * any group that they happen to be in. * @@ -3640,13 +3951,16 @@ $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ); * namespace. If you list more than one permission, a user must * have all of them to edit pages in that namespace. * - * Note: NS_MEDIAWIKI is implicitly restricted to editinterface. + * @note NS_MEDIAWIKI is implicitly restricted to 'editinterface'. */ $wgNamespaceProtection = array(); /** * Pages in namespaces in this array can not be used as templates. - * Elements must be numeric namespace ids. + * + * Elements MUST be numeric namespace ids, you can safely use the MediaWiki + * namespaces constants (NS_USER, NS_MAIN...). + * * Among other things, this may be useful to enforce read-restrictions * which may otherwise be bypassed by using the template machanism. */ @@ -3662,11 +3976,15 @@ $wgNonincludableNamespaces = array(); * * When left at 0, all registered accounts will pass. * - * Example: - * <code> + * @par Example: + * Set automatic confirmation to 10 minutes (which is 600 seconds): + * @code * $wgAutoConfirmAge = 600; // ten minutes + * @endcode + * Set age to one day: + * @code * $wgAutoConfirmAge = 3600*24; // one day - * </code> + * @endcode */ $wgAutoConfirmAge = 0; @@ -3674,14 +3992,18 @@ $wgAutoConfirmAge = 0; * Number of edits an account requires before it is autoconfirmed. * Passing both this AND the time requirement is needed. Example: * - * <code> + * @par Example: + * @code * $wgAutoConfirmCount = 50; - * </code> + * @endcode */ $wgAutoConfirmCount = 0; /** * Automatically add a usergroup to any user who matches certain conditions. + * + * @todo Redocument $wgAutopromote + * * The format is * array( '&' or '|' or '^' or '!', cond1, cond2, ... ) * where cond1, cond2, ... are themselves conditions; *OR* @@ -3709,14 +4031,19 @@ $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 + * The format is: + * @code * 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 + * @endcode + * Where event is either: + * - 'onEdit' (when user edits) + * - 'onView' (when user views the wiki) + * + * Criteria has the same format as $wgAutopromote * * @see $wgAutopromote * @since 1.18 @@ -3734,16 +4061,23 @@ $wgAutopromoteOnceLogInRC = true; /** * $wgAddGroups and $wgRemoveGroups can be used to give finer control over who - * can assign which groups at Special:Userrights. Example configuration: + * can assign which groups at Special:Userrights. * + * @par Example: + * Bureaucrats can add any group: * @code - * // Bureaucrat can add any group * $wgAddGroups['bureaucrat'] = true; - * // Bureaucrats can only remove bots and sysops + * @endcode + * Bureaucrats can only remove bots and sysops: + * @code * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' ); - * // Sysops can make bots + * @endcode + * Sysops can make bots: + * @code * $wgAddGroups['sysop'] = array( 'bot' ); - * // Sysops can disable other sysops in an emergency, and disable bots + * @endcode + * Sysops can disable other sysops in an emergency, and disable bots: + * @code * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' ); * @endcode */ @@ -3763,8 +4097,10 @@ $wgAvailableRights = array(); */ $wgDeleteRevisionsLimit = 0; -/** Number of accounts each IP address may create, 0 to disable. - * Requires memcached */ +/** + * Number of accounts each IP address may create, 0 to disable. + * + * @warning Requires memcached */ $wgAccountCreationThrottle = 0; /** @@ -3774,8 +4110,9 @@ $wgAccountCreationThrottle = 0; * There's no administrator override on-wiki, so be careful what you set. :) * May be an array of regexes or a single string for backwards compatibility. * - * See http://en.wikipedia.org/wiki/Regular_expression - * Note that each regex needs a beginning/end delimiter, eg: # or / + * @see http://en.wikipedia.org/wiki/Regular_expression + * + * @note Each regex needs a beginning/end delimiter, eg: # or / */ $wgSpamRegex = array(); @@ -3783,54 +4120,46 @@ $wgSpamRegex = array(); $wgSummarySpamRegex = array(); /** - * Similarly you can get a function to do the job. The function will be given - * the following args: - * - a Title object for the article the edit is made on - * - the text submitted in the textarea (wpTextbox1) - * - the section number. - * The return should be boolean indicating whether the edit matched some evilness: - * - true : block it - * - false : let it through - * - * @deprecated since 1.17 Use hooks. See SpamBlacklist extension. - * @var $wgFilterCallback bool|string|Closure - */ -$wgFilterCallback = false; - -/** - * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies + * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open + * proxies * @since 1.16 */ $wgEnableDnsBlacklist = false; /** - * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for backward - * compatibility + * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for + * backward compatibility. */ $wgEnableSorbs = false; /** - * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. This is an - * array of either a URL or an array with the URL and a key (should the blacklist - * require a key). For example: + * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. + * + * This is an array of either a URL or an array with the URL and a key (should + * the blacklist require a key). + * + * @par Example: * @code * $wgDnsBlacklistUrls = array( * // String containing URL - * 'http.dnsbl.sorbs.net', + * 'http.dnsbl.sorbs.net.', * // Array with URL and key, for services that require a key - * array( 'dnsbl.httpbl.net', 'mykey' ), + * array( 'dnsbl.httpbl.net.', 'mykey' ), * // Array with just the URL. While this works, it is recommended that you * // just use a string as shown above - * array( 'opm.tornevall.org' ) + * array( 'opm.tornevall.org.' ) * ); * @endcode + * + * @note You should end the domain name with a . to avoid searching your + * eventual domain search suffixes. * @since 1.16 */ $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' ); /** - * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for backward - * compatibility + * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for + * backward compatibility. */ $wgSorbsUrl = array(); @@ -3841,13 +4170,24 @@ $wgSorbsUrl = array(); $wgProxyWhitelist = array(); /** - * Simple rate limiter options to brake edit floods. Maximum number actions - * allowed in the given number of seconds; after that the violating client re- - * ceives HTTP 500 error pages until the period elapses. + * Simple rate limiter options to brake edit floods. + * + * Maximum number actions allowed in the given number of seconds; after that + * the violating client receives HTTP 500 error pages until the period + * elapses. + * + * @par Example: + * To set a generic maximum of 4 hits in 60 seconds: + * @code + * $wgRateLimits = array( 4, 60 ); + * @endcode * - * array( 4, 60 ) for a maximum of 4 hits in 60 seconds. + * You could also limit per action and then type of users. See the inline + * code for a template to use. * - * This option set is experimental and likely to change. Requires memcached. + * This option set is experimental and likely to change. + * + * @warning Requires memcached. */ $wgRateLimits = array( 'edit' => array( @@ -3896,7 +4236,8 @@ $wgQueryPageDefaultLimit = 50; /** * Limit password attempts to X attempts per Y seconds per IP per account. - * Requires memcached. + * + * @warning Requires memcached. */ $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 ); @@ -3911,10 +4252,10 @@ $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 ); * If you enable this, every editor's IP address will be scanned for open HTTP * proxies. * - * Don't enable this. Many sysops will report "hostile TCP port scans" to your - * ISP and ask for your server to be shut down. - * + * @warning Don't enable this. Many sysops will report "hostile TCP port scans" + * to your ISP and ask for your server to be shut down. * You have been warned. + * */ $wgBlockOpenProxies = false; /** Port we want to scan for a proxy */ @@ -4048,22 +4389,29 @@ $wgDebugRedirects = false; /** * 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. + * This is normally false to avoid overlapping debug entries due to gen=css + * and gen=js requests. */ $wgDebugRawPage = false; /** * Send debug data to an HTML comment in the output. * - * This may occasionally be useful when supporting a non-technical end-user. It's - * more secure than exposing the debug log file to the web, since the output only - * contains private data for the current user. But it's not ideal for development - * use since data is lost on fatal errors and redirects. + * This may occasionally be useful when supporting a non-technical end-user. + * It's more secure than exposing the debug log file to the web, since the + * output only contains private data for the current user. But it's not ideal + * for development use since data is lost on fatal errors and redirects. */ $wgDebugComments = false; /** + * Extensive database transaction state debugging + * + * @since 1.20 + */ +$wgDebugDBTransactions = false; + +/** * Write SQL queries to the debug log */ $wgDebugDumpSql = false; @@ -4120,11 +4468,23 @@ $wgShowExceptionDetails = false; $wgShowDBErrorBacktrace = false; /** + * If true, send the exception backtrace to the error log + */ +$wgLogExceptionBacktrace = true; + +/** * Expose backend server host names through the API and various HTML comments */ $wgShowHostnames = false; /** + * Override server hostname detection with a hardcoded value. + * Should be a string, default false. + * @since 1.20 + */ +$wgOverrideHostname = false; + +/** * If set to true MediaWiki will throw notices for some possible error * conditions and for deprecated functions. */ @@ -4135,7 +4495,7 @@ $wgDevelopmentWarnings = false; * development warnings will not be generated for deprecations added in releases * after the limit. */ -$wgDeprecationReleaseLimit = '1.17'; +$wgDeprecationReleaseLimit = false; /** Only record profiling info for pages that took longer than this */ $wgProfileLimit = 0.0; @@ -4201,6 +4561,14 @@ $wgAggregateStatsID = false; $wgDisableCounters = false; /** + * Set this to an integer to only do synchronous site_stats updates + * one every *this many* updates. The other requests go into pending + * delta values in $wgMemc. Make sure that $wgMemc is a global cache. + * If set to -1, updates *only* go to $wgMemc (useful for daemons). + */ +$wgSiteStatsAsyncFactor = false; + +/** * Parser test suite files to be run by parserTests.php when no specific * filename is passed to it. * @@ -4228,7 +4596,7 @@ $wgParserTestFiles = array( * ); */ $wgParserTestRemote = false; - + /** * Allow running of javascript test suites via [[Special:JavaScriptTest]] (such as QUnit). */ @@ -4239,7 +4607,17 @@ $wgEnableJavaScriptTest = false; */ $wgJavaScriptTestConfig = array( 'qunit' => array( + // Page where documentation can be found relevant to the QUnit test suite being ran. + // Used in the intro paragraph on [[Special:JavaScriptTest/qunit]] for the + // documentation link in the "javascripttest-qunit-intro" message. 'documentation' => '//www.mediawiki.org/wiki/Manual:JavaScript_unit_testing', + // If you are submitting the QUnit test suite to a TestSwarm instance, + // point this to the "inject.js" script of that instance. This is was registers + // the QUnit hooks to extract the test results and push them back up into the + // TestSwarm database. + // @example 'http://localhost/testswarm/js/inject.js' + // @example '//integration.mediawiki.org/testswarm/js/inject.js' + 'testswarm-injectjs' => false, ), ); @@ -4307,16 +4685,10 @@ $wgCountTotalSearchHits = false; $wgOpenSearchTemplate = false; /** - * Enable suggestions while typing in search boxes - * (results are passed around in OpenSearch format) - * Requires $wgEnableOpenSearchSuggest = true; - */ -$wgEnableMWSuggest = false; - -/** * Enable OpenSearch suggestions requested by MediaWiki. Set this to - * false if you've disabled MWSuggest or another suggestion script and - * want reduce load caused by cached scripts pulling suggestions. + * false if you've disabled scripts that use api?action=opensearch and + * want reduce load caused by cached scripts still pulling suggestions. + * It will let the API fallback by responding with an empty array. */ $wgEnableOpenSearchSuggest = true; @@ -4326,26 +4698,19 @@ $wgEnableOpenSearchSuggest = true; $wgSearchSuggestCacheExpiry = 1200; /** - * Template for internal MediaWiki suggestion engine, defaults to API action=opensearch - * - * Placeholders: {searchTerms}, {namespaces}, {dbname} - * - */ -$wgMWSuggestTemplate = false; - -/** * If you've disabled search semi-permanently, this also disables updates to the * table. If you ever re-enable, be sure to rebuild the search table. */ $wgDisableSearchUpdate = false; /** - * List of namespaces which are searched by default. Example: + * List of namespaces which are searched by default. * - * <code> + * @par Example: + * @code * $wgNamespacesToBeSearchedDefault[NS_MAIN] = true; * $wgNamespacesToBeSearchedDefault[NS_PROJECT] = true; - * </code> + * @endcode */ $wgNamespacesToBeSearchedDefault = array( NS_MAIN => true, @@ -4353,9 +4718,9 @@ $wgNamespacesToBeSearchedDefault = array( /** * Namespaces to be searched when user clicks the "Help" tab - * on Special:Search + * on Special:Search. * - * Same format as $wgNamespacesToBeSearchedDefault + * Same format as $wgNamespacesToBeSearchedDefault. */ $wgNamespacesToBeSearchedHelp = array( NS_PROJECT => true, @@ -4363,8 +4728,10 @@ $wgNamespacesToBeSearchedHelp = array( ); /** - * If set to true the 'searcheverything' preference will be effective only for logged-in users. - * Useful for big wikis to maintain different search profiles for anonymous and logged-in users. + * If set to true the 'searcheverything' preference will be effective only for + * logged-in users. + * Useful for big wikis to maintain different search profiles for anonymous and + * logged-in users. * */ $wgSearchEverythingOnlyLoggedIn = false; @@ -4380,18 +4747,22 @@ $wgDisableInternalSearch = false; * If the URL includes '$1', this will be replaced with the URL-encoded * search term. * - * For example, to forward to Google you'd have something like: - * $wgSearchForwardUrl = 'http://www.google.com/search?q=$1' . - * '&domains=http://example.com' . - * '&sitesearch=http://example.com' . - * '&ie=utf-8&oe=utf-8'; + * @par Example: + * To forward to Google you'd have something like: + * @code + * $wgSearchForwardUrl = + * 'http://www.google.com/search?q=$1' . + * '&domains=http://example.com' . + * '&sitesearch=http://example.com' . + * '&ie=utf-8&oe=utf-8'; + * @endcode */ $wgSearchForwardUrl = null; /** - * Search form behavior - * true = use Go & Search buttons - * false = use Go button & Advanced search link + * Search form behavior. + * - true = use Go & Search buttons + * - false = use Go button & Advanced search link */ $wgUseTwoButtonsSearchForm = true; @@ -4408,11 +4779,13 @@ $wgSitemapNamespaces = false; * maintenance/generateSitemap.php script. * * This should be a map of namespace IDs to priority - * Example: + * @par Example: + * @code * $wgSitemapNamespacesPriorities = array( * NS_USER => '0.9', * NS_HELP => '0.0', * ); + * @endcode */ $wgSitemapNamespacesPriorities = false; @@ -4530,6 +4903,22 @@ $wgReadOnlyFile = false; */ $wgUpgradeKey = false; +/** + * Map GIT repository URLs to viewer URLs to provide links in Special:Version + * + * Key is a pattern passed to preg_match() and preg_replace(), + * without the delimiters (which are #) and must match the whole URL. + * The value is the replacement for the key (it can contain $1, etc.) + * %h will be replaced by the short SHA-1 (7 first chars) and %H by the + * full SHA-1 of the HEAD revision. + * + * @since 1.20 + */ +$wgGitRepositoryViewers = array( + 'https://gerrit.wikimedia.org/r/p/(.*)' => 'https://gerrit.wikimedia.org/r/gitweb?p=$1;h=%H', + 'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' => 'https://gerrit.wikimedia.org/r/gitweb?p=$1;h=%H', +); + /** @} */ # End of maintenance } /************************************************************************//** @@ -4627,18 +5016,23 @@ $wgFeedDiffCutoff = 32768; /** Override the site's default RSS/ATOM feed for recentchanges that appears on * every page. Some sites might have a different feed they'd like to promote * instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one). - * Ex: $wgSiteFeed['format'] = "http://example.com/somefeed.xml"; Format can be one - * of either 'rss' or 'atom'. + * Should be a format as key (either 'rss' or 'atom') and an URL to the feed + * as value. + * @par Example: + * Configure the 'atom' feed to http://example.com/somefeed.xml + * @code + * $wgSiteFeed['atom'] = "http://example.com/somefeed.xml"; + * @endcode */ $wgOverrideSiteFeed = array(); /** - * Available feeds objects + * Available feeds objects. * Should probably only be defined when a page is syndicated ie when - * $wgOut->isSyndicated() is true + * $wgOut->isSyndicated() is true. */ $wgFeedClasses = array( - 'rss' => 'RSSFeed', + 'rss' => 'RSSFeed', 'atom' => 'AtomFeed', ); @@ -4797,9 +5191,9 @@ $wgExportAllowListContributors = false; * can become *insanely large* and could easily break your wiki, * it's disabled by default for now. * - * There's a HARD CODED limit of 5 levels of recursion to prevent a - * crazy-big export from being done by someone setting the depth - * number too high. In other words, last resort safety net. + * @warning There's a HARD CODED limit of 5 levels of recursion to prevent a + * crazy-big export from being done by someone setting the depth number too + * high. In other words, last resort safety net. */ $wgExportMaxLinkDepth = 0; @@ -4821,7 +5215,8 @@ $wgExportAllowAll = false; */ /** - * A list of callback functions which are called once MediaWiki is fully initialised + * A list of callback functions which are called once MediaWiki is fully + * initialised */ $wgExtensionFunctions = array(); @@ -4836,9 +5231,10 @@ $wgExtensionFunctions = array(); * Variables defined in extensions will override conflicting variables defined * in the core. * - * Example: - * $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php'; - * + * @par Example: + * @code + * $wgExtensionMessagesFiles['ConfirmEdit'] = __DIR__.'/ConfirmEdit.i18n.php'; + * @endcode */ $wgExtensionMessagesFiles = array(); @@ -4852,7 +5248,9 @@ $wgExtensionMessagesFiles = array(); * Registration is done with $pout->addOutputHook( $tag, $data ). * * The callback has the form: + * @code * function outputHook( $outputPage, $parserOutput, $data ) { ... } + * @endcode */ $wgParserOutputHooks = array(); @@ -4883,7 +5281,7 @@ $wgAutoloadClasses = array(); * urls, descriptions and pointers to localized description msgs. Note that * the version, url, description and descriptionmsg key can be omitted. * - * <code> + * @code * $wgExtensionCredits[$type][] = array( * 'name' => 'Example extension', * 'version' => 1.9, @@ -4893,7 +5291,7 @@ $wgAutoloadClasses = array(); * 'description' => 'An example extension', * 'descriptionmsg' => 'exampleextension-desc', * ); - * </code> + * @endcode * * Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'. * Where 'descriptionmsg' can be an array with message key and parameters: @@ -4909,12 +5307,30 @@ $wgAuth = null; /** * Global list of hooks. - * Add a hook by doing: + * + * The key is one of the events made available by MediaWiki, you can find + * a description for most of them in docs/hooks.txt. The array is used + * internally by Hook:run(). + * + * The value can be one of: + * + * - A function name: + * @code * $wgHooks['event_name'][] = $function; - * or: + * @endcode + * - A function with some data: + * @code * $wgHooks['event_name'][] = array($function, $data); - * or: + * @endcode + * - A an object method: + * @code * $wgHooks['event_name'][] = array($object, 'method'); + * @endcode + * + * @warning You should always append to an event array or you will end up + * deleting a previous registered hook. + * + * @todo Does it support PHP closures? */ $wgHooks = array(); @@ -5068,17 +5484,19 @@ $wgLogRestrictions = array( * * See $wgLogTypes for a list of available log types. * - * For example: + * @par Example: + * @code * $wgFilterLogTypes => array( * 'move' => true, * 'import' => false, * ); + * @endcode * * Will display show/hide links for the move and import logs. Move logs will be * hidden by default unless the link is clicked. Import logs will be shown by * default, and hidden when the link is clicked. * - * A message of the form log-show-hide-<type> should be added, and will be used + * A message of the form log-show-hide-[type] should be added, and will be used * for the link text. */ $wgFilterLogTypes = array( @@ -5091,7 +5509,7 @@ $wgFilterLogTypes = array( * * Extensions with custom log types may add to this array. * - * Since 1.19, if you follow the naming convention log-name-TYPE, + * @since 1.19, if you follow the naming convention log-name-TYPE, * where TYPE is your log type, yoy don't need to use this array. */ $wgLogNames = array( @@ -5114,7 +5532,7 @@ $wgLogNames = array( * * Extensions with custom log types may add to this array. * - * Since 1.19, if you follow the naming convention log-description-TYPE, + * @since 1.19, if you follow the naming convention log-description-TYPE, * where TYPE is your log type, yoy don't need to use this array. */ $wgLogHeaders = array( @@ -5164,10 +5582,12 @@ $wgLogActions = array( * @see LogFormatter */ $wgLogActionsHandlers = array( - // move, move_redir - 'move/*' => 'MoveLogFormatter', - // delete, restore, revision, event - 'delete/*' => 'DeleteLogFormatter', + 'move/move' => 'MoveLogFormatter', + 'move/move_redir' => 'MoveLogFormatter', + 'delete/delete' => 'DeleteLogFormatter', + 'delete/restore' => 'DeleteLogFormatter', + 'delete/revision' => 'DeleteLogFormatter', + 'delete/event' => 'DeleteLogFormatter', 'suppress/revision' => 'DeleteLogFormatter', 'suppress/event' => 'DeleteLogFormatter', 'suppress/delete' => 'DeleteLogFormatter', @@ -5266,6 +5686,7 @@ $wgSpecialPageGroups = array( 'Mostlinkedtemplates' => 'highuse', 'Mostcategories' => 'highuse', 'Mostimages' => 'highuse', + 'Mostinterwikis' => 'highuse', 'Mostrevisions' => 'highuse', 'Allpages' => 'pages', @@ -5328,7 +5749,7 @@ $wgMaxRedirectLinksRetrieved = 500; */ /** - * Array of allowed values for the title=foo&action=<action> parameter. Syntax is: + * 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() @@ -5364,11 +5785,6 @@ $wgActions = array( */ $wgDisabledActions = array(); -/** - * Allow the "info" action, very inefficient at the moment - */ -$wgAllowPageInfo = false; - /** @} */ # end actions } /*************************************************************************//** @@ -5393,8 +5809,10 @@ $wgDefaultRobotPolicy = 'index,follow'; * URLs, so search engine spiders risk getting lost in a maze of twisty special * pages, all alike, and never reaching your actual content. * - * Example: + * @par Example: + * @code * $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' ); + * @endcode */ $wgNamespaceRobotPolicies = array(); @@ -5402,10 +5820,18 @@ $wgNamespaceRobotPolicies = array(); * Robot policies per article. These override the per-namespace robot policies. * Must be in the form of an array where the key part is a properly canonical- * ised text form title and the value is a robot policy. - * Example: - * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex,follow', - * 'User:Bob' => 'index,follow' ); - * Example that DOES NOT WORK because the names are not canonical text forms: + * + * @par Example: + * @code + * $wgArticleRobotPolicies = array( + * 'Main Page' => 'noindex,follow', + * 'User:Bob' => 'index,follow', + * ); + * @endcode + * + * @par Example that DOES NOT WORK because the names are not canonical text + * forms: + * @code * $wgArticleRobotPolicies = array( * # Underscore, not space! * 'Main_Page' => 'noindex,follow', @@ -5414,6 +5840,7 @@ $wgNamespaceRobotPolicies = array(); * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false for that namespace)! * 'abc' => 'noindex,nofollow' * ); + * @endcode */ $wgArticleRobotPolicies = array(); @@ -5421,8 +5848,11 @@ $wgArticleRobotPolicies = array(); * An array of namespace keys in which the __INDEX__/__NOINDEX__ magic words * will not function, so users can't decide whether pages in that namespace are * indexed by search engines. If set to null, default to $wgContentNamespaces. - * Example: + * + * @par Example: + * @code * $wgExemptFromUserRobotsControl = array( NS_MAIN, NS_TALK, NS_PROJECT ); + * @endcode */ $wgExemptFromUserRobotsControl = null; @@ -5452,9 +5882,10 @@ $wgEnableAPI = true; $wgEnableWriteAPI = true; /** - * API module extensions + * API module extensions. * Associative array mapping module name to class name. * Extension modules may override the core modules. + * @todo Describe each of the variables, group them and add examples */ $wgAPIModules = array(); $wgAPIMetaModules = array(); @@ -5469,7 +5900,7 @@ $wgAPIMaxDBRows = 5000; /** * The maximum size (in bytes) of an API result. - * Don't set this lower than $wgMaxArticleSize*1024 + * @warning Do not set this lower than $wgMaxArticleSize*1024 */ $wgAPIMaxResultSize = 8388608; @@ -5524,17 +5955,18 @@ $wgAjaxLicensePreview = true; * This is currently only used by the API (requests to api.php) * $wgCrossSiteAJAXdomains can be set using a wildcard syntax: * - * '*' matches any number of characters - * '?' matches any 1 character - * - * Example: - $wgCrossSiteAJAXdomains = array( - 'www.mediawiki.org', - '*.wikipedia.org', - '*.wikimedia.org', - '*.wiktionary.org', - ); + * - '*' matches any number of characters + * - '?' matches any 1 character * + * @par Example: + * @code + * $wgCrossSiteAJAXdomains = array( + * 'www.mediawiki.org', + * '*.wikipedia.org', + * '*.wikimedia.org', + * '*.wiktionary.org', + * ); + * @endcode */ $wgCrossSiteAJAXdomains = array(); @@ -5638,7 +6070,7 @@ $wgUpdateRowsPerQuery = 100; /** * The build directory for HipHop compilation. - * Defaults to $IP/maintenance/hiphop/build. + * Defaults to '$IP/maintenance/hiphop/build'. */ $wgHipHopBuildDirectory = false; @@ -5658,8 +6090,9 @@ $wgHipHopCompilerProcs = 'detect'; * * To compile extensions with HipHop, set $wgExtensionsDirectory correctly, * and use code like: - * + * @code * require( MWInit::extensionSetupPath( 'Extension/Extension.php' ) ); + * @endcode * * to include the extension setup file from LocalSettings.php. It is not * necessary to set this variable unless you use MWInit::extensionSetupPath(). @@ -5682,6 +6115,19 @@ $wgCompiledFiles = array(); /************************************************************************//** + * @name Mobile support + * @{ + */ + +/** + * Name of the class used for mobile device detection, must be inherited from + * IDeviceDetector. + */ +$wgDeviceDetectionClass = 'DeviceDetection'; + +/** @} */ # End of Mobile support } + +/************************************************************************//** * @name Miscellaneous * @{ */ @@ -5691,9 +6137,11 @@ $wgExternalDiffEngine = false; /** * Disable redirects to special pages and interwiki redirects, which use a 302 - * and have no "redirected from" link. Note this is only for articles with #Redirect - * in them. URL's containing a local interwiki prefix (or a non-canonical special - * page name) are still hard redirected regardless of this setting. + * and have no "redirected from" link. + * + * @note This is only for articles with #REDIRECT in them. URL's containing a + * local interwiki prefix (or a non-canonical special page name) are still hard + * redirected regardless of this setting. */ $wgDisableHardRedirects = false; @@ -5704,8 +6152,8 @@ $wgDisableHardRedirects = false; $wgLinkHolderBatchSize = 1000; /** - * By default MediaWiki does not register links pointing to same server in externallinks dataset, - * use this value to override: + * By default MediaWiki does not register links pointing to same server in + * externallinks dataset, use this value to override: */ $wgRegisterInternalExternals = false; @@ -5733,8 +6181,10 @@ $wgRedirectOnLogin = null; * This configuration array maps pool types to an associative array. The only * defined key in the associative array is "class", which gives the class name. * The remaining elements are passed through to the class as constructor - * parameters. Example: + * parameters. * + * @par Example: + * @code * $wgPoolCounterConf = array( 'ArticleView' => array( * 'class' => 'PoolCounter_Client', * 'timeout' => 15, // wait timeout in seconds @@ -5742,6 +6192,7 @@ $wgRedirectOnLogin = null; * 'maxqueue' => 50, // maximum number of total threads in each pool * ... any extension-specific options... * ); + * @endcode */ $wgPoolCounterConf = null; @@ -5760,6 +6211,13 @@ $wgDBtestuser = ''; //db user that has permission to create and drop the test da $wgDBtestpassword = ''; /** + * Whether the user must enter their password to change their e-mail address + * + * @since 1.20 + */ +$wgRequirePasswordforEmailChange = true; + +/** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker * @} diff --git a/includes/DeferredUpdates.php b/includes/DeferredUpdates.php index 262994e3..b4989a69 100644 --- a/includes/DeferredUpdates.php +++ b/includes/DeferredUpdates.php @@ -1,5 +1,26 @@ <?php /** + * Interface and manager for deferred updates. + * + * 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 + */ + +/** * Interface that deferrable updates should implement. Basically required so we * can validate input on DeferredUpdates::addUpdate() * @@ -67,10 +88,19 @@ class DeferredUpdates { } foreach ( $updates as $update ) { - $update->doUpdate(); + try { + $update->doUpdate(); - if ( $doCommit && $dbw->trxLevel() ) { - $dbw->commit( __METHOD__ ); + if ( $doCommit && $dbw->trxLevel() ) { + $dbw->commit( __METHOD__ ); + } + } catch ( MWException $e ) { + // We don't want exceptions thrown during deferred updates to + // be reported to the user since the output is already sent. + // Instead we just log them. + if ( !$e instanceof ErrorPageError ) { + wfDebugLog( 'exception', $e->getLogMessage() ); + } } } diff --git a/includes/Defines.php b/includes/Defines.php index 26deb2ba..be9f9816 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -6,10 +6,29 @@ * since this file will not be executed during request startup for a compiled * MediaWiki. * + * 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 */ /** + * @defgroup Constants MediaWiki constants + */ + +/** * Version constants for the benefit of extensions */ define( 'MW_SPECIALPAGE_VERSION', 2 ); @@ -25,6 +44,8 @@ define( 'DBO_DEFAULT', 16 ); define( 'DBO_PERSISTENT', 32 ); define( 'DBO_SYSDBA', 64 ); //for oracle maintenance define( 'DBO_DDLMODE', 128 ); // when using schema files: mostly for Oracle +define( 'DBO_SSL', 256 ); +define( 'DBO_COMPRESS', 512 ); /**@}*/ /**@{ @@ -125,8 +146,8 @@ define( 'AV_SCAN_FAILED', false ); #scan failed (scanner not found or error in * Anti-lock flags * See DefaultSettings.php for a description */ -define( 'ALF_PRELOAD_LINKS', 1 ); -define( 'ALF_PRELOAD_EXISTENCE', 2 ); +define( 'ALF_PRELOAD_LINKS', 1 ); // unused +define( 'ALF_PRELOAD_EXISTENCE', 2 ); // unused define( 'ALF_NO_LINK_LOCK', 4 ); define( 'ALF_NO_BLOCK_LOCK', 8 ); /**@}*/ @@ -184,7 +205,7 @@ define( 'LIST_SET_PREPARED', 8); // List of (?, ?, ?) for DatabaseIbm_db2 /** * Unicode and normalisation related */ -require_once dirname(__FILE__).'/normal/UtfNormalDefines.php'; +require_once __DIR__.'/normal/UtfNormalDefines.php'; /**@{ * Hook support constants diff --git a/includes/DeprecatedGlobal.php b/includes/DeprecatedGlobal.php new file mode 100644 index 00000000..4d7b9689 --- /dev/null +++ b/includes/DeprecatedGlobal.php @@ -0,0 +1,55 @@ +<?php +/** + * Delayed loading of deprecated global objects. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Class to allow throwing wfDeprecated warnings + * when people use globals that we do not want them to. + * (For example like $wgArticle) + */ + +class DeprecatedGlobal extends StubObject { + // The m's are to stay consistent with parent class. + protected $mRealValue, $mVersion; + + function __construct( $name, $realValue, $version = false ) { + parent::__construct( $name ); + $this->mRealValue = $realValue; + $this->mVersion = $version; + } + + function _newObject() { + /* Put the caller offset for wfDeprecated as 6, as + * that gives the function that uses this object, since: + * 1 = this function ( _newObject ) + * 2 = StubObject::_unstub + * 3 = StubObject::_call + * 4 = StubObject::__call + * 5 = DeprecatedGlobal::<method of global called> + * 6 = Actual function using the global. + * Of course its theoretically possible to have other call + * sequences for this method, but that seems to be + * rather unlikely. + */ + wfDeprecated( '$' . $this->mGlobal, $this->mVersion, false, 6 ); + return $this->mRealValue; + } +} diff --git a/includes/EditPage.php b/includes/EditPage.php index d00d9114..b762cad1 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -1,6 +1,22 @@ <?php /** - * Contains the EditPage class + * Page edition user interface. + * + * 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 */ @@ -37,11 +53,6 @@ class EditPage { const AS_HOOK_ERROR = 210; /** - * Status: The filter function set in $wgFilterCallback returned true (= block it) - */ - const AS_FILTERING = 211; - - /** * Status: A hook function returned an error */ const AS_HOOK_ERROR_EXPECTED = 212; @@ -145,6 +156,11 @@ class EditPage { const AS_IMAGE_REDIRECT_LOGGED = 234; /** + * HTML id and name for the beginning of the edit form. + */ + const EDITFORM_ID = 'editform'; + + /** * @var Article */ var $mArticle; @@ -183,6 +199,12 @@ class EditPage { */ var $mParserOutput; + /** + * Has a summary been preset using GET parameter &summary= ? + * @var Bool + */ + var $hasPresetSummary = false; + var $mBaseRevision = false; var $mShowSummaryField = true; @@ -282,7 +304,7 @@ class EditPage { } wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__.": enter\n" ); + wfDebug( __METHOD__ . ": enter\n" ); // If they used redlink=1 and the page exists, redirect to the main article if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) { @@ -333,7 +355,7 @@ class EditPage { return; } - wfProfileIn( __METHOD__."-business-end" ); + wfProfileIn( __METHOD__ . "-business-end" ); $this->isConflict = false; // css / js subpages of user pages get a special treatment @@ -355,7 +377,7 @@ class EditPage { if ( 'save' == $this->formtype ) { if ( !$this->attemptSave() ) { - wfProfileOut( __METHOD__."-business-end" ); + wfProfileOut( __METHOD__ . "-business-end" ); wfProfileOut( __METHOD__ ); return; } @@ -366,18 +388,18 @@ class EditPage { if ( 'initial' == $this->formtype || $this->firsttime ) { if ( $this->initialiseForm() === false ) { $this->noSuchSectionPage(); - wfProfileOut( __METHOD__."-business-end" ); + wfProfileOut( __METHOD__ . "-business-end" ); wfProfileOut( __METHOD__ ); return; } - if ( !$this->mTitle->getArticleId() ) + if ( !$this->mTitle->getArticleID() ) wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); else wfRunHooks( 'EditFormInitialText', array( $this ) ); } $this->showEditForm(); - wfProfileOut( __METHOD__."-business-end" ); + wfProfileOut( __METHOD__ . "-business-end" ); wfProfileOut( __METHOD__ ); } @@ -394,7 +416,7 @@ class EditPage { } # Ignore some permissions errors when a user is just previewing/viewing diffs $remove = array(); - foreach( $permErrors as $error ) { + foreach ( $permErrors as $error ) { if ( ( $this->preview || $this->diff ) && ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' ) ) { @@ -469,7 +491,7 @@ class EditPage { */ function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) { wfDeprecated( __METHOD__, '1.19' ); - + global $wgRequest, $wgOut; if ( $wgRequest->getBool( 'redlink' ) ) { // The edit page was reached via a red link. @@ -501,7 +523,7 @@ class EditPage { // Standard preference behaviour return true; } elseif ( !$this->mTitle->exists() && - isset($wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]) && + isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) && $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] ) { // Categories are special @@ -518,7 +540,7 @@ class EditPage { * @return bool */ protected function isWrongCaseCssJsPage() { - if( $this->mTitle->isCssJsSubpage() ) { + if ( $this->mTitle->isCssJsSubpage() ) { $name = $this->mTitle->getSkinFromCssJsSubpage(); $skins = array_merge( array_keys( Skin::getSkinNames() ), @@ -558,31 +580,31 @@ class EditPage { # Also remove trailing whitespace, but don't remove _initial_ # whitespace from the text boxes. This may be significant formatting. $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' ); - if ( !$request->getCheck('wpTextbox2') ) { + if ( !$request->getCheck( 'wpTextbox2' ) ) { // Skip this if wpTextbox2 has input, it indicates that we came // from a conflict page with raw page text, not a custom form // modified by subclasses - wfProfileIn( get_class($this)."::importContentFormData" ); + wfProfileIn( get_class( $this ) . "::importContentFormData" ); $textbox1 = $this->importContentFormData( $request ); - if ( isset($textbox1) ) + if ( isset( $textbox1 ) ) $this->textbox1 = $textbox1; - wfProfileOut( get_class($this)."::importContentFormData" ); + wfProfileOut( get_class( $this ) . "::importContentFormData" ); } - # Truncate for whole multibyte characters. +5 bytes for ellipsis - $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); + # Truncate for whole multibyte characters + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 255 ); # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for # section titles. $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary ); - + # Treat sectiontitle the same way as summary. # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is # currently doing double duty as both edit summary and section title. Right now this # is just to allow API edits to work around this limitation, but this should be # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312). - $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 250 ); + $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 255 ); $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle ); $this->edittime = $request->getVal( 'wpEdittime' ); @@ -650,7 +672,7 @@ class EditPage { { $this->allowBlankSummary = true; } else { - $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary'); + $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) || !$wgUser->getOption( 'forceeditsummary' ); } $this->autoSumm = $request->getText( 'wpAutoSummary' ); @@ -669,7 +691,7 @@ class EditPage { $this->minoredit = false; $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden by request parameters $this->recreate = false; - + // When creating a new section, we can preload a section title by passing it as the // preloadtitle parameter in the URL (Bug 13100) if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { @@ -679,6 +701,9 @@ class EditPage { } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { $this->summary = $request->getText( 'summary' ); + if ( $this->summary !== '' ) { + $this->hasPresetSummary = true; + } } if ( $request->getVal( 'minor' ) ) { @@ -731,7 +756,7 @@ class EditPage { } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { # Watch creations $this->watchthis = true; - } elseif ( $this->mTitle->userIsWatching() ) { + } elseif ( $wgUser->isWatched( $this->mTitle ) ) { # Already watched $this->watchthis = true; } @@ -796,7 +821,7 @@ class EditPage { # Otherwise, $text will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && $undorev->getPage() == $oldrev->getPage() && - $undorev->getPage() == $this->mTitle->getArticleId() && + $undorev->getPage() == $this->mTitle->getArticleID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { @@ -810,12 +835,13 @@ class EditPage { # If we just undid one rev, use an autosummary $firstrev = $oldrev->getNext(); - if ( $firstrev->getId() == $undo ) { - $undoSummary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() ); + if ( $firstrev && $firstrev->getId() == $undo ) { + $undoSummary = wfMessage( 'undo-summary', $undo, $undorev->getUserText() )->inContentLanguage()->text(); if ( $this->summary === '' ) { $this->summary = $undoSummary; } else { - $this->summary = $undoSummary . wfMsgForContent( 'colon-separator' ) . $this->summary; + $this->summary = $undoSummary . wfMessage( 'colon-separator' ) + ->inContentLanguage()->text() . $this->summary; } $this->undidRev = $undo; } @@ -830,7 +856,7 @@ class EditPage { $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}"; $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" . - wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true ); + wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true ); } if ( $text === false ) { @@ -852,7 +878,7 @@ class EditPage { * * This difers from Article::getContent() that when a missing revision is * encountered the result will be an empty string and not the - * 'missing-article' message. + * 'missing-revision' message. * * @since 1.19 * @return string @@ -907,7 +933,7 @@ class EditPage { if ( !empty( $this->mPreloadText ) ) { return $this->mPreloadText; } - + if ( $preload === '' ) { return ''; } @@ -959,7 +985,6 @@ class EditPage { $bot = $wgUser->isAllowed( 'bot' ) && $this->bot; $status = $this->internalAttemptSave( $resultDetails, $bot ); // FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status - if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) { $this->didSave = true; } @@ -976,7 +1001,6 @@ class EditPage { return true; case self::AS_HOOK_ERROR: - case self::AS_FILTERING: return false; case self::AS_SUCCESS_NEW_ARTICLE: @@ -1011,7 +1035,7 @@ class EditPage { return false; case self::AS_BLOCKED_PAGE_FOR_USER: - throw new UserBlockedError( $wgUser->mBlock ); + throw new UserBlockedError( $wgUser->getBlock() ); case self::AS_IMAGE_REDIRECT_ANON: case self::AS_IMAGE_REDIRECT_LOGGED: @@ -1031,8 +1055,15 @@ class EditPage { $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage'; throw new PermissionsError( $permission ); + default: + // We don't recognize $status->value. The only way that can happen + // is if an extension hook aborted from inside ArticleSave. + // Render the status object into $this->hookError + // FIXME this sucks, we should just use the Status object throughout + $this->hookError = '<div class="error">' . $status->getWikitext() . + '</div>'; + return true; } - return false; } /** @@ -1048,8 +1079,7 @@ class EditPage { * 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, $wgRequest, $wgParser; - global $wgMaxArticleSize; + global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize; $status = Status::newGood(); @@ -1095,13 +1125,6 @@ class EditPage { wfProfileOut( __METHOD__ ); return $status; } - if ( $wgFilterCallback && is_callable( $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 $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' ); @@ -1179,9 +1202,10 @@ class EditPage { wfProfileOut( __METHOD__ . '-checks' ); - # If article is new, insert it. - $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); - $new = ( $aid == 0 ); + # Load the page data from the master. If anything changes in the meantime, + # we detect it by using page_latest like a token in a 1 try compare-and-swap. + $this->mArticle->loadPageData( 'fromdbmaster' ); + $new = !$this->mArticle->exists(); if ( $new ) { // Late check for create permission, just in case *PARANOIA* @@ -1215,44 +1239,37 @@ class EditPage { 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 $status; - } - } - $text = $this->textbox1; $result['sectionanchor'] = ''; if ( $this->section == 'new' ) { if ( $this->sectiontitle !== '' ) { // Insert the section title above the content. - $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text; - + $text = wfMessage( 'newsectionheaderdefaultlevel', $this->sectiontitle ) + ->inContentLanguage()->text() . "\n\n" . $text; + // Jump to the new section $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); - + // If no edit summary was specified, create one automatically from the section // title and have it link to the new section. Otherwise, respect the summary as // passed. if ( $this->summary === '' ) { $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + $this->summary = wfMessage( 'newsectionsummary' ) + ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); } } elseif ( $this->summary !== '' ) { // Insert the section title above the content. - $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text; - + $text = wfMessage( 'newsectionheaderdefaultlevel', $this->summary ) + ->inContentLanguage()->text() . "\n\n" . $text; + // Jump to the new section $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); // Create a link to the new section from the edit summary. $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + $this->summary = wfMessage( 'newsectionsummary' ) + ->rawParams( $cleanSummary )->inContentLanguage()->text(); } } @@ -1261,10 +1278,7 @@ class EditPage { } else { # Article exists. Check for edit conflict. - - $this->mArticle->clear(); # Force reload of dates, etc. $timestamp = $this->mArticle->getTimestamp(); - wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); if ( $timestamp != $this->edittime ) { @@ -1279,15 +1293,15 @@ class EditPage { } else { // New comment; suppress conflict. $this->isConflict = false; - wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); + wfDebug( __METHOD__ . ": conflict suppressed; new section\n" ); } - } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) { + } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER, $this->mTitle->getArticleID(), $wgUser->getId(), $this->edittime ) ) { # Suppress edit conflict with self, except for section edits where merging is required. wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); $this->isConflict = false; } } - + // If sectiontitle is set, use it, otherwise use the summary as the section title (for // backwards compatibility with old forms/bots). if ( $this->sectiontitle !== '' ) { @@ -1295,7 +1309,7 @@ class EditPage { } else { $sectionTitle = $this->summary; } - + if ( $this->isConflict ) { wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" ); $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime ); @@ -1385,14 +1399,16 @@ class EditPage { // passed. if ( $this->summary === '' ) { $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + $this->summary = wfMessage( 'newsectionsummary' ) + ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); } } elseif ( $this->summary !== '' ) { $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); # This is a new section, so create a link to the new section # in the revision summary. $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + $this->summary = wfMessage( 'newsectionsummary' ) + ->rawParams( $cleanSummary )->inContentLanguage()->text(); } } elseif ( $this->section != '' ) { # Try to get a section anchor from the section source, redirect to edited section if header found @@ -1440,8 +1456,17 @@ class EditPage { wfProfileOut( __METHOD__ ); return $status; } else { - $this->isConflict = true; - $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares + // Failure from doEdit() + // Show the edit conflict page for certain recognized errors from doEdit(), + // but don't show it for errors from extension hooks + $errors = $doEditStatus->getErrorsArray(); + if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict', + 'edit-already-exists' ) ) ) + { + $this->isConflict = true; + // Destroys data doEdit() put in $status->value but who cares + $doEditStatus->value = self::AS_END; + } wfProfileOut( __METHOD__ ); return $doEditStatus; } @@ -1452,56 +1477,27 @@ class EditPage { */ protected function commitWatch() { global $wgUser; - if ( $this->watchthis xor $this->mTitle->userIsWatching() ) { + if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle ) ) { $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); + $dbw->begin( __METHOD__ ); if ( $this->watchthis ) { WatchAction::doWatch( $this->mTitle, $wgUser ); } else { WatchAction::doUnwatch( $this->mTitle, $wgUser ); } - $dbw->commit(); + $dbw->commit( __METHOD__ ); } } /** - * Check if no edits were made by other users since - * the time a user started editing the page. Limit to - * 50 revisions for the sake of performance. - * - * @param $id int - * @param $edittime string - * - * @return bool - */ - protected function userWasLastToEdit( $id, $edittime ) { - if( !$id ) return false; - $dbw = wfGetDB( DB_MASTER ); - $res = $dbw->select( 'revision', - 'rev_user', - array( - 'rev_page' => $this->mTitle->getArticleId(), - 'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) ) - ), - __METHOD__, - array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) ); - foreach ( $res as $row ) { - if( $row->rev_user != $id ) { - return false; - } - } - return true; - } - - /** * @private * @todo document * - * @parma $editText string + * @param $editText string * * @return bool */ - function mergeChangesInto( &$editText ){ + function mergeChangesInto( &$editText ) { wfProfileIn( __METHOD__ ); $db = wfGetDB( DB_MASTER ); @@ -1552,7 +1548,7 @@ class EditPage { * * @param $text string * - * @return string|false matching string or false + * @return string|bool matching string or false */ public static function matchSpamRegex( $text ) { global $wgSpamRegex; @@ -1564,9 +1560,9 @@ class EditPage { /** * Check given input text against $wgSpamRegex, and return the text of the first match. * - * @parma $text string + * @param $text string * - * @return string|false matching string or false + * @return string|bool matching string or false */ public static function matchSummarySpamRegex( $text ) { global $wgSummarySpamRegex; @@ -1580,9 +1576,9 @@ class EditPage { * @return bool|string */ protected static function matchSpamRegexInternal( $text, $regexes ) { - foreach( $regexes as $regex ) { + foreach ( $regexes as $regex ) { $matches = array(); - if( preg_match( $regex, $text, $matches ) ) { + if ( preg_match( $regex, $text, $matches ) ) { return $matches[0]; } } @@ -1595,7 +1591,7 @@ class EditPage { $wgOut->addModules( 'mediawiki.action.edit' ); if ( $wgUser->getOption( 'uselivepreview', false ) ) { - $wgOut->addModules( 'mediawiki.legacy.preview' ); + $wgOut->addModules( 'mediawiki.action.edit.preview' ); } // Bug #19334: textarea jumps when editing articles in IE8 $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' ); @@ -1605,21 +1601,21 @@ class EditPage { # Enabled article-related sidebar, toplinks, etc. $wgOut->setArticleRelated( true ); + $contextTitle = $this->getContextTitle(); if ( $this->isConflict ) { - $wgOut->setPageTitle( wfMessage( 'editconflict', $this->getContextTitle()->getPrefixedText() ) ); - } elseif ( $this->section != '' ) { + $msg = 'editconflict'; + } elseif ( $contextTitle->exists() && $this->section != '' ) { $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; - $wgOut->setPageTitle( wfMessage( $msg, $this->getContextTitle()->getPrefixedText() ) ); } else { - # Use the title defined by DISPLAYTITLE magic word when present - if ( isset( $this->mParserOutput ) - && ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) { - $title = $dt; - } else { - $title = $this->getContextTitle()->getPrefixedText(); - } - $wgOut->setPageTitle( wfMessage( 'editing', $title ) ); + $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ? + 'editing' : 'creating'; + } + # Use the title defined by DISPLAYTITLE magic word when present + $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false; + if ( $displayTitle === false ) { + $displayTitle = $contextTitle->getPrefixedText(); } + $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) ); } /** @@ -1636,6 +1632,24 @@ class EditPage { if ( $namespace == NS_MEDIAWIKI ) { # Show a warning if editing an interface message $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' ); + } else if( $namespace == NS_FILE ) { + # Show a hint to shared repo + $file = wfFindFile( $this->mTitle ); + if( $file && !$file->isLocal() ) { + $descUrl = $file->getDescriptionUrl(); + # there must be a description url to show a hint to shared repo + if( $descUrl ) { + if( !$this->mTitle->exists() ) { + $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", array ( + 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl + ) ); + } else { + $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", array( + 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl + ) ); + } + } + } } # Show a warning message when someone creates/edits a user (talk) page but the user does not exist @@ -1645,7 +1659,7 @@ class EditPage { $username = $parts[0]; $user = User::newFromName( $username, false /* allow IP users*/ ); $ip = User::isIP( $username ); - if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist + if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>", array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) ); } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked @@ -1679,7 +1693,7 @@ class EditPage { '', array( 'lim' => 10, 'conds' => array( "log_action != 'revision'" ), 'showIfEmpty' => false, - 'msgKey' => array( 'recreate-moveddeleted-warn') ) + 'msgKey' => array( 'recreate-moveddeleted-warn' ) ) ); } } @@ -1715,16 +1729,16 @@ class EditPage { wfProfileIn( __METHOD__ ); - #need to parse the preview early so that we know which templates are used, - #otherwise users with "show preview after edit box" will get a blank list - #we parse this near the beginning so that setHeaders can do the title - #setting work instead of leaving it in getPreviewText + # need to parse the preview early so that we know which templates are used, + # otherwise users with "show preview after edit box" will get a blank list + # we parse this near the beginning so that setHeaders can do the title + # setting work instead of leaving it in getPreviewText $previewOutput = ''; if ( $this->formtype == 'preview' ) { $previewOutput = $this->getPreviewText(); } - wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ); + wfRunHooks( 'EditPage::showEditForm:initial', array( &$this, &$wgOut ) ); $this->setHeaders(); @@ -1753,7 +1767,7 @@ class EditPage { } } - $wgOut->addHTML( Html::openElement( 'form', array( 'id' => 'editform', 'name' => 'editform', + $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID, 'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ), 'enctype' => 'multipart/form-data' ) ) ); @@ -1777,14 +1791,19 @@ class EditPage { : 'confirmrecreate'; $wgOut->addHTML( '<div class="mw-confirm-recreate">' . - wfMsgExt( $key, 'parseinline', $username, "<nowiki>$comment</nowiki>" ) . - Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, + wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() . + Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false, array( 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) ) . '</div>' ); } + # When the summary is hidden, also hide them on preview/show changes + if( $this->nosummary ) { + $wgOut->addHTML( Html::hidden( 'nosummary', true ) ); + } + # If a blank edit summary was previously provided, and the appropriate # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the # user being bounced back more than once in the event that a summary @@ -1796,6 +1815,17 @@ class EditPage { $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) ); } + if ( $this->undidRev ) { + $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) ); + } + + if ( $this->hasPresetSummary ) { + // If a summary has been preset using &summary= we dont want to prompt for + // a different summary. Only prompt for a summary if the summary is blanked. + // (Bug 17416) + $this->autoSumm = md5( '' ); + } + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) ); @@ -1827,10 +1857,6 @@ class EditPage { $wgOut->addHTML( $this->editFormTextAfterContent ); - $wgOut->addWikiText( $this->getCopywarn() ); - - $wgOut->addHTML( $this->editFormTextAfterWarn ); - $this->showStandardInputs(); $this->showFormAfterText(); @@ -1870,7 +1896,7 @@ class EditPage { preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ); if ( !empty( $matches[2] ) ) { global $wgParser; - return $wgParser->stripSectionName(trim($matches[2])); + return $wgParser->stripSectionName( trim( $matches[2] ) ); } else { return false; } @@ -1884,8 +1910,8 @@ class EditPage { } # Optional notices on a per-namespace and per-page basis - $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); - $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage(); + $editnotice_ns = 'editnotice-' . $this->mTitle->getNamespace(); + $editnotice_ns_message = wfMessage( $editnotice_ns ); if ( $editnotice_ns_message->exists() ) { $wgOut->addWikiText( $editnotice_ns_message->plain() ); } @@ -1893,16 +1919,16 @@ class EditPage { $parts = explode( '/', $this->mTitle->getDBkey() ); $editnotice_base = $editnotice_ns; while ( count( $parts ) > 0 ) { - $editnotice_base .= '-'.array_shift( $parts ); - $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage(); + $editnotice_base .= '-' . array_shift( $parts ); + $editnotice_base_msg = wfMessage( $editnotice_base ); if ( $editnotice_base_msg->exists() ) { - $wgOut->addWikiText( $editnotice_base_msg->plain() ); + $wgOut->addWikiText( $editnotice_base_msg->plain() ); } } } else { # Even if there are no subpages in namespace, we still don't want / in MW ns. $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->mTitle->getDBkey() ); - $editnoticeMsg = wfMessage( $editnoticeText )->inContentLanguage(); + $editnoticeMsg = wfMessage( $editnoticeText ); if ( $editnoticeMsg->exists() ) { $wgOut->addWikiText( $editnoticeMsg->plain() ); } @@ -1968,8 +1994,7 @@ class EditPage { // Something went wrong $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n", - array( 'missing-article', $this->mTitle->getPrefixedText(), - wfMsgNoTrans( 'missingarticle-rev', $this->oldid ) ) ); + array( 'missing-revision', $this->oldid ) ); } } } @@ -2010,12 +2035,12 @@ class EditPage { } if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? - list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources(); + list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources(); $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n"; $cascadeSourcesCount = count( $cascadeSources ); if ( $cascadeSourcesCount > 0 ) { # Explain, and list the titles responsible - foreach( $cascadeSources as $page ) { + foreach ( $cascadeSources as $page ) { $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; } } @@ -2024,7 +2049,7 @@ class EditPage { } if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '', - array( 'lim' => 1, + array( 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => array( 'titleprotectedwarning' ), 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ) ); @@ -2038,14 +2063,17 @@ class EditPage { $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>", array( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ) ); } else { - if( !wfMessage('longpage-hint')->isDisabled() ) { + if ( !wfMessage( 'longpage-hint' )->isDisabled() ) { $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>", array( 'longpage-hint', $wgLang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) ) ); } } + # Add header copyright warning + $this->showHeaderCopyrightWarning(); } + /** * Standard summary input and label (wgSummary), abstracted so EditPage * subclasses may reorganize the form. @@ -2060,9 +2088,9 @@ class EditPage { * * @return array An array in the format array( $label, $input ) */ - function getSummaryInput($summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null) { - //Note: the maxlength is overriden in JS to 250 and to make it use UTF-8 bytes, not characters. - $inputAttrs = ( is_array($inputAttrs) ? $inputAttrs : array() ) + array( + function getSummaryInput( $summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null ) { + // Note: the maxlength is overriden in JS to 255 and to make it use UTF-8 bytes, not characters. + $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array( 'id' => 'wpSummary', 'maxlength' => '200', 'tabindex' => '1', @@ -2070,7 +2098,7 @@ class EditPage { 'spellcheck' => 'true', ) + Linker::tooltipAndAccesskeyAttribs( 'summary' ); - $spanLabelAttrs = ( is_array($spanLabelAttrs) ? $spanLabelAttrs : array() ) + array( + $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : array() ) + array( 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary', 'id' => "wpSummaryLabel" ); @@ -2107,9 +2135,9 @@ class EditPage { } } $summary = $wgContLang->recodeForEdit( $summary ); - $labelText = wfMsgExt( $isSubjectPreview ? 'subject' : 'summary', 'parseinline' ); - list($label, $input) = $this->getSummaryInput($summary, $labelText, array( 'class' => $summaryClass ), array()); - $wgOut->addHTML("{$label} {$input}"); + $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse(); + list( $label, $input ) = $this->getSummaryInput( $summary, $labelText, array( 'class' => $summaryClass ), array() ); + $wgOut->addHTML( "{$label} {$input}" ); } /** @@ -2126,11 +2154,12 @@ class EditPage { global $wgParser; if ( $isSubjectPreview ) - $summary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $summary ) ); + $summary = wfMessage( 'newsectionsummary', $wgParser->stripSectionName( $summary ) ) + ->inContentLanguage()->text(); $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview'; - $summary = wfMsgExt( $message, 'parseinline' ) . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview ); + $summary = wfMessage( $message )->parse() . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview ); return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary ); } @@ -2146,7 +2175,7 @@ class EditPage { HTML ); if ( !$this->checkUnicodeCompliantBrowser() ) - $wgOut->addHTML(Html::hidden( 'safemode', '1' )); + $wgOut->addHTML( Html::hidden( 'safemode', '1' ) ); } protected function showFormAfterText() { @@ -2183,7 +2212,7 @@ HTML * The $textoverride method can be used by subclasses overriding showContentForm * to pass back to this method. * - * @param $customAttribs An array of html attributes to use in the textarea + * @param $customAttribs array of html attributes to use in the textarea * @param $textoverride String: optional text to override $this->textarea1 with */ protected function showTextbox1( $customAttribs = null, $textoverride = null ) { @@ -2230,7 +2259,7 @@ HTML global $wgOut, $wgUser; $wikitext = $this->safeUnicodeOutput( $content ); - if ( strval($wikitext) !== '' ) { + if ( strval( $wikitext ) !== '' ) { // Ensure there's a newline at the end, otherwise adding lines // is awkward. // But don't add a newline if the ext is empty, or Firefox in XHTML @@ -2272,7 +2301,7 @@ HTML $wgOut->addHTML( '</div>' ); - if ( $this->formtype == 'diff') { + if ( $this->formtype == 'diff' ) { $this->showDiff(); } } @@ -2285,12 +2314,12 @@ HTML */ protected function showPreview( $text ) { global $wgOut; - if ( $this->mTitle->getNamespace() == NS_CATEGORY) { + if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { $this->mArticle->openShowCategory(); } # This hook seems slightly odd here, but makes things more # consistent for extensions. - wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) ); + wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$text ) ); $wgOut->addHTML( $text ); if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { $this->mArticle->closeShowCategory(); @@ -2307,7 +2336,16 @@ HTML function showDiff() { global $wgUser, $wgContLang, $wgParser, $wgOut; - $oldtext = $this->mArticle->getRawText(); + $oldtitlemsg = 'currentrev'; + # if message does not exist, show diff against the preloaded default + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) { + $oldtext = $this->mTitle->getDefaultMessageText(); + if( $oldtext !== false ) { + $oldtitlemsg = 'defaultmessagetext'; + } + } else { + $oldtext = $this->mArticle->getRawText(); + } $newtext = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); @@ -2317,8 +2355,8 @@ HTML $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts ); if ( $oldtext !== false || $newtext != '' ) { - $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) ); - $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) ); + $oldtitle = wfMessage( $oldtitlemsg )->parse(); + $newtitle = wfMessage( 'yourtext' )->parse(); $de = new DifferenceEngine( $this->mArticle->getContext() ); $de->setText( $oldtext, $newtext ); @@ -2332,6 +2370,18 @@ HTML } /** + * Show the header copyright warning. + */ + protected function showHeaderCopyrightWarning() { + $msg = 'editpage-head-copy-warn'; + if ( !wfMessage( $msg )->isDisabled() ) { + global $wgOut; + $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>", + 'editpage-head-copy-warn' ); + } + } + + /** * Give a chance for site and per-namespace customizations of * terms of service summary link that might exist separately * from the copyright notice. @@ -2342,7 +2392,7 @@ HTML protected function showTosSummary() { $msg = 'editpage-tos-summary'; wfRunHooks( 'EditPageTosSummary', array( $this->mTitle, &$msg ) ); - if( !wfMessage( $msg )->isDisabled() ) { + if ( !wfMessage( $msg )->isDisabled() ) { global $wgOut; $wgOut->addHTML( '<div class="mw-tos-summary">' ); $wgOut->addWikiMsg( $msg ); @@ -2357,21 +2407,30 @@ HTML '</div>' ); } + /** + * Get the copyright warning + * + * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility + */ protected function getCopywarn() { + return self::getCopyrightWarning( $this->mTitle ); + } + + public static function getCopyrightWarning( $title ) { global $wgRightsText; if ( $wgRightsText ) { $copywarnMsg = array( 'copyrightwarning', - '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', + '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]', $wgRightsText ); } else { $copywarnMsg = array( 'copyrightwarning2', - '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' ); + '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ); } // Allow for site and per-namespace customization of contribution/copyright notice. - wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) ); + wfRunHooks( 'EditPageCopyrightWarning', array( $title, &$copywarnMsg ) ); return "<div id=\"editpage-copywarn\">\n" . - call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n</div>"; + call_user_func_array( 'wfMessage', $copywarnMsg )->plain() . "\n</div>"; } protected function showStandardInputs( &$tabindex = 2 ) { @@ -2386,18 +2445,24 @@ HTML $checkboxes = $this->getCheckboxes( $tabindex, array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" ); + + // Show copyright warning. + $wgOut->addWikiText( $this->getCopywarn() ); + $wgOut->addHTML( $this->editFormTextAfterWarn ); + $wgOut->addHTML( "<div class='editButtons'>\n" ); $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" ); $cancel = $this->getCancelLink(); if ( $cancel !== '' ) { - $cancel .= wfMsgExt( 'pipe-separator' , 'escapenoentities' ); - } - $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) ); - $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. - htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. - htmlspecialchars( wfMsg( 'newwindow' ) ); - $wgOut->addHTML( " <span class='editHelp'>{$cancel}{$edithelp}</span>\n" ); + $cancel .= wfMessage( 'pipe-separator' )->text(); + } + $edithelpurl = Skin::makeInternalOrExternalUrl( wfMessage( 'edithelppage' )->inContentLanguage()->text() ); + $edithelp = '<a target="helpwindow" href="' . $edithelpurl . '">' . + wfMessage( 'edithelp' )->escaped() . '</a> ' . + wfMessage( 'newwindow' )->parse(); + $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" ); + $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" ); $wgOut->addHTML( "</div><!-- editButtons -->\n</div><!-- editOptions -->\n" ); } @@ -2413,7 +2478,10 @@ HTML $de = new DifferenceEngine( $this->mArticle->getContext() ); $de->setText( $this->textbox2, $this->textbox1 ); - $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) ); + $de->showDiff( + wfMessage( 'yourtext' )->parse(), + wfMessage( 'storedversion' )->text() + ); $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" ); $this->showTextbox2(); @@ -2431,7 +2499,7 @@ HTML return Linker::linkKnown( $this->getContextTitle(), - wfMsgExt( 'cancel', array( 'parseinline' ) ), + wfMessage( 'cancel' )->parse(), array( 'id' => 'mw-editform-cancel' ), $cancelParams ); @@ -2499,11 +2567,11 @@ HTML array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); // Quick paranoid permission checks... - if( is_object( $data ) ) { - if( $data->log_deleted & LogPage::DELETED_USER ) - $data->user_name = wfMsgHtml( 'rev-deleted-user' ); - if( $data->log_deleted & LogPage::DELETED_COMMENT ) - $data->log_comment = wfMsgHtml( 'rev-deleted-comment' ); + if ( is_object( $data ) ) { + if ( $data->log_deleted & LogPage::DELETED_USER ) + $data->user_name = wfMessage( 'rev-deleted-user' )->escaped(); + if ( $data->log_deleted & LogPage::DELETED_COMMENT ) + $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped(); } return $data; } @@ -2513,7 +2581,7 @@ HTML * @return string */ function getPreviewText() { - global $wgOut, $wgUser, $wgParser, $wgRawHtml; + global $wgOut, $wgUser, $wgParser, $wgRawHtml, $wgLang; wfProfileIn( __METHOD__ ); @@ -2526,7 +2594,7 @@ HTML // string, which happens when you initially edit // a category page, due to automatic preview-on-open. $parsedNote = $wgOut->parse( "<div class='previewnote'>" . - wfMsg( 'session_fail_preview_html' ) . "</div>", true, /* interface */true ); + wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true ); } wfProfileOut( __METHOD__ ); return $parsedNote; @@ -2534,29 +2602,28 @@ HTML if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { - $note = wfMsg( 'token_suffix_mismatch' ); + $note = wfMessage( 'token_suffix_mismatch' )->plain(); } else { - $note = wfMsg( 'session_fail_preview' ); + $note = wfMessage( 'session_fail_preview' )->plain(); } } elseif ( $this->incompleteForm ) { - $note = wfMsg( 'edit_form_incomplete' ); + $note = wfMessage( 'edit_form_incomplete' )->plain(); } else { - $note = wfMsg( 'previewnote' ); + $note = wfMessage( 'previewnote' )->plain() . + ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]'; } - $parserOptions = ParserOptions::newFromUser( $wgUser ); + $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() ); + $parserOptions->setEditSection( false ); - $parserOptions->setTidy( true ); $parserOptions->setIsPreview( true ); - $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' ); + $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' ); # don't parse non-wikitext pages, show message about preview - # XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago? - - if ( $this->isCssJsSubpage || !$this->mTitle->isWikitextPage() ) { - if( $this->mTitle->isCssJsSubpage() ) { + if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) { + if ( $this->mTitle->isCssJsSubpage() ) { $level = 'user'; - } elseif( $this->mTitle->isCssOrJsPage() ) { + } elseif ( $this->mTitle->isCssOrJsPage() ) { $level = 'site'; } else { $level = false; @@ -2564,64 +2631,66 @@ HTML # Used messages to make sure grep find them: # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview - if( $level ) { - if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) { - $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>"; - $class = "mw-code mw-css"; - } elseif (preg_match( "/\\.js$/", $this->mTitle->getText() ) ) { - $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>"; - $class = "mw-code mw-js"; + $class = 'mw-code'; + if ( $level ) { + if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) { + $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMessage( "{$level}csspreview" )->text() . "\n</div>"; + $class .= " mw-css"; + } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) { + $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMessage( "{$level}jspreview" )->text() . "\n</div>"; + $class .= " mw-js"; } else { throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' ); } + $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); + $previewHTML = $parserOutput->getText(); + } else { + $previewHTML = ''; } - $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); - $previewHTML = $parserOutput->mText; $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n"; } else { - $rt = Title::newFromRedirectArray( $this->textbox1 ); - if ( $rt ) { - $previewHTML = $this->mArticle->viewRedirect( $rt, false ); - } else { - $toparse = $this->textbox1; - - # If we're adding a comment, we need to show the - # summary as the headline - if ( $this->section == "new" && $this->summary != "" ) { - $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse; - } + $toparse = $this->textbox1; - wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); + # If we're adding a comment, we need to show the + # summary as the headline + if ( $this->section == "new" && $this->summary != "" ) { + $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse; + } - $parserOptions->enableLimitReport(); + wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); - $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions ); - $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions ); + $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions ); + $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions ); + $rt = Title::newFromRedirectArray( $this->textbox1 ); + if ( $rt ) { + $previewHTML = $this->mArticle->viewRedirect( $rt, false ); + } else { $previewHTML = $parserOutput->getText(); - $this->mParserOutput = $parserOutput; - $wgOut->addParserOutputNoText( $parserOutput ); + } - if ( count( $parserOutput->getWarnings() ) ) { - $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); - } + $this->mParserOutput = $parserOutput; + $wgOut->addParserOutputNoText( $parserOutput ); + + if ( count( $parserOutput->getWarnings() ) ) { + $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); } } - if( $this->isConflict ) { - $conflict = '<h2 id="mw-previewconflict">' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; + if ( $this->isConflict ) { + $conflict = '<h2 id="mw-previewconflict">' . wfMessage( 'previewconflict' )->escaped() . "</h2>\n"; } else { $conflict = '<hr />'; } $previewhead = "<div class='previewnote'>\n" . - '<h2 id="mw-previewheader">' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>" . + '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" . $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n"; $pageLang = $this->mTitle->getPageLanguage(); $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), - 'class' => 'mw-content-'.$pageLang->getDir() ); + 'class' => 'mw-content-' . $pageLang->getDir() ); $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML ); wfProfileOut( __METHOD__ ); @@ -2637,9 +2706,9 @@ HTML if ( !isset( $this->mParserOutput ) ) { return $templates; } - foreach( $this->mParserOutput->getTemplates() as $ns => $template) { - foreach( array_keys( $template ) as $dbk ) { - $templates[] = Title::makeTitle($ns, $dbk); + foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) { + foreach ( array_keys( $template ) as $dbk ) { + $templates[] = Title::makeTitle( $ns, $dbk ); } } return $templates; @@ -2680,8 +2749,8 @@ HTML 'id' => 'mw-editbutton-bold', 'open' => '\'\'\'', 'close' => '\'\'\'', - 'sample' => wfMsg( 'bold_sample' ), - 'tip' => wfMsg( 'bold_tip' ), + 'sample' => wfMessage( 'bold_sample' )->text(), + 'tip' => wfMessage( 'bold_tip' )->text(), 'key' => 'B' ), array( @@ -2689,8 +2758,8 @@ HTML 'id' => 'mw-editbutton-italic', 'open' => '\'\'', 'close' => '\'\'', - 'sample' => wfMsg( 'italic_sample' ), - 'tip' => wfMsg( 'italic_tip' ), + 'sample' => wfMessage( 'italic_sample' )->text(), + 'tip' => wfMessage( 'italic_tip' )->text(), 'key' => 'I' ), array( @@ -2698,8 +2767,8 @@ HTML 'id' => 'mw-editbutton-link', 'open' => '[[', 'close' => ']]', - 'sample' => wfMsg( 'link_sample' ), - 'tip' => wfMsg( 'link_tip' ), + 'sample' => wfMessage( 'link_sample' )->text(), + 'tip' => wfMessage( 'link_tip' )->text(), 'key' => 'L' ), array( @@ -2707,8 +2776,8 @@ HTML 'id' => 'mw-editbutton-extlink', 'open' => '[', 'close' => ']', - 'sample' => wfMsg( 'extlink_sample' ), - 'tip' => wfMsg( 'extlink_tip' ), + 'sample' => wfMessage( 'extlink_sample' )->text(), + 'tip' => wfMessage( 'extlink_tip' )->text(), 'key' => 'X' ), array( @@ -2716,8 +2785,8 @@ HTML 'id' => 'mw-editbutton-headline', 'open' => "\n== ", 'close' => " ==\n", - 'sample' => wfMsg( 'headline_sample' ), - 'tip' => wfMsg( 'headline_tip' ), + 'sample' => wfMessage( 'headline_sample' )->text(), + 'tip' => wfMessage( 'headline_tip' )->text(), 'key' => 'H' ), $imagesAvailable ? array( @@ -2725,8 +2794,8 @@ HTML 'id' => 'mw-editbutton-image', 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':', 'close' => ']]', - 'sample' => wfMsg( 'image_sample' ), - 'tip' => wfMsg( 'image_tip' ), + 'sample' => wfMessage( 'image_sample' )->text(), + 'tip' => wfMessage( 'image_tip' )->text(), 'key' => 'D', ) : false, $imagesAvailable ? array( @@ -2734,17 +2803,17 @@ HTML 'id' => 'mw-editbutton-media', 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':', 'close' => ']]', - 'sample' => wfMsg( 'media_sample' ), - 'tip' => wfMsg( 'media_tip' ), + 'sample' => wfMessage( 'media_sample' )->text(), + 'tip' => wfMessage( 'media_tip' )->text(), 'key' => 'M' ) : false, - $wgUseTeX ? array( + $wgUseTeX ? array( 'image' => $wgLang->getImageFile( 'button-math' ), 'id' => 'mw-editbutton-math', 'open' => "<math>", 'close' => "</math>", - 'sample' => wfMsg( 'math_sample' ), - 'tip' => wfMsg( 'math_tip' ), + 'sample' => wfMessage( 'math_sample' )->text(), + 'tip' => wfMessage( 'math_tip' )->text(), 'key' => 'C' ) : false, array( @@ -2752,8 +2821,8 @@ HTML 'id' => 'mw-editbutton-nowiki', 'open' => "<nowiki>", 'close' => "</nowiki>", - 'sample' => wfMsg( 'nowiki_sample' ), - 'tip' => wfMsg( 'nowiki_tip' ), + 'sample' => wfMessage( 'nowiki_sample' )->text(), + 'tip' => wfMessage( 'nowiki_tip' )->text(), 'key' => 'N' ), array( @@ -2762,7 +2831,7 @@ HTML 'open' => '--~~~~', 'close' => '', 'sample' => '', - 'tip' => wfMsg( 'sig_tip' ), + 'tip' => wfMessage( 'sig_tip' )->text(), 'key' => 'Y' ), array( @@ -2771,7 +2840,7 @@ HTML 'open' => "\n----\n", 'close' => '', 'sample' => '', - 'tip' => wfMsg( 'hr_tip' ), + 'tip' => wfMessage( 'hr_tip' )->text(), 'key' => 'R' ) ); @@ -2797,7 +2866,7 @@ HTML $script .= Xml::encodeJsCall( 'mw.toolbar.addButton', $params ); } - + // This used to be called on DOMReady from mediawiki.action.edit, which // ended up causing race conditions with the setup code above. $script .= "\n" . @@ -2818,7 +2887,7 @@ HTML * Returns an array of html code of the following checkboxes: * minor and watch * - * @param $tabindex Current tabindex + * @param $tabindex int Current tabindex * @param $checked Array of checkbox => bool, where bool indicates the checked * status of the checkbox * @@ -2832,11 +2901,11 @@ HTML // don't show the minor edit checkbox if it's a new page or section if ( !$this->isNew ) { $checkboxes['minor'] = ''; - $minorLabel = wfMsgExt( 'minoredit', array( 'parseinline' ) ); + $minorLabel = wfMessage( 'minoredit' )->parse(); if ( $wgUser->isAllowed( 'minoredit' ) ) { $attribs = array( 'tabindex' => ++$tabindex, - 'accesskey' => wfMsg( 'accesskey-minoredit' ), + 'accesskey' => wfMessage( 'accesskey-minoredit' )->text(), 'id' => 'wpMinoredit', ); $checkboxes['minor'] = @@ -2847,12 +2916,12 @@ HTML } } - $watchLabel = wfMsgExt( 'watchthis', array( 'parseinline' ) ); + $watchLabel = wfMessage( 'watchthis' )->parse(); $checkboxes['watch'] = ''; if ( $wgUser->isLoggedIn() ) { $attribs = array( 'tabindex' => ++$tabindex, - 'accesskey' => wfMsg( 'accesskey-watch' ), + 'accesskey' => wfMessage( 'accesskey-watch' )->text(), 'id' => 'wpWatchthis', ); $checkboxes['watch'] = @@ -2869,7 +2938,7 @@ HTML * Returns an array of html code of the following buttons: * save, diff, preview and live * - * @param $tabindex Current tabindex + * @param $tabindex int Current tabindex * * @return array */ @@ -2881,11 +2950,11 @@ HTML 'name' => 'wpSave', 'type' => 'submit', 'tabindex' => ++$tabindex, - 'value' => wfMsg( 'savearticle' ), - 'accesskey' => wfMsg( 'accesskey-save' ), - 'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']', + 'value' => wfMessage( 'savearticle' )->text(), + 'accesskey' => wfMessage( 'accesskey-save' )->text(), + 'title' => wfMessage( 'tooltip-save' )->text() . ' [' . wfMessage( 'accesskey-save' )->text() . ']', ); - $buttons['save'] = Xml::element('input', $temp, ''); + $buttons['save'] = Xml::element( 'input', $temp, '' ); ++$tabindex; // use the same for preview and live preview $temp = array( @@ -2893,9 +2962,9 @@ HTML 'name' => 'wpPreview', 'type' => 'submit', 'tabindex' => $tabindex, - 'value' => wfMsg( 'showpreview' ), - 'accesskey' => wfMsg( 'accesskey-preview' ), - 'title' => wfMsg( 'tooltip-preview' ) . ' [' . wfMsg( 'accesskey-preview' ) . ']', + 'value' => wfMessage( 'showpreview' )->text(), + 'accesskey' => wfMessage( 'accesskey-preview' )->text(), + 'title' => wfMessage( 'tooltip-preview' )->text() . ' [' . wfMessage( 'accesskey-preview' )->text() . ']', ); $buttons['preview'] = Xml::element( 'input', $temp, '' ); $buttons['live'] = ''; @@ -2905,9 +2974,9 @@ HTML 'name' => 'wpDiff', 'type' => 'submit', 'tabindex' => ++$tabindex, - 'value' => wfMsg( 'showdiff' ), - 'accesskey' => wfMsg( 'accesskey-diff' ), - 'title' => wfMsg( 'tooltip-diff' ) . ' [' . wfMsg( 'accesskey-diff' ) . ']', + 'value' => wfMessage( 'showdiff' )->text(), + 'accesskey' => wfMessage( 'accesskey-diff' )->text(), + 'title' => wfMessage( 'tooltip-diff' )->text() . ' [' . wfMessage( 'accesskey-diff' )->text() . ']', ); $buttons['diff'] = Xml::element( 'input', $temp, '' ); @@ -2923,8 +2992,8 @@ HTML * failure, etc). * * @todo This doesn't include category or interlanguage links. - * Would need to enhance it a bit, <s>maybe wrap them in XML - * or something...</s> that might also require more skin + * Would need to enhance it a bit, "<s>maybe wrap them in XML + * or something...</s>" that might also require more skin * initialization, so check whether that's a problem. */ function livePreview() { @@ -2954,7 +3023,7 @@ HTML wfDeprecated( __METHOD__, '1.19' ); global $wgUser; - throw new UserBlockedError( $wgUser->mBlock ); + throw new UserBlockedError( $wgUser->getBlock() ); } /** @@ -2988,7 +3057,7 @@ HTML $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) ); - $res = wfMsgExt( 'nosuchsectiontext', 'parse', $this->section ); + $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock(); wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) ); $wgOut->addHTML( $res ); @@ -2998,12 +3067,12 @@ HTML /** * Produce the stock "your edit contains spam" page * - * @param $match Text which triggered one or more filters + * @param $match string Text which triggered one or more filters * @deprecated since 1.17 Use method spamPageWithContent() instead */ static function spamPage( $match = false ) { wfDeprecated( __METHOD__, '1.17' ); - + global $wgOut, $wgTitle; $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) ); @@ -3021,12 +3090,15 @@ HTML /** * Show "your edit contains spam" page with your diff and text * - * @param $match Text which triggered one or more filters + * @param $match string|Array|bool Text (or array of texts) which triggered one or more filters */ public function spamPageWithContent( $match = false ) { - global $wgOut; + global $wgOut, $wgLang; $this->textbox2 = $this->textbox1; + if( is_array( $match ) ){ + $match = $wgLang->listToText( $match ); + } $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) ); $wgOut->addHTML( '<div id="spamprotected">' ); @@ -3037,9 +3109,7 @@ HTML $wgOut->addHTML( '</div>' ); $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" ); - $de = new DifferenceEngine( $this->mArticle->getContext() ); - $de->setText( $this->getCurrentText(), $this->textbox2 ); - $de->showDiff( wfMsg( "storedversion" ), wfMsgExt( 'yourtext', 'parseinline' ) ); + $this->showDiff(); $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" ); $this->showTextbox2(); @@ -3066,14 +3136,16 @@ HTML * @private */ function checkUnicodeCompliantBrowser() { - global $wgBrowserBlackList; - if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) { + global $wgBrowserBlackList, $wgRequest; + + $currentbrowser = $wgRequest->getHeader( 'User-Agent' ); + if ( $currentbrowser === false ) { // No User-Agent header sent? Trust it by default... return true; } - $currentbrowser = $_SERVER["HTTP_USER_AGENT"]; + foreach ( $wgBrowserBlackList as $browser ) { - if ( preg_match($browser, $currentbrowser) ) { + if ( preg_match( $browser, $currentbrowser ) ) { return false; } } @@ -3144,25 +3216,25 @@ HTML $bytesleft = 0; $result = ""; $working = 0; - for( $i = 0; $i < strlen( $invalue ); $i++ ) { + for ( $i = 0; $i < strlen( $invalue ); $i++ ) { $bytevalue = ord( $invalue[$i] ); - if ( $bytevalue <= 0x7F ) { //0xxx xxxx + if ( $bytevalue <= 0x7F ) { // 0xxx xxxx $result .= chr( $bytevalue ); $bytesleft = 0; - } elseif ( $bytevalue <= 0xBF ) { //10xx xxxx + } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx $working = $working << 6; - $working += ($bytevalue & 0x3F); + $working += ( $bytevalue & 0x3F ); $bytesleft--; if ( $bytesleft <= 0 ) { $result .= "&#x" . strtoupper( dechex( $working ) ) . ";"; } - } elseif ( $bytevalue <= 0xDF ) { //110x xxxx + } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx $working = $bytevalue & 0x1F; $bytesleft = 1; - } elseif ( $bytevalue <= 0xEF ) { //1110 xxxx + } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx $working = $bytevalue & 0x0F; $bytesleft = 2; - } else { //1111 0xxx + } else { // 1111 0xxx $working = $bytevalue & 0x07; $bytesleft = 3; } @@ -3181,20 +3253,20 @@ HTML */ function unmakesafe( $invalue ) { $result = ""; - for( $i = 0; $i < strlen( $invalue ); $i++ ) { - if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i+3] != '0' ) ) { + for ( $i = 0; $i < strlen( $invalue ); $i++ ) { + if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) { $i += 3; $hexstring = ""; do { $hexstring .= $invalue[$i]; $i++; - } while( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) ); + } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) ); // Do some sanity checks. These aren't needed for reversability, // but should help keep the breakage down if the editor // breaks one of the entities whilst editing. - if ( (substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6) ) { - $codepoint = hexdec($hexstring); + if ( ( substr( $invalue, $i, 1 ) == ";" ) and ( strlen( $hexstring ) <= 6 ) ) { + $codepoint = hexdec( $hexstring ); $result .= codepointToUtf8( $codepoint ); } else { $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 ); diff --git a/includes/Exception.php b/includes/Exception.php index 3bd89b6e..714f73e8 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -1,6 +1,21 @@ <?php /** - * Exception class and handler + * Exception class and handler. + * + * 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 */ @@ -15,8 +30,11 @@ * @ingroup Exception */ class MWException extends Exception { + var $logId; + /** - * Should the exception use $wgOut to output the error ? + * Should the exception use $wgOut to output the error? + * * @return bool */ function useOutputPage() { @@ -27,7 +45,8 @@ class MWException extends Exception { } /** - * Can the extension use wfMsg() to get i18n messages ? + * Can the extension use the Message class/wfMessage to get i18n-ed messages? + * * @return bool */ function useMessageCache() { @@ -45,19 +64,19 @@ class MWException extends Exception { /** * Run hook to allow extensions to modify the text of the exception * - * @param $name String: class name of the exception - * @param $args Array: arguments to pass to the callback functions - * @return Mixed: string to output or null if any hook has been called + * @param $name string: class name of the exception + * @param $args array: arguments to pass to the callback functions + * @return string|null string to output or null if any hook has been called */ function runHooks( $name, $args = array() ) { global $wgExceptionHooks; if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) { - return; // Just silently ignore + return null; // Just silently ignore } if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) ) { - return; + return null; } $hooks = $wgExceptionHooks[ $name ]; @@ -74,22 +93,23 @@ class MWException extends Exception { return $result; } } + return null; } /** * Get a message from i18n * - * @param $key String: message name - * @param $fallback String: default message if the message cache can't be + * @param $key string: message name + * @param $fallback string: default message if the message cache can't be * called by the exception * The function also has other parameters that are arguments for the message - * @return String message with arguments replaced + * @return string message with arguments replaced */ function msg( $key, $fallback /*[, params...] */ ) { $args = array_slice( func_get_args(), 2 ); if ( $this->useMessageCache() ) { - return wfMsgNoTrans( $key, $args ); + return wfMessage( $key, $args )->plain(); } else { return wfMsgReplaceArgs( $fallback, $args ); } @@ -100,7 +120,7 @@ class MWException extends Exception { * backtrace to the error, otherwise show a message to ask to set it to true * to show that information. * - * @return String html to output + * @return string html to output */ function getHTML() { global $wgShowExceptionDetails; @@ -110,15 +130,22 @@ class MWException extends Exception { '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . "</p>\n"; } else { - return "<p>Set <b><tt>\$wgShowExceptionDetails = true;</tt></b> " . + return + "<div class=\"errorbox\">" . + '[' . $this->getLogId() . '] ' . + gmdate( 'Y-m-d H:i:s' ) . + ": Fatal exception of type " . get_class( $this ) . "</div>\n" . + "<!-- Set \$wgShowExceptionDetails = true; " . "at the bottom of LocalSettings.php to show detailed " . - "debugging information.</p>"; + "debugging information. -->"; } } /** + * Get the text to display when reporting the error on the command line. * If $wgShowExceptionDetails is true, return a text message with a * backtrace to the error. + * * @return string */ function getText() { @@ -134,22 +161,38 @@ class MWException extends Exception { } /** - * Return titles of this error page - * @return String + * Return the title of the page when reporting this error in a HTTP response. + * + * @return string */ function getPageTitle() { return $this->msg( 'internalerror', "Internal error" ); } /** + * Get a random ID for this error. + * This allows to link the exception to its correspoding log entry when + * $wgShowExceptionDetails is set to false. + * + * @return string + */ + function getLogId() { + if ( $this->logId === null ) { + $this->logId = wfRandomString( 8 ); + } + return $this->logId; + } + + /** * Return the requested URL and point to file and line number from which the - * exception occured + * exception occurred * - * @return String + * @return string */ function getLogMessage() { global $wgRequest; + $id = $this->getLogId(); $file = $this->getFile(); $line = $this->getLine(); $message = $this->getMessage(); @@ -163,10 +206,12 @@ class MWException extends Exception { $url = '[no req]'; } - return "$url Exception from line $line of $file: $message"; + return "[$id] $url Exception from line $line of $file: $message"; } - /** Output the exception report using HTML */ + /** + * Output the exception report using HTML. + */ function reportHTML() { global $wgOut; if ( $this->useOutputPage() ) { @@ -182,13 +227,19 @@ class MWException extends Exception { $wgOut->output(); } else { header( "Content-Type: text/html; charset=utf-8" ); + echo "<!doctype html>\n" . + '<html><head>' . + '<title>' . htmlspecialchars( $this->getPageTitle() ) . '</title>' . + "</head><body>\n"; + $hookResult = $this->runHooks( get_class( $this ) . "Raw" ); if ( $hookResult ) { - die( $hookResult ); + echo $hookResult; + } else { + echo $this->getHTML(); } - echo $this->getHTML(); - die(1); + echo "</body></html>\n"; } } @@ -197,21 +248,35 @@ class MWException extends Exception { * It will be either HTML or plain text based on isCommandLine(). */ function report() { + global $wgLogExceptionBacktrace; $log = $this->getLogMessage(); if ( $log ) { - wfDebugLog( 'exception', $log ); + if ( $wgLogExceptionBacktrace ) { + wfDebugLog( 'exception', $log . "\n" . $this->getTraceAsString() . "\n" ); + } else { + wfDebugLog( 'exception', $log ); + } } - if ( self::isCommandLine() ) { + if ( defined( 'MW_API' ) ) { + // Unhandled API exception, we can't be sure that format printer is alive + header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) ); + wfHttpError(500, 'Internal Server Error', $this->getText() ); + } elseif ( self::isCommandLine() ) { MWExceptionHandler::printError( $this->getText() ); } else { + header( "HTTP/1.1 500 MediaWiki exception" ); + header( "Status: 500 MediaWiki exception", true ); + $this->reportHTML(); } } /** - * @static + * Check whether we are in command line mode or not to report the exception + * in the correct format. + * * @return bool */ static function isCommandLine() { @@ -222,6 +287,8 @@ class MWException extends Exception { /** * Exception class which takes an HTML error message, and does not * produce a backtrace. Replacement for OutputPage::fatalError(). + * + * @since 1.7 * @ingroup Exception */ class FatalError extends MWException { @@ -242,14 +309,20 @@ class FatalError extends MWException { } /** - * An error page which can definitely be safely rendered using the OutputPage + * An error page which can definitely be safely rendered using the OutputPage. + * + * @since 1.7 * @ingroup Exception */ class ErrorPageError extends MWException { public $title, $msg, $params; /** - * Note: these arguments are keys into wfMsg(), not text! + * Note: these arguments are keys into wfMessage(), not text! + * + * @param $title string|Message Message key (string) for page title, or a Message object + * @param $msg string|Message Message key (string) for error text, or a Message object + * @param $params array with parameters to wfMessage() */ function __construct( $title, $msg, $params = null ) { $this->title = $title; @@ -259,14 +332,13 @@ class ErrorPageError extends MWException { if( $msg instanceof Message ){ parent::__construct( $msg ); } else { - parent::__construct( wfMsg( $msg ) ); + parent::__construct( wfMessage( $msg )->text() ); } } function report() { global $wgOut; - $wgOut->showErrorPage( $this->title, $this->msg, $this->params ); $wgOut->output(); } @@ -276,12 +348,14 @@ class ErrorPageError extends MWException { * Show an error page on a badtitle. * Similar to ErrorPage, but emit a 400 HTTP error code to let mobile * browser it is not really a valid content. + * + * @since 1.19 + * @ingroup Exception */ class BadTitleError extends ErrorPageError { - /** - * @param $msg string A message key (default: 'badtitletext') - * @param $params Array parameter to wfMsg() + * @param $msg string|Message A message key (default: 'badtitletext') + * @param $params Array parameter to wfMessage() */ function __construct( $msg = 'badtitletext', $params = null ) { parent::__construct( 'badtitle', $msg, $params ); @@ -305,6 +379,8 @@ class BadTitleError extends ErrorPageError { /** * Show an error when a user tries to do something they do not have the necessary * permissions for. + * + * @since 1.18 * @ingroup Exception */ class PermissionsError extends ErrorPageError { @@ -341,7 +417,9 @@ class PermissionsError extends ErrorPageError { /** * Show an error when the wiki is locked/read-only and the user tries to do - * something that requires write access + * something that requires write access. + * + * @since 1.18 * @ingroup Exception */ class ReadOnlyError extends ErrorPageError { @@ -355,7 +433,9 @@ class ReadOnlyError extends ErrorPageError { } /** - * Show an error when the user hits a rate limit + * Show an error when the user hits a rate limit. + * + * @since 1.18 * @ingroup Exception */ class ThrottledError extends ErrorPageError { @@ -369,12 +449,14 @@ class ThrottledError extends ErrorPageError { public function report(){ global $wgOut; $wgOut->setStatusCode( 503 ); - return parent::report(); + parent::report(); } } /** - * Show an error when the user tries to do something whilst blocked + * Show an error when the user tries to do something whilst blocked. + * + * @since 1.18 * @ingroup Exception */ class UserBlockedError extends ErrorPageError { @@ -391,7 +473,7 @@ class UserBlockedError extends ErrorPageError { $reason = $block->mReason; if( $reason == '' ) { - $reason = wfMsg( 'blockednoreason' ); + $reason = wfMessage( 'blockednoreason' )->text(); } /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked. @@ -416,9 +498,57 @@ class UserBlockedError extends ErrorPageError { } /** + * Shows a generic "user is not logged in" error page. + * + * This is essentially an ErrorPageError exception which by default use the + * 'exception-nologin' as a title and 'exception-nologin-text' for the message. + * @see bug 37627 + * @since 1.20 + * + * @par Example: + * @code + * if( $user->isAnon ) { + * throw new UserNotLoggedIn(); + * } + * @endcode + * + * Please note the parameters are mixed up compared to ErrorPageError, this + * is done to be able to simply specify a reason whitout overriding the default + * title. + * + * @par Example: + * @code + * if( $user->isAnon ) { + * throw new UserNotLoggedIn( 'action-require-loggedin' ); + * } + * @endcode + * + * @ingroup Exception + */ +class UserNotLoggedIn extends ErrorPageError { + + /** + * @param $reasonMsg A message key containing the reason for the error. + * Optional, default: 'exception-nologin-text' + * @param $titleMsg A message key to set the page title. + * Optional, default: 'exception-nologin' + * @param $params Parameters to wfMessage(). + * Optiona, default: null + */ + public function __construct( + $reasonMsg = 'exception-nologin-text', + $titleMsg = 'exception-nologin', + $params = null + ) { + parent::__construct( $titleMsg, $reasonMsg, $params ); + } +} + +/** * Show an error that looks like an HTTP server error. * Replacement for wfHttpError(). * + * @since 1.19 * @ingroup Exception */ class HttpError extends MWException { @@ -438,7 +568,7 @@ class HttpError extends MWException { $this->content = $content; } - public function reportHTML() { + public function report() { $httpMessage = HttpStatus::getMessage( $this->httpCode ); header( "Status: {$this->httpCode} {$httpMessage}" ); @@ -458,7 +588,7 @@ class HttpError extends MWException { $content = htmlspecialchars( $this->content ); } - print "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n". + print "<!DOCTYPE html>\n". "<html><head><title>$header</title></head>\n" . "<body><h1>$header</h1><p>$content</p></body></html>\n"; } @@ -508,7 +638,7 @@ class MWExceptionHandler { if ( $cmdLine ) { self::printError( $message ); } else { - self::escapeEchoAndDie( $message ); + echo nl2br( htmlspecialchars( $message ) ) . "\n"; } } } else { @@ -522,7 +652,7 @@ class MWExceptionHandler { if ( $cmdLine ) { self::printError( $message ); } else { - self::escapeEchoAndDie( $message ); + echo nl2br( htmlspecialchars( $message ) ) . "\n"; } } } @@ -530,7 +660,8 @@ class MWExceptionHandler { /** * Print a message, if possible to STDERR. * Use this in command line mode only (see isCommandLine) - * @param $message String Failure text + * + * @param $message string Failure text */ public static function printError( $message ) { # NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). @@ -543,16 +674,6 @@ class MWExceptionHandler { } /** - * Print a message after escaping it and converting newlines to <br> - * Use this for non-command line failures - * @param $message String Failure text - */ - private static function escapeEchoAndDie( $message ) { - echo nl2br( htmlspecialchars( $message ) ) . "\n"; - die(1); - } - - /** * Exception handler which simulates the appropriate catch() handling: * * try { diff --git a/includes/Export.php b/includes/Export.php index 7773d03c..f01fb237 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -49,6 +49,23 @@ class WikiExporter { const TEXT = 0; const STUB = 1; + var $buffer; + + var $text; + + /** + * @var DumpOutput + */ + var $sink; + + /** + * Returns the export schema version. + * @return string + */ + public static function schemaVersion() { + return "0.7"; + } + /** * If using WikiExporter::STREAM to stream a large amount of data, * provide a database connection which is not managed by @@ -103,7 +120,7 @@ class WikiExporter { * the most recent version. */ public function allPages() { - return $this->dumpFrom( '' ); + $this->dumpFrom( '' ); } /** @@ -118,7 +135,7 @@ class WikiExporter { if ( $end ) { $condition .= ' AND page_id < ' . intval( $end ); } - return $this->dumpFrom( $condition ); + $this->dumpFrom( $condition ); } /** @@ -133,27 +150,34 @@ class WikiExporter { if ( $end ) { $condition .= ' AND rev_id < ' . intval( $end ); } - return $this->dumpFrom( $condition ); + $this->dumpFrom( $condition ); } /** * @param $title Title */ public function pageByTitle( $title ) { - return $this->dumpFrom( + $this->dumpFrom( 'page_namespace=' . $title->getNamespace() . ' AND page_title=' . $this->db->addQuotes( $title->getDBkey() ) ); } + /** + * @param $name string + * @throws MWException + */ public function pageByName( $name ) { $title = Title::newFromText( $name ); if ( is_null( $title ) ) { throw new MWException( "Can't export invalid title" ); } else { - return $this->pageByTitle( $title ); + $this->pageByTitle( $title ); } } + /** + * @param $names array + */ public function pagesByName( $names ) { foreach ( $names as $name ) { $this->pageByName( $name ); @@ -161,20 +185,28 @@ class WikiExporter { } public function allLogs() { - return $this->dumpFrom( '' ); + $this->dumpFrom( '' ); } + /** + * @param $start int + * @param $end int + */ public function logsByRange( $start, $end ) { $condition = 'log_id >= ' . intval( $start ); if ( $end ) { $condition .= ' AND log_id < ' . intval( $end ); } - return $this->dumpFrom( $condition ); + $this->dumpFrom( $condition ); } - # Generates the distinct list of authors of an article - # Not called by default (depends on $this->list_authors) - # Can be set by Special:Export when not exporting whole history + /** + * Generates the distinct list of authors of an article + * Not called by default (depends on $this->list_authors) + * Can be set by Special:Export when not exporting whole history + * + * @param $cond + */ protected function do_list_authors( $cond ) { wfProfileIn( __METHOD__ ); $this->author_list = "<contributors>"; @@ -205,13 +237,15 @@ class WikiExporter { wfProfileOut( __METHOD__ ); } + /** + * @param $cond string + * @throws MWException + * @throws Exception + */ protected function dumpFrom( $cond = '' ) { wfProfileIn( __METHOD__ ); # For logging dumps... if ( $this->history & self::LOGS ) { - if ( $this->buffer == WikiExporter::STREAM ) { - $prev = $this->db->bufferResults( false ); - } $where = array( 'user_id = log_user' ); # Hide private logs $hideLogs = LogEventsList::getExcludeClause( $this->db ); @@ -220,16 +254,49 @@ class WikiExporter { if ( $cond ) $where[] = $cond; # Get logging table name for logging.* clause $logging = $this->db->tableName( 'logging' ); - $result = $this->db->select( array( 'logging', 'user' ), - array( "{$logging}.*", 'user_name' ), // grab the user name - $where, - __METHOD__, - array( 'ORDER BY' => 'log_id', 'USE INDEX' => array( 'logging' => 'PRIMARY' ) ) - ); - $wrapper = $this->db->resultObject( $result ); - $this->outputLogStream( $wrapper ); + if ( $this->buffer == WikiExporter::STREAM ) { - $this->db->bufferResults( $prev ); + $prev = $this->db->bufferResults( false ); + } + $wrapper = null; // Assuring $wrapper is not undefined, if exception occurs early + try { + $result = $this->db->select( array( 'logging', 'user' ), + array( "{$logging}.*", 'user_name' ), // grab the user name + $where, + __METHOD__, + array( 'ORDER BY' => 'log_id', 'USE INDEX' => array( 'logging' => 'PRIMARY' ) ) + ); + $wrapper = $this->db->resultObject( $result ); + $this->outputLogStream( $wrapper ); + if ( $this->buffer == WikiExporter::STREAM ) { + $this->db->bufferResults( $prev ); + } + } catch ( Exception $e ) { + // Throwing the exception does not reliably free the resultset, and + // would also leave the connection in unbuffered mode. + + // Freeing result + try { + if ( $wrapper ) { + $wrapper->free(); + } + } catch ( Exception $e2 ) { + // Already in panic mode -> ignoring $e2 as $e has + // higher priority + } + + // Putting database back in previous buffer mode + try { + if ( $this->buffer == WikiExporter::STREAM ) { + $this->db->bufferResults( $prev ); + } + } catch ( Exception $e2 ) { + // Already in panic mode -> ignoring $e2 as $e has + // higher priority + } + + // Inform caller about problem + throw $e; } # For page dumps... } else { @@ -279,7 +346,7 @@ class WikiExporter { } elseif ( $this->history & WikiExporter::RANGE ) { # Dump of revisions within a specified range $join['revision'] = array( 'INNER JOIN', 'page_id=rev_page' ); - $opts['ORDER BY'] = 'rev_page ASC, rev_id ASC'; + $opts['ORDER BY'] = array( 'rev_page ASC', 'rev_id ASC' ); } else { # Uknown history specification parameter? wfProfileOut( __METHOD__ ); @@ -300,20 +367,46 @@ class WikiExporter { $prev = $this->db->bufferResults( false ); } - wfRunHooks( 'ModifyExportQuery', + $wrapper = null; // Assuring $wrapper is not undefined, if exception occurs early + try { + wfRunHooks( 'ModifyExportQuery', array( $this->db, &$tables, &$cond, &$opts, &$join ) ); - # Do the query! - $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join ); - $wrapper = $this->db->resultObject( $result ); - # Output dump results - $this->outputPageStream( $wrapper ); - if ( $this->list_authors ) { + # Do the query! + $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join ); + $wrapper = $this->db->resultObject( $result ); + # Output dump results $this->outputPageStream( $wrapper ); - } - if ( $this->buffer == WikiExporter::STREAM ) { - $this->db->bufferResults( $prev ); + if ( $this->buffer == WikiExporter::STREAM ) { + $this->db->bufferResults( $prev ); + } + } catch ( Exception $e ) { + // Throwing the exception does not reliably free the resultset, and + // would also leave the connection in unbuffered mode. + + // Freeing result + try { + if ( $wrapper ) { + $wrapper->free(); + } + } catch ( Exception $e2 ) { + // Already in panic mode -> ignoring $e2 as $e has + // higher priority + } + + // Putting database back in previous buffer mode + try { + if ( $this->buffer == WikiExporter::STREAM ) { + $this->db->bufferResults( $prev ); + } + } catch ( Exception $e2 ) { + // Already in panic mode -> ignoring $e2 as $e has + // higher priority + } + + // Inform caller about problem + throw $e; } } wfProfileOut( __METHOD__ ); @@ -324,7 +417,7 @@ class WikiExporter { * The result set should be sorted/grouped by page to avoid duplicate * page records in the output. * - * The result set will be freed once complete. Should be safe for + * Should be safe for * streaming (non-buffered) queries, as long as it was made on a * separate database connection not managed by LoadBalancer; some * blob storage types will make queries to pull source data. @@ -363,6 +456,9 @@ class WikiExporter { } } + /** + * @param $resultset array + */ protected function outputLogStream( $resultset ) { foreach ( $resultset as $row ) { $output = $this->writer->writeLogItem( $row ); @@ -377,14 +473,16 @@ class WikiExporter { class XmlDumpWriter { /** * Returns the export schema version. + * @deprecated in 1.20; use WikiExporter::schemaVersion() instead * @return string */ function schemaVersion() { - return "0.6"; + wfDeprecated( __METHOD__, '1.20' ); + return WikiExporter::schemaVersion(); } /** - * Opens the XML output stream's root <mediawiki> element. + * Opens the XML output stream's root "<mediawiki>" element. * This does not include an xml directive, so is safe to include * as a subelement in a larger XML stream. Namespace and XML Schema * references are included. @@ -395,7 +493,7 @@ class XmlDumpWriter { */ function openStream() { global $wgLanguageCode; - $ver = $this->schemaVersion(); + $ver = WikiExporter::schemaVersion(); return Xml::element( 'mediawiki', array( 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/", 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", @@ -408,6 +506,9 @@ class XmlDumpWriter { $this->siteInfo(); } + /** + * @return string + */ function siteInfo() { $info = array( $this->sitename(), @@ -420,20 +521,32 @@ class XmlDumpWriter { "\n </siteinfo>\n"; } + /** + * @return string + */ function sitename() { global $wgSitename; return Xml::element( 'sitename', array(), $wgSitename ); } + /** + * @return string + */ function generator() { global $wgVersion; return Xml::element( 'generator', array(), "MediaWiki $wgVersion" ); } + /** + * @return string + */ function homelink() { return Xml::element( 'base', array(), Title::newMainPage()->getCanonicalUrl() ); } + /** + * @return string + */ function caseSetting() { global $wgCapitalLinks; // "case-insensitive" option is reserved for future @@ -441,6 +554,9 @@ class XmlDumpWriter { return Xml::element( 'case', array(), $sensitivity ); } + /** + * @return string + */ function namespaces() { global $wgContLang; $spaces = "<namespaces>\n"; @@ -466,7 +582,7 @@ class XmlDumpWriter { } /** - * Opens a <page> section on the output stream, with data + * Opens a "<page>" section on the output stream, with data * from the given database row. * * @param $row object @@ -486,13 +602,7 @@ class XmlDumpWriter { $out .= ' ' . Xml::element( 'redirect', array( 'title' => self::canonicalTitle( $redirect ) ) ) . "\n"; } } - - if ( $row->rev_sha1 ) { - $out .= " " . Xml::element('sha1', null, strval($row->rev_sha1) ) . "\n"; - } else { - $out .= " <sha1/>\n"; - } - + if ( $row->page_restrictions != '' ) { $out .= ' ' . Xml::element( 'restrictions', array(), strval( $row->page_restrictions ) ) . "\n"; @@ -504,16 +614,17 @@ class XmlDumpWriter { } /** - * Closes a <page> section on the output stream. + * Closes a "<page>" section on the output stream. * * @access private + * @return string */ function closePage() { return " </page>\n"; } /** - * Dumps a <revision> section on the output stream, with + * Dumps a "<revision>" section on the output stream, with * data filled in from the given database row. * * @param $row object @@ -525,6 +636,9 @@ class XmlDumpWriter { $out = " <revision>\n"; $out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n"; + if( $row->rev_parent_id ) { + $out .= " " . Xml::element( 'parentid', null, strval( $row->rev_parent_id ) ) . "\n"; + } $out .= $this->writeTimestamp( $row->rev_timestamp ); @@ -540,7 +654,13 @@ class XmlDumpWriter { if ( $row->rev_deleted & Revision::DELETED_COMMENT ) { $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; } elseif ( $row->rev_comment != '' ) { - $out .= " " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n"; + $out .= " " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n"; + } + + if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) { + $out .= " " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n"; + } else { + $out .= " <sha1/>\n"; } $text = ''; @@ -568,7 +688,7 @@ class XmlDumpWriter { } /** - * Dumps a <logitem> section on the output stream, with + * Dumps a "<logitem>" section on the output stream, with * data filled in from the given database row. * * @param $row object @@ -578,64 +698,78 @@ class XmlDumpWriter { function writeLogItem( $row ) { wfProfileIn( __METHOD__ ); - $out = " <logitem>\n"; - $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n"; + $out = " <logitem>\n"; + $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n"; - $out .= $this->writeTimestamp( $row->log_timestamp ); + $out .= $this->writeTimestamp( $row->log_timestamp, " " ); if ( $row->log_deleted & LogPage::DELETED_USER ) { - $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; + $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; } else { - $out .= $this->writeContributor( $row->log_user, $row->user_name ); + $out .= $this->writeContributor( $row->log_user, $row->user_name, " " ); } if ( $row->log_deleted & LogPage::DELETED_COMMENT ) { - $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; + $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; } elseif ( $row->log_comment != '' ) { - $out .= " " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n"; + $out .= " " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n"; } - $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n"; - $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n"; + $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n"; + $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n"; if ( $row->log_deleted & LogPage::DELETED_ACTION ) { - $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; + $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; } else { $title = Title::makeTitle( $row->log_namespace, $row->log_title ); - $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n"; - $out .= " " . Xml::elementClean( 'params', + $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n"; + $out .= " " . Xml::elementClean( 'params', array( 'xml:space' => 'preserve' ), strval( $row->log_params ) ) . "\n"; } - $out .= " </logitem>\n"; + $out .= " </logitem>\n"; wfProfileOut( __METHOD__ ); return $out; } - function writeTimestamp( $timestamp ) { + /** + * @param $timestamp string + * @param $indent string Default to six spaces + * @return string + */ + function writeTimestamp( $timestamp, $indent = " " ) { $ts = wfTimestamp( TS_ISO_8601, $timestamp ); - return " " . Xml::element( 'timestamp', null, $ts ) . "\n"; + return $indent . Xml::element( 'timestamp', null, $ts ) . "\n"; } - function writeContributor( $id, $text ) { - $out = " <contributor>\n"; + /** + * @param $id + * @param $text string + * @param $indent string Default to six spaces + * @return string + */ + function writeContributor( $id, $text, $indent = " " ) { + $out = $indent . "<contributor>\n"; if ( $id || !IP::isValid( $text ) ) { - $out .= " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n"; - $out .= " " . Xml::element( 'id', null, strval( $id ) ) . "\n"; + $out .= $indent . " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n"; + $out .= $indent . " " . Xml::element( 'id', null, strval( $id ) ) . "\n"; } else { - $out .= " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n"; + $out .= $indent . " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n"; } - $out .= " </contributor>\n"; + $out .= $indent . "</contributor>\n"; return $out; } /** * Warning! This data is potentially inconsistent. :( + * @param $row + * @param $dumpContents bool + * @return string */ function writeUploads( $row, $dumpContents = false ) { - if ( $row->page_namespace == NS_IMAGE ) { + if ( $row->page_namespace == NS_FILE ) { $img = wfLocalFile( $row->page_title ); if ( $img && $img->exists() ) { $out = ''; @@ -670,10 +804,15 @@ class XmlDumpWriter { } else { $contents = ''; } + if ( $file->isDeleted( File::DELETED_COMMENT ) ) { + $comment = Xml::element( 'comment', array( 'deleted' => 'deleted' ) ); + } else { + $comment = Xml::elementClean( 'comment', null, $file->getDescription() ); + } return " <upload>\n" . $this->writeTimestamp( $file->getTimestamp() ) . $this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) . - " " . Xml::elementClean( 'comment', null, $file->getDescription() ) . "\n" . + " " . $comment . "\n" . " " . Xml::element( 'filename', null, $file->getName() ) . "\n" . $archiveName . " " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" . @@ -688,7 +827,7 @@ class XmlDumpWriter { * Return prefixed text form of title, but using the content language's * canonical namespace. This skips any special-casing such as gendered * user namespaces -- which while useful, are not yet listed in the - * XML <siteinfo> data so are unsafe in export. + * XML "<siteinfo>" data so are unsafe in export. * * @param Title $title * @return string @@ -716,32 +855,55 @@ class XmlDumpWriter { * @ingroup Dump */ class DumpOutput { + + /** + * @param $string string + */ function writeOpenStream( $string ) { $this->write( $string ); } + /** + * @param $string string + */ function writeCloseStream( $string ) { $this->write( $string ); } + /** + * @param $page + * @param $string string + */ function writeOpenPage( $page, $string ) { $this->write( $string ); } + /** + * @param $string string + */ function writeClosePage( $string ) { $this->write( $string ); } + /** + * @param $rev + * @param $string string + */ function writeRevision( $rev, $string ) { $this->write( $string ); } + /** + * @param $rev + * @param $string string + */ function writeLogItem( $rev, $string ) { $this->write( $string ); } /** * Override to write to a different stream type. + * @param $string string * @return bool */ function write( $string ) { @@ -773,6 +935,7 @@ class DumpOutput { /** * Returns the name of the file or files which are * being written to, if there are any. + * @return null */ function getFilenames() { return NULL; @@ -784,27 +947,56 @@ class DumpOutput { * @ingroup Dump */ class DumpFileOutput extends DumpOutput { - protected $handle, $filename; + protected $handle = false, $filename; + /** + * @param $file + */ function __construct( $file ) { $this->handle = fopen( $file, "wt" ); $this->filename = $file; } + /** + * @param $string string + */ + function writeCloseStream( $string ) { + parent::writeCloseStream( $string ); + if ( $this->handle ) { + fclose( $this->handle ); + $this->handle = false; + } + } + + /** + * @param $string string + */ function write( $string ) { fputs( $this->handle, $string ); } + /** + * @param $newname + */ function closeRenameAndReopen( $newname ) { $this->closeAndRename( $newname, true ); } + /** + * @param $newname + * @throws MWException + */ function renameOrException( $newname ) { if (! rename( $this->filename, $newname ) ) { throw new MWException( __METHOD__ . ": rename of file {$this->filename} to $newname failed\n" ); } } + /** + * @param $newname array + * @return mixed + * @throws MWException + */ function checkRenameArgCount( $newname ) { if ( is_array( $newname ) ) { if ( count( $newname ) > 1 ) { @@ -816,10 +1008,17 @@ class DumpFileOutput extends DumpOutput { return $newname; } + /** + * @param $newname mixed + * @param $open bool + */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); if ( $newname ) { - fclose( $this->handle ); + if ( $this->handle ) { + fclose( $this->handle ); + $this->handle = false; + } $this->renameOrException( $newname ); if ( $open ) { $this->handle = fopen( $this->filename, "wt" ); @@ -827,6 +1026,9 @@ class DumpFileOutput extends DumpOutput { } } + /** + * @return string|null + */ function getFilenames() { return $this->filename; } @@ -840,7 +1042,12 @@ class DumpFileOutput extends DumpOutput { */ class DumpPipeOutput extends DumpFileOutput { protected $command, $filename; + protected $procOpenResource = false; + /** + * @param $command + * @param $file null + */ function __construct( $command, $file = null ) { if ( !is_null( $file ) ) { $command .= " > " . wfEscapeShellArg( $file ); @@ -851,6 +1058,20 @@ class DumpPipeOutput extends DumpFileOutput { $this->filename = $file; } + /** + * @param $string string + */ + function writeCloseStream( $string ) { + parent::writeCloseStream( $string ); + if ( $this->procOpenResource ) { + proc_close( $this->procOpenResource ); + $this->procOpenResource = false; + } + } + + /** + * @param $command + */ function startCommand( $command ) { $spec = array( 0 => array( "pipe", "r" ), @@ -860,15 +1081,28 @@ class DumpPipeOutput extends DumpFileOutput { $this->handle = $pipes[0]; } + /** + * @param mixed $newname + */ function closeRenameAndReopen( $newname ) { $this->closeAndRename( $newname, true ); } + /** + * @param $newname mixed + * @param $open bool + */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); if ( $newname ) { - fclose( $this->handle ); - proc_close( $this->procOpenResource ); + if ( $this->handle ) { + fclose( $this->handle ); + $this->handle = false; + } + if ( $this->procOpenResource ) { + proc_close( $this->procOpenResource ); + $this->procOpenResource = false; + } $this->renameOrException( $newname ); if ( $open ) { $command = $this->command; @@ -885,6 +1119,10 @@ class DumpPipeOutput extends DumpFileOutput { * @ingroup Dump */ class DumpGZipOutput extends DumpPipeOutput { + + /** + * @param $file string + */ function __construct( $file ) { parent::__construct( "gzip", $file ); } @@ -895,6 +1133,10 @@ class DumpGZipOutput extends DumpPipeOutput { * @ingroup Dump */ class DumpBZip2Output extends DumpPipeOutput { + + /** + * @param $file string + */ function __construct( $file ) { parent::__construct( "bzip2", $file ); } @@ -905,12 +1147,20 @@ class DumpBZip2Output extends DumpPipeOutput { * @ingroup Dump */ class Dump7ZipOutput extends DumpPipeOutput { + + /** + * @param $file string + */ function __construct( $file ) { $command = $this->setup7zCommand( $file ); parent::__construct( $command ); $this->filename = $file; } + /** + * @param $file string + * @return string + */ function setup7zCommand( $file ) { $command = "7za a -bd -si " . wfEscapeShellArg( $file ); // Suppress annoying useless crap from p7zip @@ -919,6 +1169,10 @@ class Dump7ZipOutput extends DumpPipeOutput { return( $command ); } + /** + * @param $newname string + * @param $open bool + */ function closeAndRename( $newname, $open = false ) { $newname = $this->checkRenameArgCount( $newname ); if ( $newname ) { @@ -933,8 +1187,6 @@ class Dump7ZipOutput extends DumpPipeOutput { } } - - /** * Dump output filter class. * This just does output filtering and streaming; XML formatting is done @@ -942,18 +1194,44 @@ class Dump7ZipOutput extends DumpPipeOutput { * @ingroup Dump */ class DumpFilter { + + /** + * @var DumpOutput + * FIXME will need to be made protected whenever legacy code + * is updated. + */ + public $sink; + + /** + * @var bool + */ + protected $sendingThisPage; + + /** + * @param $sink DumpOutput + */ function __construct( &$sink ) { $this->sink =& $sink; } + /** + * @param $string string + */ function writeOpenStream( $string ) { $this->sink->writeOpenStream( $string ); } + /** + * @param $string string + */ function writeCloseStream( $string ) { $this->sink->writeCloseStream( $string ); } + /** + * @param $page + * @param $string string + */ function writeOpenPage( $page, $string ) { $this->sendingThisPage = $this->pass( $page, $string ); if ( $this->sendingThisPage ) { @@ -961,6 +1239,9 @@ class DumpFilter { } } + /** + * @param $string string + */ function writeClosePage( $string ) { if ( $this->sendingThisPage ) { $this->sink->writeClosePage( $string ); @@ -968,30 +1249,49 @@ class DumpFilter { } } + /** + * @param $rev + * @param $string string + */ function writeRevision( $rev, $string ) { if ( $this->sendingThisPage ) { $this->sink->writeRevision( $rev, $string ); } } + /** + * @param $rev + * @param $string string + */ function writeLogItem( $rev, $string ) { $this->sink->writeRevision( $rev, $string ); } + /** + * @param $newname string + */ function closeRenameAndReopen( $newname ) { $this->sink->closeRenameAndReopen( $newname ); } + /** + * @param $newname string + * @param $open bool + */ function closeAndRename( $newname, $open = false ) { $this->sink->closeAndRename( $newname, $open ); } + /** + * @return array + */ function getFilenames() { return $this->sink->getFilenames(); } /** * Override for page-based filter types. + * @param $page * @return bool */ function pass( $page ) { @@ -1004,6 +1304,11 @@ class DumpFilter { * @ingroup Dump */ class DumpNotalkFilter extends DumpFilter { + + /** + * @param $page + * @return bool + */ function pass( $page ) { return !MWNamespace::isTalk( $page->page_namespace ); } @@ -1017,6 +1322,10 @@ class DumpNamespaceFilter extends DumpFilter { var $invert = false; var $namespaces = array(); + /** + * @param $sink DumpOutput + * @param $param + */ function __construct( &$sink, $param ) { parent::__construct( $sink ); @@ -1059,6 +1368,10 @@ class DumpNamespaceFilter extends DumpFilter { } } + /** + * @param $page + * @return bool + */ function pass( $page ) { $match = isset( $this->namespaces[$page->page_namespace] ); return $this->invert xor $match; @@ -1073,11 +1386,18 @@ class DumpNamespaceFilter extends DumpFilter { class DumpLatestFilter extends DumpFilter { var $page, $pageString, $rev, $revString; + /** + * @param $page + * @param $string string + */ function writeOpenPage( $page, $string ) { $this->page = $page; $this->pageString = $string; } + /** + * @param $string string + */ function writeClosePage( $string ) { if ( $this->rev ) { $this->sink->writeOpenPage( $this->page, $this->pageString ); @@ -1090,6 +1410,10 @@ class DumpLatestFilter extends DumpFilter { $this->pageString = null; } + /** + * @param $rev + * @param $string string + */ function writeRevision( $rev, $string ) { if ( $rev->rev_id == $this->page->page_latest ) { $this->rev = $rev; @@ -1103,51 +1427,82 @@ class DumpLatestFilter extends DumpFilter { * @ingroup Dump */ class DumpMultiWriter { + + /** + * @param $sinks + */ function __construct( $sinks ) { $this->sinks = $sinks; $this->count = count( $sinks ); } + /** + * @param $string string + */ function writeOpenStream( $string ) { for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeOpenStream( $string ); } } + /** + * @param $string string + */ function writeCloseStream( $string ) { for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeCloseStream( $string ); } } + /** + * @param $page + * @param $string string + */ function writeOpenPage( $page, $string ) { for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeOpenPage( $page, $string ); } } + /** + * @param $string + */ function writeClosePage( $string ) { for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeClosePage( $string ); } } + /** + * @param $rev + * @param $string + */ function writeRevision( $rev, $string ) { for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeRevision( $rev, $string ); } } + /** + * @param $newnames + */ function closeRenameAndReopen( $newnames ) { $this->closeAndRename( $newnames, true ); } + /** + * @param $newnames array + * @param bool $open + */ function closeAndRename( $newnames, $open = false ) { for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->closeAndRename( $newnames[$i], $open ); } } + /** + * @return array + */ function getFilenames() { $filenames = array(); for ( $i = 0; $i < $this->count; $i++ ) { @@ -1158,6 +1513,10 @@ class DumpMultiWriter { } +/** + * @param $string string + * @return string + */ function xmlsafe( $string ) { wfProfileIn( __FUNCTION__ ); diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index b8704758..34683253 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -80,10 +80,16 @@ class ExternalEdit extends ContextSource { } elseif ( $this->getRequest()->getVal( 'mode' ) == 'file' ) { $type = "Edit file"; $image = wfLocalFile( $this->getTitle() ); - $urls = array( 'File' => array( - 'Extension' => $image->getExtension(), - 'URL' => $image->getCanonicalURL() - ) ); + if ( $image ) { + $urls = array( + 'File' => array( + 'Extension' => $image->getExtension(), + 'URL' => $image->getCanonicalURL() + ) + ); + } else{ + $urls = array(); + } } else { $type = "Edit text"; # *.wiki file extension is used by some editors for syntax diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index 3bee6ed8..61d4ef7c 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -1,5 +1,26 @@ <?php /** + * Data storage in external repositories. + * + * 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 + */ + +/** * @defgroup ExternalStorage ExternalStorage */ @@ -24,7 +45,7 @@ class ExternalStore { * * @param $url String: The URL of the text to get * @param $params Array: associative array of parameters for the ExternalStore object. - * @return The text stored or false on error + * @return string|bool The text stored or false on error */ static function fetchFromURL( $url, $params = array() ) { global $wgExternalStores; @@ -81,7 +102,7 @@ class ExternalStore { * @param $url * @param $data * @param $params array - * @return string|false The URL of the stored data item, or false on error + * @return string|bool The URL of the stored data item, or false on error */ static function insert( $url, $data, $params = array() ) { list( $proto, $params ) = explode( '://', $url, 2 ); diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index 4920a91c..6f2b33e1 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -1,4 +1,24 @@ <?php +/** + * External storage in SQL database. + * + * 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 + */ /** * DB accessable external objects @@ -73,6 +93,7 @@ class ExternalStoreDB { /** * Fetch data from given URL * @param $url String: an url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage. + * @return mixed */ function fetchFromURL( $url ) { $path = explode( '/', $url ); @@ -157,7 +178,7 @@ class ExternalStoreDB { throw new MWException( __METHOD__.': no insert ID' ); } if ( $dbw->getFlag( DBO_TRX ) ) { - $dbw->commit(); + $dbw->commit( __METHOD__ ); } return "DB://$cluster/$id"; } diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php index 092ff7d8..311e32b3 100644 --- a/includes/ExternalStoreHttp.php +++ b/includes/ExternalStoreHttp.php @@ -1,4 +1,24 @@ <?php +/** + * External storage using HTTP requests. + * + * 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 + */ /** * Example class for HTTP accessable external objects. diff --git a/includes/ExternalUser.php b/includes/ExternalUser.php index 37716390..9a01deb7 100644 --- a/includes/ExternalUser.php +++ b/includes/ExternalUser.php @@ -18,6 +18,8 @@ * 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 */ /** @@ -98,7 +100,7 @@ abstract class ExternalUser { * This is a wrapper around newFromId(). * * @param $user User - * @return ExternalUser|false + * @return ExternalUser|bool False on failure */ public static function newFromUser( $user ) { global $wgExternalAuthType; diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php index 8415ec08..60f7600d 100644 --- a/includes/FakeTitle.php +++ b/includes/FakeTitle.php @@ -1,4 +1,24 @@ <?php +/** + * Fake title class that triggers an error if any members are called. + * + * 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 + */ /** * Fake title class that triggers an error if any members are called @@ -59,7 +79,6 @@ class FakeTitle extends Title { function getSkinFromCssJsSubpage() { $this->error(); } function isCssSubpage() { $this->error(); } function isJsSubpage() { $this->error(); } - function userCanEditCssJsSubpage() { $this->error(); } function userCanEditCssSubpage() { $this->error(); } function userCanEditJsSubpage() { $this->error(); } function isCascadeProtected() { $this->error(); } @@ -88,8 +107,6 @@ class FakeTitle extends Title { function moveNoAuth( &$nt ) { $this->error(); } function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { $this->error(); } function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); } - function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) { $this->error(); } - function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) { $this->error(); } function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); } function isSingleRevRedirect() { $this->error(); } function isValidMoveTarget( $nt ) { $this->error(); } diff --git a/includes/Fallback.php b/includes/Fallback.php index b517cd16..4b138c11 100644 --- a/includes/Fallback.php +++ b/includes/Fallback.php @@ -1,6 +1,7 @@ <?php - /** + * Fallback functions for PHP installed without mbstring support. + * * 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 @@ -16,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @file */ /** diff --git a/includes/Feed.php b/includes/Feed.php index 351f3572..f9dbf5ba 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -183,34 +183,35 @@ class FeedItem { * @todo document (needs one-sentence top-level class description). * @ingroup Feed */ -class ChannelFeed extends FeedItem { - /**#@+ - * Abstract function, override! - * @abstract - */ - +abstract class ChannelFeed extends FeedItem { /** * Generate Header of the feed + * @par Example: + * @code + * print "<feed>"; + * @endcode + * @param $item */ - function outHeader() { - # print "<feed>"; - } + abstract public function outHeader(); /** * Generate an item + * @par Example: + * @code + * print "<item>...</item>"; + * @endcode * @param $item */ - function outItem( $item ) { - # print "<item>...</item>"; - } + abstract public function outItem( $item ); /** * Generate Footer of the feed + * @par Example: + * @code + * print "</feed>"; + * @endcode */ - function outFooter() { - # print "</feed>"; - } - /**#@-*/ + abstract public function outFooter(); /** * Setup and send HTTP headers. Don't send any content; @@ -334,6 +335,7 @@ class RSSFeed extends ChannelFeed { class AtomFeed extends ChannelFeed { /** * @todo document + * @return string */ function formatTime( $ts ) { // need to use RFC 822 time format at least for rss2.0 diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index cf42329b..11b2675d 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -1,4 +1,25 @@ <?php +/** + * Helper functions for feeds. + * + * 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 Feed + */ /** * Helper functions for feeds @@ -64,7 +85,7 @@ class FeedUtils { $row->rc_last_oldid, $row->rc_this_oldid, $timestamp, ($row->rc_deleted & Revision::DELETED_COMMENT) - ? wfMsgHtml('rev-deleted-comment') + ? wfMessage('rev-deleted-comment')->escaped() : $row->rc_comment, $actiontext ); @@ -108,21 +129,22 @@ class FeedUtils { if( $oldid ) { wfProfileIn( __METHOD__."-dodiff" ); - #$diffText = $de->getDiff( wfMsg( 'revisionasof', + #$diffText = $de->getDiff( wfMessage( 'revisionasof', # $wgLang->timeanddate( $timestamp ), # $wgLang->date( $timestamp ), - # $wgLang->time( $timestamp ) ), - # wfMsg( 'currentrev' ) ); + # $wgLang->time( $timestamp ) )->text(), + # wfMessage( 'currentrev' )->text() ); + $diffText = ''; // Don't bother generating the diff if we won't be able to show it if ( $wgFeedDiffCutoff > 0 ) { $de = new DifferenceEngine( $title, $oldid, $newid ); $diffText = $de->getDiff( - wfMsg( 'previousrevision' ), // hack - wfMsg( 'revisionasof', + wfMessage( 'previousrevision' )->text(), // hack + wfMessage( 'revisionasof', $wgLang->timeanddate( $timestamp ), $wgLang->date( $timestamp ), - $wgLang->time( $timestamp ) ) ); + $wgLang->time( $timestamp ) )->text() ); } if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) { @@ -148,7 +170,7 @@ class FeedUtils { // Omit large new page diffs, bug 29110 $diffText = self::getDiffLink( $title, $newid ); } else { - $diffText = '<p><b>' . wfMsg( 'newpage' ) . '</b></p>' . + $diffText = '<p><b>' . wfMessage( 'newpage' )->text() . '</b></p>' . '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>'; } } @@ -165,6 +187,7 @@ class FeedUtils { * @param $title Title object: used to generate the diff URL * @param $newid Integer newid for this diff * @param $oldid Integer|null oldid for the diff. Null means it is a new article + * @return string */ protected static function getDiffLink( Title $title, $newid, $oldid = null ) { $queryParameters = ($oldid == null) @@ -173,7 +196,7 @@ class FeedUtils { $diffUrl = $title->getFullUrl( $queryParameters ); $diffLink = Html::element( 'a', array( 'href' => $diffUrl ), - wfMsgForContent( 'showdiff' ) ); + wfMessage( 'showdiff' )->inContentLanguage()->text() ); return $diffLink; } diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 11f9aea5..e75ad729 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -1,10 +1,31 @@ <?php +/** + * File deletion user interface. + * + * 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 + * @author Rob Church <robchur@gmail.com> + * @ingroup Media + */ /** * File deletion user interface * * @ingroup Media - * @author Rob Church <robchur@gmail.com> */ class FileDeleteForm { @@ -80,7 +101,8 @@ class FileDeleteForm { $reason = $deleteReason; } elseif ( $deleteReason != '' ) { // Entry from drop down menu + additional comment - $reason = $deleteReasonList . wfMsgForContent( 'colon-separator' ) . $deleteReason; + $reason = $deleteReasonList . wfMessage( 'colon-separator' ) + ->inContentLanguage()->text() . $deleteReason; } else { $reason = $deleteReasonList; } @@ -89,9 +111,7 @@ class FileDeleteForm { if( !$status->isGood() ) { $wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" ); - $wgOut->addHTML( '<span class="error">' ); - $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); - $wgOut->addHTML( '</span>' ); + $wgOut->addWikiText( '<div class="error">' . $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) . '</div>' ); } if( $status->ok ) { $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) ); @@ -100,10 +120,12 @@ class FileDeleteForm { // file, otherwise go back to the description page $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() ); - if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) { - WatchAction::doWatch( $this->title, $wgUser ); - } elseif ( $this->title->userIsWatching() ) { - WatchAction::doUnwatch( $this->title, $wgUser ); + if ( $wgUser->isLoggedIn() && $wgRequest->getCheck( 'wpWatch' ) != $wgUser->isWatched( $this->title ) ) { + if ( $wgRequest->getCheck( 'wpWatch' ) ) { + WatchAction::doWatch( $this->title, $wgUser ); + } else { + WatchAction::doUnwatch( $this->title, $wgUser ); + } } } return; @@ -122,6 +144,7 @@ class FileDeleteForm { * @param $reason String: reason of the deletion * @param $suppress Boolean: whether to mark all deleted versions as restricted * @param $user User object performing the request + * @return bool|Status */ public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress, User $user = null ) { if ( $user === null ) { @@ -134,12 +157,20 @@ class FileDeleteForm { $status = $file->deleteOld( $oldimage, $reason, $suppress ); if( $status->ok ) { // Need to do a log item - $log = new LogPage( 'delete' ); - $logComment = wfMsgForContent( 'deletedrevision', $oldimage ); + $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text(); if( trim( $reason ) != '' ) { - $logComment .= wfMsgForContent( 'colon-separator' ) . $reason; + $logComment .= wfMessage( 'colon-separator' ) + ->inContentLanguage()->text() . $reason; } - $log->addEntry( 'delete', $title, $logComment ); + + $logtype = $suppress ? 'suppress' : 'delete'; + + $logEntry = new ManualLogEntry( $logtype, 'delete' ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( $title ); + $logEntry->setComment( $logComment ); + $logid = $logEntry->insert(); + $logEntry->publish( $logid ); } } else { $status = Status::newFatal( 'cannotdelete', @@ -150,17 +181,20 @@ class FileDeleteForm { try { // delete the associated article first $error = ''; - if ( $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user ) >= WikiPage::DELETE_SUCCESS ) { + $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user ); + // doDeleteArticleReal() returns a non-fatal error status if the page + // or revision is missing, so check for isOK() rather than isGood() + if ( $deleteStatus->isOK() ) { $status = $file->delete( $reason, $suppress ); if( $status->isOK() ) { - $dbw->commit(); + $dbw->commit( __METHOD__ ); } else { - $dbw->rollback(); + $dbw->rollback( __METHOD__ ); } } } catch ( MWException $e ) { // rollback before returning to prevent UI from displaying incorrect "View or restore N deleted edits?" - $dbw->rollback(); + $dbw->rollback( __METHOD__ ); throw $e; } } @@ -182,7 +216,7 @@ class FileDeleteForm { $suppress = "<tr id=\"wpDeleteSuppressRow\"> <td></td> <td class='mw-input'><strong>" . - Xml::checkLabel( wfMsg( 'revdelete-suppress' ), + Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '3' ) ) . "</strong></td> </tr>"; @@ -190,27 +224,32 @@ class FileDeleteForm { $suppress = ''; } - $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->title->userIsWatching(); + $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $wgUser->isWatched( $this->title ); $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction(), 'id' => 'mw-img-deleteconfirm' ) ) . Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) . + Xml::element( 'legend', null, wfMessage( 'filedelete-legend' )->text() ) . Html::hidden( 'wpEditToken', $wgUser->getEditToken( $this->oldimage ) ) . $this->prepareMessage( 'filedelete-intro' ) . Xml::openElement( 'table', array( 'id' => 'mw-img-deleteconfirm-table' ) ) . "<tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'filedelete-comment' ), 'wpDeleteReasonList' ) . + Xml::label( wfMessage( 'filedelete-comment' )->text(), 'wpDeleteReasonList' ) . "</td> <td class='mw-input'>" . - Xml::listDropDown( 'wpDeleteReasonList', - wfMsgForContent( 'filedelete-reason-dropdown' ), - wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) . + Xml::listDropDown( + 'wpDeleteReasonList', + wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->text(), + wfMessage( 'filedelete-reason-otherlist' )->inContentLanguage()->text(), + '', + 'wpReasonDropDown', + 1 + ) . "</td> </tr> <tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'filedelete-otherreason' ), 'wpReason' ) . + Xml::label( wfMessage( 'filedelete-otherreason' )->text(), 'wpReason' ) . "</td> <td class='mw-input'>" . Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), @@ -223,7 +262,7 @@ class FileDeleteForm { <tr> <td></td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'watchthis' ), + Xml::checkLabel( wfMessage( 'watchthis' )->text(), 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . "</td> </tr>"; @@ -232,7 +271,7 @@ class FileDeleteForm { <tr> <td></td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'filedelete-submit' ), + Xml::submitButton( wfMessage( 'filedelete-submit' )->text(), array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '4' ) ) . "</td> </tr>" . @@ -244,7 +283,7 @@ class FileDeleteForm { $title = Title::makeTitle( NS_MEDIAWIKI, 'Filedelete-reason-dropdown' ); $link = Linker::link( $title, - wfMsgHtml( 'filedelete-edit-reasonlist' ), + wfMessage( 'filedelete-edit-reasonlist' )->escaped(), array(), array( 'action' => 'edit' ) ); @@ -259,7 +298,8 @@ class FileDeleteForm { */ private function showLogEntries() { global $wgOut; - $wgOut->addHTML( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); + $deleteLogPage = new LogPage( 'delete' ); + $wgOut->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" ); LogEventsList::showLogExtract( $wgOut, 'delete', $this->title ); } @@ -274,19 +314,17 @@ class FileDeleteForm { private function prepareMessage( $message ) { global $wgLang; if( $this->oldimage ) { - return wfMsgExt( + return wfMessage( "{$message}-old", # To ensure grep will find them: 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old' - 'parse', wfEscapeWikiText( $this->title->getText() ), $wgLang->date( $this->getTimestamp(), true ), $wgLang->time( $this->getTimestamp(), true ), - wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) ); + wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) )->parseAsBlock(); } else { - return wfMsgExt( + return wfMessage( $message, - 'parse', wfEscapeWikiText( $this->title->getText() ) - ); + )->parseAsBlock(); } } diff --git a/includes/ForkController.php b/includes/ForkController.php index 9cacef54..448bc03b 100644 --- a/includes/ForkController.php +++ b/includes/ForkController.php @@ -1,4 +1,24 @@ <?php +/** + * Class for managing forking command line scripts. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ /** * Class for managing forking command line scripts. @@ -49,6 +69,7 @@ class ForkController { * This will return 'child' in the child processes. In the parent process, * it will run until all the child processes exit or a TERM signal is * received. It will then return 'done'. + * @return string */ public function start() { // Trap SIGTERM @@ -116,8 +137,10 @@ class ForkController { protected function prepareEnvironment() { global $wgMemc; - // Don't share DB or memcached connections + // Don't share DB, storage, or memcached connections wfGetLBFactory()->destroyInstance(); + FileBackendGroup::destroySingleton(); + LockManagerGroup::destroySingleton(); ObjectCache::clear(); $wgMemc = null; } diff --git a/includes/FormOptions.php b/includes/FormOptions.php index ccc87d8a..33bbd86a 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -1,16 +1,35 @@ <?php /** * Helper class to keep track of options when mixing links and form elements. - * @todo This badly need some examples and tests :-) * * Copyright © 2008, Niklas Laxstiröm - * * Copyright © 2011, Antoine Musso * + * 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 * @author Niklas Laxström * @author Antoine Musso */ +/** + * Helper class to keep track of options when mixing links and form elements. + * + * @todo This badly need some examples and tests :-) + */ class FormOptions implements ArrayAccess { /** @name Type constants * Used internally to map an option value to a WebRequest accessor @@ -65,7 +84,7 @@ class FormOptions implements ArrayAccess { * * @param $data Mixed: value to guess type for * @exception MWException Unsupported datatype - * @return Type constant + * @return int Type constant */ public static function guessType( $data ) { if ( is_bool( $data ) ) { @@ -291,11 +310,17 @@ class FormOptions implements ArrayAccess { * @see http://php.net/manual/en/class.arrayaccess.php */ /* @{ */ - /** Whether option exist*/ + /** + * Whether option exist + * @return bool + */ public function offsetExists( $name ) { return isset( $this->options[$name] ); } - /** Retrieve an option value */ + /** + * Retrieve an option value + * @return Mixed + */ public function offsetGet( $name ) { return $this->getValue( $name ); } diff --git a/includes/GitInfo.php b/includes/GitInfo.php new file mode 100644 index 00000000..c3c30733 --- /dev/null +++ b/includes/GitInfo.php @@ -0,0 +1,214 @@ +<?php +/** + * A class to help return information about a git repo MediaWiki may be inside + * This is used by Special:Version and is also useful for the LocalSettings.php + * of anyone working on large branches in git to setup config that show up only + * when specific branches are currently checked out. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +class GitInfo { + + /** + * Singleton for the repo at $IP + */ + protected static $repo = null; + + /** + * Location of the .git directory + */ + protected $basedir; + + /** + * Map of repo URLs to viewer URLs. Access via static method getViewers(). + */ + private static $viewers = false; + + /** + * @param $dir string The root directory of the repo where the .git dir can be found + */ + public function __construct( $dir ) { + $this->basedir = "{$dir}/.git/"; + } + + /** + * Return a singleton for the repo at $IP + * @return GitInfo + */ + public static function repo() { + global $IP; + if ( is_null( self::$repo ) ) { + self::$repo = new self( $IP ); + } + return self::$repo; + } + + /** + * Check if a string looks like a hex encoded SHA1 hash + * + * @param $str string The string to check + * @return bool Whether or not the string looks like a SHA1 + */ + public static function isSHA1( $str ) { + return !!preg_match( '/^[0-9A-F]{40}$/i', $str ); + } + + /** + * Return the HEAD of the repo (without any opening "ref: ") + * @return string The HEAD + */ + public function getHead() { + $HEADfile = "{$this->basedir}/HEAD"; + + if ( !is_readable( $HEADfile ) ) { + return false; + } + + $HEAD = file_get_contents( $HEADfile ); + + if ( preg_match( "/ref: (.*)/", $HEAD, $m ) ) { + return rtrim( $m[1] ); + } else { + return rtrim( $HEAD ); + } + } + + /** + * Return the SHA1 for the current HEAD of the repo + * @return string A SHA1 or false + */ + public function getHeadSHA1() { + $HEAD = $this->getHead(); + + // If detached HEAD may be a SHA1 + if ( self::isSHA1( $HEAD ) ) { + return $HEAD; + } + + // If not a SHA1 it may be a ref: + $REFfile = "{$this->basedir}{$HEAD}"; + if ( !is_readable( $REFfile ) ) { + return false; + } + + $sha1 = rtrim( file_get_contents( $REFfile ) ); + + return $sha1; + } + + /** + * Return the name of the current branch, or HEAD if not found + * @return string The branch name, HEAD, or false + */ + public function getCurrentBranch() { + $HEAD = $this->getHead(); + if ( $HEAD && preg_match( "#^refs/heads/(.*)$#", $HEAD, $m ) ) { + return $m[1]; + } else { + return $HEAD; + } + } + + /** + * Get an URL to a web viewer link to the HEAD revision. + * + * @return string|bool string if an URL is available or false otherwise. + */ + public function getHeadViewUrl() { + $config = "{$this->basedir}/config"; + if ( !is_readable( $config ) ) { + return false; + } + + $configArray = parse_ini_file( $config, true ); + $remote = false; + + // Use the "origin" remote repo if available or any other repo if not. + if ( isset( $configArray['remote origin'] ) ) { + $remote = $configArray['remote origin']; + } else { + foreach( $configArray as $sectionName => $sectionConf ) { + if ( substr( $sectionName, 0, 6 ) == 'remote' ) { + $remote = $sectionConf; + } + } + } + + if ( $remote === false || !isset( $remote['url'] ) ) { + return false; + } + + $url = $remote['url']; + if ( substr( $url, -4 ) !== '.git' ) { + $url .= '.git'; + } + foreach( self::getViewers() as $repo => $viewer ) { + $pattern = '#^' . $repo . '$#'; + if ( preg_match( $pattern, $url ) ) { + $viewerUrl = preg_replace( $pattern, $viewer, $url ); + $headSHA1 = $this->getHeadSHA1(); + $replacements = array( + '%h' => substr( $headSHA1, 0, 7 ), + '%H' => $headSHA1 + ); + return strtr( $viewerUrl, $replacements ); + } + } + return false; + } + + /** + * @see self::getHeadSHA1 + * @return string + */ + public static function headSHA1() { + return self::repo()->getHeadSHA1(); + } + + /** + * @see self::getCurrentBranch + * @return string + */ + public static function currentBranch() { + return self::repo()->getCurrentBranch(); + } + + /** + * @see self::getHeadViewUrl() + * @return bool|string + */ + public static function headViewUrl() { + return self::repo()->getHeadViewUrl(); + } + + /** + * Gets the list of repository viewers + * @return array + */ + protected static function getViewers() { + global $wgGitRepositoryViewers; + + if( self::$viewers === false ) { + self::$viewers = $wgGitRepositoryViewers; + wfRunHooks( 'GitViewers', array( &self::$viewers ) ); + } + + return self::$viewers; + } +} diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 65fc643e..8f701c6b 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -1,6 +1,22 @@ <?php /** - * Global functions used everywhere + * Global functions used everywhere. + * + * 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 */ @@ -14,39 +30,54 @@ if ( !defined( 'MEDIAWIKI' ) ) { /** * Compatibility functions * - * We support PHP 5.2.3 and up. + * We support PHP 5.3.2 and up. * Re-implementations of newer functions or functions in non-standard * PHP extensions may be included here. */ if( !function_exists( 'iconv' ) ) { - /** @codeCoverageIgnore */ + /** + * @codeCoverageIgnore + * @return string + */ function iconv( $from, $to, $string ) { return Fallback::iconv( $from, $to, $string ); } } if ( !function_exists( 'mb_substr' ) ) { - /** @codeCoverageIgnore */ + /** + * @codeCoverageIgnore + * @return string + */ function mb_substr( $str, $start, $count='end' ) { return Fallback::mb_substr( $str, $start, $count ); } - /** @codeCoverageIgnore */ + /** + * @codeCoverageIgnore + * @return int + */ function mb_substr_split_unicode( $str, $splitPos ) { return Fallback::mb_substr_split_unicode( $str, $splitPos ); } } if ( !function_exists( 'mb_strlen' ) ) { - /** @codeCoverageIgnore */ + /** + * @codeCoverageIgnore + * @return int + */ function mb_strlen( $str, $enc = '' ) { return Fallback::mb_strlen( $str, $enc ); } } if( !function_exists( 'mb_strpos' ) ) { - /** @codeCoverageIgnore */ + /** + * @codeCoverageIgnore + * @return int + */ function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) { return Fallback::mb_strpos( $haystack, $needle, $offset, $encoding ); } @@ -54,7 +85,10 @@ if( !function_exists( 'mb_strpos' ) ) { } if( !function_exists( 'mb_strrpos' ) ) { - /** @codeCoverageIgnore */ + /** + * @codeCoverageIgnore + * @return int + */ function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) { return Fallback::mb_strrpos( $haystack, $needle, $offset, $encoding ); } @@ -63,7 +97,10 @@ if( !function_exists( 'mb_strrpos' ) ) { // Support for Wietse Venema's taint feature if ( !function_exists( 'istainted' ) ) { - /** @codeCoverageIgnore */ + /** + * @codeCoverageIgnore + * @return int + */ function istainted( $var ) { return 0; } @@ -200,7 +237,7 @@ function wfMergeErrorArrays( /*...*/ ) { * @param $after Mixed: The key to insert after * @return Array */ -function wfArrayInsertAfter( $array, $insert, $after ) { +function wfArrayInsertAfter( array $array, array $insert, $after ) { // Find the offset of the element to insert after. $keys = array_keys( $array ); $offsetByKey = array_flip( $keys ); @@ -274,6 +311,24 @@ function wfRandom() { } /** + * Get a random string containing a number of pesudo-random hex + * characters. + * @note This is not secure, if you are trying to generate some sort + * of token please use MWCryptRand instead. + * + * @param $length int The length of the string to generate + * @return String + * @since 1.20 + */ +function wfRandomString( $length = 32 ) { + $str = ''; + while ( strlen( $str ) < $length ) { + $str .= dechex( mt_rand() ); + } + return substr( $str, 0, $length ); +} + +/** * We want some things to be included as literal characters in our title URLs * for prettiness, which urlencode encodes by default. According to RFC 1738, * all of the following should be safe: @@ -329,7 +384,7 @@ function wfUrlencode( $s ) { * @param $prefix String * @return String */ -function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) { +function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) { if ( !is_null( $array2 ) ) { $array1 = $array1 + $array2; } @@ -348,7 +403,7 @@ function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) { foreach ( $value as $k => $v ) { $cgi .= $firstTime ? '' : '&'; if ( is_array( $v ) ) { - $cgi .= wfArrayToCGI( $v, null, $key . "[$k]" ); + $cgi .= wfArrayToCgi( $v, null, $key . "[$k]" ); } else { $cgi .= urlencode( $key . "[$k]" ) . '=' . urlencode( $v ); } @@ -366,7 +421,7 @@ function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) { } /** - * This is the logical opposite of wfArrayToCGI(): it accepts a query string as + * This is the logical opposite of wfArrayToCgi(): it accepts a query string as * its argument and returns the same string in array form. This allows compa- * tibility with legacy functions that accept raw query strings instead of nice * arrays. Of course, keys and values are urldecode()d. @@ -423,7 +478,7 @@ function wfCgiToArray( $query ) { */ function wfAppendQuery( $url, $query ) { if ( is_array( $query ) ) { - $query = wfArrayToCGI( $query ); + $query = wfArrayToCgi( $query ); } if( $query != '' ) { if( false === strpos( $url, '?' ) ) { @@ -731,6 +786,9 @@ function wfParseUrl( $url ) { return false; } + // parse_url() incorrectly handles schemes case-sensitively. Convert it to lowercase. + $bits['scheme'] = strtolower( $bits['scheme'] ); + // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it if ( in_array( $bits['scheme'] . '://', $wgUrlProtocols ) ) { $bits['delimiter'] = '://'; @@ -765,6 +823,31 @@ function wfParseUrl( $url ) { } /** + * Take a URL, make sure it's expanded to fully qualified, and replace any + * encoded non-ASCII Unicode characters with their UTF-8 original forms + * for more compact display and legibility for local audiences. + * + * @todo handle punycode domains too + * + * @param $url string + * @return string + */ +function wfExpandIRI( $url ) { + return preg_replace_callback( '/((?:%[89A-F][0-9A-F])+)/i', 'wfExpandIRI_callback', wfExpandUrl( $url ) ); +} + +/** + * Private callback for wfExpandIRI + * @param array $matches + * @return string + */ +function wfExpandIRI_callback( $matches ) { + return urldecode( $matches[1] ); +} + + + +/** * Make URL indexes, appropriate for the el_index field of externallinks. * * @param $url String @@ -852,25 +935,21 @@ function wfMatchesDomainList( $url, $domains ) { * @param $logonly Bool: set true to avoid appearing in HTML when $wgDebugComments is set */ function wfDebug( $text, $logonly = false ) { - global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage; - global $wgDebugLogPrefix, $wgShowDebug; - - static $cache = array(); // Cache of unoutputted messages - $text = wfDebugTimer() . $text; + global $wgDebugLogFile, $wgProfileOnly, $wgDebugRawPage, $wgDebugLogPrefix; if ( !$wgDebugRawPage && wfIsDebugRawPage() ) { return; } - if ( ( $wgDebugComments || $wgShowDebug ) && !$logonly ) { - $cache[] = $text; + $timer = wfDebugTimer(); + if ( $timer !== '' ) { + $text = preg_replace( '/[^\n]/', $timer . '\0', $text, 1 ); + } - if ( isset( $wgOut ) && is_object( $wgOut ) ) { - // add the message and any cached messages to the output - array_map( array( $wgOut, 'debug' ), $cache ); - $cache = array(); - } + if ( !$logonly ) { + MWDebug::debugMsg( $text ); } + if ( wfRunHooks( 'Debug', array( $text, null /* no log group */ ) ) ) { if ( $wgDebugLogFile != '' && !$wgProfileOnly ) { # Strip unprintables; they can switch terminal modes when binary data @@ -880,12 +959,11 @@ function wfDebug( $text, $logonly = false ) { wfErrorLog( $text, $wgDebugLogFile ); } } - - MWDebug::debugMsg( $text ); } /** * Returns true if debug logging should be suppressed if $wgDebugRawPage = false + * @return bool */ function wfIsDebugRawPage() { static $cache; @@ -968,11 +1046,28 @@ function wfDebugLog( $logGroup, $text, $public = true ) { * @param $text String: database error message. */ function wfLogDBError( $text ) { - global $wgDBerrorLog; + global $wgDBerrorLog, $wgDBerrorLogTZ; + static $logDBErrorTimeZoneObject = null; + if ( $wgDBerrorLog ) { $host = wfHostname(); $wiki = wfWikiID(); - $text = date( 'D M j G:i:s T Y' ) . "\t$host\t$wiki\t$text"; + + if ( $wgDBerrorLogTZ && !$logDBErrorTimeZoneObject ) { + $logDBErrorTimeZoneObject = new DateTimeZone( $wgDBerrorLogTZ ); + } + + // Workaround for https://bugs.php.net/bug.php?id=52063 + // Can be removed when min PHP > 5.3.2 + if ( $logDBErrorTimeZoneObject === null ) { + $d = date_create( "now" ); + } else { + $d = date_create( "now", $logDBErrorTimeZoneObject ); + } + + $date = $d->format( 'D M j G:i:s T Y' ); + + $text = "$date\t$host\t$wiki\t$text"; wfErrorLog( $text, $wgDBerrorLog ); } } @@ -981,41 +1076,16 @@ function wfLogDBError( $text ) { * Throws a warning that $function is deprecated * * @param $function String - * @param $version String|false: Added in 1.19. - * @param $component String|false: Added in 1.19. - * + * @param $version String|bool: Version of MediaWiki that the function was deprecated in (Added in 1.19). + * @param $component String|bool: Added in 1.19. + * @param $callerOffset integer: How far up the callstack is the original + * caller. 2 = function that called the function that called + * wfDeprecated (Added in 1.20) + * * @return null */ -function wfDeprecated( $function, $version = false, $component = false ) { - static $functionsWarned = array(); - - MWDebug::deprecated( $function, $version, $component ); - - if ( !isset( $functionsWarned[$function] ) ) { - $functionsWarned[$function] = true; - - if ( $version ) { - global $wgDeprecationReleaseLimit; - - if ( $wgDeprecationReleaseLimit && $component === false ) { - # Strip -* off the end of $version so that branches can use the - # format #.##-branchname to avoid issues if the branch is merged into - # a version of MediaWiki later than what it was branched from - $comparableVersion = preg_replace( '/-.*$/', '', $version ); - - # If the comparableVersion is larger than our release limit then - # skip the warning message for the deprecation - if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) { - return; - } - } - - $component = $component === false ? 'MediaWiki' : $component; - wfWarn( "Use of $function was deprecated in $component $version.", 2 ); - } else { - wfWarn( "Use of $function is deprecated.", 2 ); - } - } +function wfDeprecated( $function, $version = false, $component = false, $callerOffset = 2 ) { + MWDebug::deprecated( $function, $version, $component, $callerOffset + 1 ); } /** @@ -1029,34 +1099,7 @@ function wfDeprecated( $function, $version = false, $component = false ) { * is true */ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { - global $wgDevelopmentWarnings; - - MWDebug::warning( $msg, $callerOffset + 2 ); - - $callers = wfDebugBacktrace(); - if ( isset( $callers[$callerOffset + 1] ) ) { - $callerfunc = $callers[$callerOffset + 1]; - $callerfile = $callers[$callerOffset]; - if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) { - $file = $callerfile['file'] . ' at line ' . $callerfile['line']; - } else { - $file = '(internal function)'; - } - $func = ''; - if ( isset( $callerfunc['class'] ) ) { - $func .= $callerfunc['class'] . '::'; - } - if ( isset( $callerfunc['function'] ) ) { - $func .= $callerfunc['function']; - } - $msg .= " [Called from $func in $file]"; - } - - if ( $wgDevelopmentWarnings ) { - trigger_error( $msg, $level ); - } else { - wfDebug( "$msg\n" ); - } + MWDebug::warning( $msg, $callerOffset + 1, $level ); } /** @@ -1177,6 +1220,57 @@ function wfLogProfilingData() { } /** + * Increment a statistics counter + * + * @param $key String + * @param $count Int + */ +function wfIncrStats( $key, $count = 1 ) { + global $wgStatsMethod; + + $count = intval( $count ); + + if( $wgStatsMethod == 'udp' ) { + global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname, $wgAggregateStatsID; + static $socket; + + $id = $wgAggregateStatsID !== false ? $wgAggregateStatsID : $wgDBname; + + if ( !$socket ) { + $socket = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); + $statline = "stats/{$id} - 1 1 1 1 1 -total\n"; + socket_sendto( + $socket, + $statline, + strlen( $statline ), + 0, + $wgUDPProfilerHost, + $wgUDPProfilerPort + ); + } + $statline = "stats/{$id} - {$count} 1 1 1 1 {$key}\n"; + wfSuppressWarnings(); + socket_sendto( + $socket, + $statline, + strlen( $statline ), + 0, + $wgUDPProfilerHost, + $wgUDPProfilerPort + ); + wfRestoreWarnings(); + } elseif( $wgStatsMethod == 'cache' ) { + global $wgMemc; + $key = wfMemcKey( 'stats', $key ); + if ( is_null( $wgMemc->incr( $key, $count ) ) ) { + $wgMemc->add( $key, $count ); + } + } else { + // Disabled + } +} + +/** * Check if the wiki read-only lock file is present. This can be used to lock * off editing functions, but doesn't guarantee that the database will not be * modified. @@ -1247,7 +1341,7 @@ function wfGetLangObj( $langcode = false ) { return $wgLang; } - $validCodes = array_keys( Language::getLanguageNames() ); + $validCodes = array_keys( Language::fetchLanguageNames() ); if( in_array( $langcode, $validCodes ) ) { # $langcode corresponds to a valid language. return Language::factory( $langcode ); @@ -1260,7 +1354,7 @@ function wfGetLangObj( $langcode = false ) { /** * Old function when $wgBetterDirectionality existed - * Removed in core, kept in extensions for backwards compat. + * All usage removed, wfUILang can be removed in near future * * @deprecated since 1.18 * @return Language @@ -1308,6 +1402,8 @@ function wfMessageFallback( /*...*/ ) { * Use wfMsgForContent() instead if the message should NOT * change depending on the user preferences. * + * @deprecated since 1.18 + * * @param $key String: lookup key for the message, usually * defined in languages/Language.php * @@ -1328,6 +1424,8 @@ function wfMsg( $key ) { /** * Same as above except doesn't transform the message * + * @deprecated since 1.18 + * * @param $key String * @return String */ @@ -1356,6 +1454,8 @@ function wfMsgNoTrans( $key ) { * customize potentially hundreds of messages in * order to, e.g., fix a link in every possible language. * + * @deprecated since 1.18 + * * @param $key String: lookup key for the message, usually * defined in languages/Language.php * @return String @@ -1376,6 +1476,8 @@ function wfMsgForContent( $key ) { /** * Same as above except doesn't transform the message * + * @deprecated since 1.18 + * * @param $key String * @return String */ @@ -1395,6 +1497,8 @@ function wfMsgForContentNoTrans( $key ) { /** * Really get a message * + * @deprecated since 1.18 + * * @param $key String: key to get. * @param $args * @param $useDB Boolean @@ -1413,6 +1517,8 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform /** * Fetch a message string value, but don't replace any keys yet. * + * @deprecated since 1.18 + * * @param $key String * @param $useDB Bool * @param $langCode String: Code of the language to get the message for, or @@ -1468,6 +1574,8 @@ function wfMsgReplaceArgs( $message, $args ) { * to pre-escape them if you really do want plaintext, or just wrap * the whole thing in htmlspecialchars(). * + * @deprecated since 1.18 + * * @param $key String * @param string ... parameters * @return string @@ -1485,6 +1593,8 @@ function wfMsgHtml( $key ) { * to pre-escape them if you really do want plaintext, or just wrap * the whole thing in htmlspecialchars(). * + * @deprecated since 1.18 + * * @param $key String * @param string ... parameters * @return string @@ -1500,6 +1610,9 @@ function wfMsgWikiHtml( $key ) { /** * Returns message in the requested format + * + * @deprecated since 1.18 + * * @param $key String: key of the message * @param $options Array: processing rules. Can take the following options: * <i>parse</i>: parses wikitext to HTML @@ -1592,6 +1705,8 @@ function wfMsgExt( $key, $options ) { * looked up didn't exist but a XHTML string, this function checks for the * nonexistance of messages by checking the MessageCache::get() result directly. * + * @deprecated since 1.18. Use Message::isDisabled(). + * * @param $key String: the message key looked up * @return Boolean True if the message *doesn't* exist. */ @@ -1619,6 +1734,15 @@ function wfDebugDieBacktrace( $msg = '' ) { function wfHostname() { static $host; if ( is_null( $host ) ) { + + # Hostname overriding + global $wgOverrideHostname; + if( $wgOverrideHostname !== false ) { + # Set static and skip any detection + $host = $wgOverrideHostname; + return $host; + } + if ( function_exists( 'posix_uname' ) ) { // This function not present on Windows $uname = posix_uname(); @@ -1692,7 +1816,7 @@ function wfDebugBacktrace( $limit = 0 ) { } if ( $limit && version_compare( PHP_VERSION, '5.4.0', '>=' ) ) { - return array_slice( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit ), 1 ); + return array_slice( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit + 1 ), 1 ); } else { return array_slice( debug_backtrace(), 1 ); } @@ -1751,25 +1875,27 @@ function wfBacktrace() { /** * Get the name of the function which called this function + * wfGetCaller( 1 ) is the function with the wfGetCaller() call (ie. __FUNCTION__) + * wfGetCaller( 2 ) [default] is the caller of the function running wfGetCaller() + * wfGetCaller( 3 ) is the parent of that. * * @param $level Int - * @return Bool|string + * @return string */ function wfGetCaller( $level = 2 ) { - $backtrace = wfDebugBacktrace( $level ); + $backtrace = wfDebugBacktrace( $level + 1 ); if ( isset( $backtrace[$level] ) ) { return wfFormatStackFrame( $backtrace[$level] ); } else { - $caller = 'unknown'; + return 'unknown'; } - return $caller; } /** * Return a string consisting of callers in the stack. Useful sometimes * for profiling specific points. * - * @param $limit The maximum depth of the stack frame to return, or false for + * @param $limit int The maximum depth of the stack frame to return, or false for * the entire stack. * @return String */ @@ -1786,7 +1912,7 @@ function wfGetAllCallers( $limit = 3 ) { * Return a string representation of frame * * @param $frame Array - * @return Bool + * @return string */ function wfFormatStackFrame( $frame ) { return isset( $frame['class'] ) ? @@ -1806,13 +1932,7 @@ function wfFormatStackFrame( $frame ) { * @return String */ function wfShowingResults( $offset, $limit ) { - global $wgLang; - return wfMsgExt( - 'showingresults', - array( 'parseinline' ), - $wgLang->formatNum( $limit ), - $wgLang->formatNum( $offset + 1 ) - ); + return wfMessage( 'showingresults' )->numParams( $limit, $offset + 1 )->parse(); } /** @@ -1828,7 +1948,7 @@ function wfShowingResults( $offset, $limit ) { */ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { wfDeprecated( __METHOD__, '1.19' ); - + global $wgLang; $query = wfCgiToArray( $query ); @@ -1856,6 +1976,8 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { * @deprecated since 1.19; use Language::specialList() instead */ function wfSpecialList( $page, $details, $oppositedm = true ) { + wfDeprecated( __METHOD__, '1.19' ); + global $wgLang; return $wgLang->specialList( $page, $details, $oppositedm ); } @@ -1910,7 +2032,7 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { * Escapes the given text so that it may be output using addWikiText() * without any linking, formatting, etc. making its way through. This * is achieved by substituting certain characters with HTML entities. - * As required by the callers, <nowiki> is not used. + * As required by the callers, "<nowiki>" is not used. * * @param $text String: text to be escaped * @return String @@ -1978,7 +2100,7 @@ function wfSetBit( &$dest, $bit, $state = true ) { * A wrapper around the PHP function var_export(). * Either print it or add it to the regular output ($wgOut). * - * @param $var A PHP variable to dump. + * @param $var mixed A PHP variable to dump. */ function wfVarDump( $var ) { global $wgOut; @@ -2057,13 +2179,7 @@ function wfResetOutputBuffers( $resetGzipEncoding = true ) { if( $status['name'] == 'ob_gzhandler' ) { // Reset the 'Content-Encoding' field set by this handler // so we can start fresh. - if ( function_exists( 'header_remove' ) ) { - // Available since PHP 5.3.0 - header_remove( 'Content-Encoding' ); - } else { - // We need to provide a valid content-coding. See bug 28069 - header( 'Content-Encoding: identity' ); - } + header_remove( 'Content-Encoding' ); break; } } @@ -2212,11 +2328,7 @@ function wfSuppressWarnings( $end = false ) { } } else { if ( !$suppressCount ) { - // E_DEPRECATED is undefined in PHP 5.2 - if( !defined( 'E_DEPRECATED' ) ) { - define( 'E_DEPRECATED', 8192 ); - } - $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED ) ); + $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED ) ); } ++$suppressCount; } @@ -2297,118 +2409,13 @@ define( 'TS_ISO_8601_BASIC', 9 ); * @return Mixed: String / false The same date in the format specified in $outputtype or false */ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { - $uts = 0; - $da = array(); - $strtime = ''; - - if ( !$ts ) { // We want to catch 0, '', null... but not date strings starting with a letter. - $uts = time(); - $strtime = "@$uts"; - } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) { - # TS_DB - } elseif ( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) { - # TS_EXIF - } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) { - # TS_MW - } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) { - # TS_UNIX - $uts = $ts; - $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php - } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) { - # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6 - $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", - str_replace( '+00:00', 'UTC', $ts ) ); - } elseif ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) { - # TS_ISO_8601 - } elseif ( preg_match( '/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) { - #TS_ISO_8601_BASIC - } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/', $ts, $da ) ) { - # TS_POSTGRES - } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/', $ts, $da ) ) { - # TS_POSTGRES - } elseif (preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.\d\d\d$/', $ts, $da ) ) { - # TS_DB2 - } elseif ( preg_match( '/^[ \t\r\n]*([A-Z][a-z]{2},[ \t\r\n]*)?' . # Day of week - '\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' . # dd Mon yyyy - '[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d/S', $ts ) ) { # hh:mm:ss - # TS_RFC2822, accepting a trailing comment. See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171 - # The regex is a superset of rfc2822 for readability - $strtime = strtok( $ts, ';' ); - } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) { - # TS_RFC850 - $strtime = $ts; - } elseif ( preg_match( '/^[A-Z][a-z]{2} [A-Z][a-z]{2} +\d{1,2} \d\d:\d\d:\d\d \d{4}/', $ts ) ) { - # asctime - $strtime = $ts; - } else { - # Bogus value... + try { + $timestamp = new MWTimestamp( $ts ); + return $timestamp->getTimestamp( $outputtype ); + } catch( TimestampException $e ) { wfDebug("wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n"); - return false; } - - static $formats = array( - TS_UNIX => 'U', - TS_MW => 'YmdHis', - TS_DB => 'Y-m-d H:i:s', - TS_ISO_8601 => 'Y-m-d\TH:i:s\Z', - TS_ISO_8601_BASIC => 'Ymd\THis\Z', - TS_EXIF => 'Y:m:d H:i:s', // This shouldn't ever be used, but is included for completeness - TS_RFC2822 => 'D, d M Y H:i:s', - TS_ORACLE => 'd-m-Y H:i:s.000000', // Was 'd-M-y h.i.s A' . ' +00:00' before r51500 - TS_POSTGRES => 'Y-m-d H:i:s', - TS_DB2 => 'Y-m-d H:i:s', - ); - - if ( !isset( $formats[$outputtype] ) ) { - throw new MWException( 'wfTimestamp() called with illegal output type.' ); - } - - if ( function_exists( "date_create" ) ) { - if ( count( $da ) ) { - $ds = sprintf("%04d-%02d-%02dT%02d:%02d:%02d.00+00:00", - (int)$da[1], (int)$da[2], (int)$da[3], - (int)$da[4], (int)$da[5], (int)$da[6]); - - $d = date_create( $ds, new DateTimeZone( 'GMT' ) ); - } elseif ( $strtime ) { - $d = date_create( $strtime, new DateTimeZone( 'GMT' ) ); - } else { - return false; - } - - if ( !$d ) { - wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n"); - return false; - } - - $output = $d->format( $formats[$outputtype] ); - } else { - if ( count( $da ) ) { - // Warning! gmmktime() acts oddly if the month or day is set to 0 - // We may want to handle that explicitly at some point - $uts = gmmktime( (int)$da[4], (int)$da[5], (int)$da[6], - (int)$da[2], (int)$da[3], (int)$da[1] ); - } elseif ( $strtime ) { - $uts = strtotime( $strtime ); - } - - if ( $uts === false ) { - wfDebug("wfTimestamp() can't parse the timestamp (non 32-bit time? Update php): $outputtype; $ts\n"); - return false; - } - - if ( TS_UNIX == $outputtype ) { - return $uts; - } - $output = gmdate( $formats[$outputtype], $uts ); - } - - if ( ( $outputtype == TS_RFC2822 ) || ( $outputtype == TS_POSTGRES ) ) { - $output .= ' GMT'; - } - - return $output; } /** @@ -2472,11 +2479,10 @@ function swap( &$x, &$y ) { } /** - * Tries to get the system directory for temporary files. The TMPDIR, TMP, and - * TEMP environment variables are then checked in sequence, and if none are set - * try sys_get_temp_dir() for PHP >= 5.2.1. All else fails, return /tmp for Unix - * or C:\Windows\Temp for Windows and hope for the best. - * It is common to call it with tempnam(). + * Tries to get the system directory for temporary files. First + * $wgTmpDirectory is checked, and then the TMPDIR, TMP, and TEMP + * environment variables are then checked in sequence, and if none are + * set try sys_get_temp_dir(). * * NOTE: When possible, use instead the tmpfile() function to create * temporary files to avoid race conditions on file creation, etc. @@ -2484,17 +2490,20 @@ function swap( &$x, &$y ) { * @return String */ function wfTempDir() { - foreach( array( 'TMPDIR', 'TMP', 'TEMP' ) as $var ) { - $tmp = getenv( $var ); + global $wgTmpDirectory; + + if ( $wgTmpDirectory !== false ) { + return $wgTmpDirectory; + } + + $tmpDir = array_map( "getenv", array( 'TMPDIR', 'TMP', 'TEMP' ) ); + + foreach( $tmpDir as $tmp ) { if( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) { return $tmp; } } - if( function_exists( 'sys_get_temp_dir' ) ) { - return sys_get_temp_dir(); - } - # Usual defaults - return wfIsWindows() ? 'C:\Windows\Temp' : '/tmp'; + return sys_get_temp_dir(); } /** @@ -2509,7 +2518,7 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { global $wgDirectoryMode; if ( FileBackend::isStoragePath( $dir ) ) { // sanity - throw new MWException( __FUNCTION__ . " given storage path `$dir`."); + throw new MWException( __FUNCTION__ . " given storage path '$dir'." ); } if ( !is_null( $caller ) ) { @@ -2533,63 +2542,13 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { if( !$ok ) { // PHP doesn't report the path in its warning message, so add our own to aid in diagnosis. - trigger_error( __FUNCTION__ . ": failed to mkdir \"$dir\" mode $mode", E_USER_WARNING ); + trigger_error( sprintf( "%s: failed to mkdir \"%s\" mode 0%o", __FUNCTION__, $dir, $mode ), + E_USER_WARNING ); } return $ok; } /** - * Increment a statistics counter - * - * @param $key String - * @param $count Int - */ -function wfIncrStats( $key, $count = 1 ) { - global $wgStatsMethod; - - $count = intval( $count ); - - if( $wgStatsMethod == 'udp' ) { - global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname, $wgAggregateStatsID; - static $socket; - - $id = $wgAggregateStatsID !== false ? $wgAggregateStatsID : $wgDBname; - - if ( !$socket ) { - $socket = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); - $statline = "stats/{$id} - {$count} 1 1 1 1 -total\n"; - socket_sendto( - $socket, - $statline, - strlen( $statline ), - 0, - $wgUDPProfilerHost, - $wgUDPProfilerPort - ); - } - $statline = "stats/{$id} - {$count} 1 1 1 1 {$key}\n"; - wfSuppressWarnings(); - socket_sendto( - $socket, - $statline, - strlen( $statline ), - 0, - $wgUDPProfilerHost, - $wgUDPProfilerPort - ); - wfRestoreWarnings(); - } elseif( $wgStatsMethod == 'cache' ) { - global $wgMemc; - $key = wfMemcKey( 'stats', $key ); - if ( is_null( $wgMemc->incr( $key, $count ) ) ) { - $wgMemc->add( $key, $count ); - } - } else { - // Disabled - } -} - -/** * Remove a directory and all its content. * Does not hide error. */ @@ -2686,9 +2645,7 @@ function wfDl( $extension, $fileName = null ) { $canDl = false; $sapi = php_sapi_name(); - if( version_compare( PHP_VERSION, '5.3.0', '<' ) || - $sapi == 'cli' || $sapi == 'cgi' || $sapi == 'embed' ) - { + if( $sapi == 'cli' || $sapi == 'cgi' || $sapi == 'embed' ) { $canDl = ( function_exists( 'dl' ) && is_callable( 'dl' ) && wfIniGetBool( 'enable_dl' ) && !wfIniGetBool( 'safe_mode' ) ); } @@ -2773,13 +2730,15 @@ function wfEscapeShellArg( ) { * Execute a shell command, with time and memory limits mirrored from the PHP * configuration if supported. * @param $cmd String Command line, properly escaped for shell. - * @param &$retval optional, will receive the program's exit code. + * @param &$retval null|Mixed optional, will receive the program's exit code. * (non-zero is usually failure) * @param $environ Array optional environment variables which should be * added to the executed command environment. - * @return collected stdout as a string (trailing newlines stripped) + * @param $limits Array optional array with limits(filesize, memory, time) + * this overwrites the global wgShellMax* limits. + * @return string collected stdout as a string (trailing newlines stripped) */ -function wfShellExec( $cmd, &$retval = null, $environ = array() ) { +function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array() ) { global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime; static $disabled; @@ -2826,19 +2785,10 @@ function wfShellExec( $cmd, &$retval = null, $environ = array() ) { } $cmd = $envcmd . $cmd; - if ( wfIsWindows() ) { - if ( version_compare( PHP_VERSION, '5.3.0', '<' ) && /* Fixed in 5.3.0 :) */ - ( version_compare( PHP_VERSION, '5.2.1', '>=' ) || php_uname( 's' ) == 'Windows NT' ) ) - { - # Hack to work around PHP's flawed invocation of cmd.exe - # http://news.php.net/php.internals/21796 - # Windows 9x doesn't accept any kind of quotes - $cmd = '"' . $cmd . '"'; - } - } elseif ( php_uname( 's' ) == 'Linux' ) { - $time = intval( $wgMaxShellTime ); - $mem = intval( $wgMaxShellMemory ); - $filesize = intval( $wgMaxShellFileSize ); + if ( php_uname( 's' ) == 'Linux' ) { + $time = intval ( isset($limits['time']) ? $limits['time'] : $wgMaxShellTime ); + $mem = intval ( isset($limits['memory']) ? $limits['memory'] : $wgMaxShellMemory ); + $filesize = intval ( isset($limits['filesize']) ? $limits['filesize'] : $wgMaxShellFileSize ); if ( $time > 0 && $mem > 0 ) { $script = "$IP/bin/ulimit4.sh"; @@ -2879,21 +2829,29 @@ function wfInitShellLocale() { } /** - * Generate a shell-escaped command line string to run a maintenance script. + * Alias to wfShellWikiCmd() + * @see wfShellWikiCmd() + */ +function wfShellMaintenanceCmd( $script, array $parameters = array(), array $options = array() ) { + return wfShellWikiCmd( $script, $parameters, $options ); +} + +/** + * Generate a shell-escaped command line string to run a MediaWiki cli script. * Note that $parameters should be a flat array and an option with an argument * should consist of two consecutive items in the array (do not use "--option value"). - * @param $script string MediaWiki maintenance script path + * @param $script string MediaWiki cli script path * @param $parameters Array Arguments and options to the script * @param $options Array Associative array of options: * 'php': The path to the php executable * 'wrapper': Path to a PHP wrapper to handle the maintenance script * @return Array */ -function wfShellMaintenanceCmd( $script, array $parameters = array(), array $options = array() ) { +function wfShellWikiCmd( $script, array $parameters = array(), array $options = array() ) { global $wgPhpCli; // Give site config file a chance to run the script in a wrapper. // The caller may likely want to call wfBasename() on $script. - wfRunHooks( 'wfShellMaintenanceCmd', array( &$script, &$parameters, &$options ) ); + wfRunHooks( 'wfShellWikiCmd', array( &$script, &$parameters, &$options ) ); $cmd = isset( $options['php'] ) ? array( $options['php'] ) : array( $wgPhpCli ); if ( isset( $options['wrapper'] ) ) { $cmd[] = $options['wrapper']; @@ -3096,11 +3054,11 @@ function wfUseMW( $req_ver ) { /** * Return the final portion of a pathname. - * Reimplemented because PHP5's basename() is buggy with multibyte text. + * Reimplemented because PHP5's "basename()" is buggy with multibyte text. * http://bugs.php.net/bug.php?id=33898 * * PHP's basename() only considers '\' a pathchar on Windows and Netware. - * We'll consider it so always, as we don't want \s in our Unix paths either. + * We'll consider it so always, as we don't want '\s' in our Unix paths either. * * @param $path String * @param $suffix String: to remove if present @@ -3294,11 +3252,6 @@ function wfHttpOnlySafe() { /** * Check if there is sufficent entropy in php's built-in session generation - * PHP's built-in session entropy is enabled if: - * - entropy_file is set or you're on Windows with php 5.3.3+ - * - AND entropy_length is > 0 - * We treat it as disabled if it doesn't have an entropy length of at least 32 - * * @return bool true = there is sufficient entropy */ function wfCheckEntropy() { @@ -3319,6 +3272,10 @@ function wfFixSessionID() { return; } + // PHP's built-in session entropy is enabled if: + // - entropy_file is set or you're on Windows with php 5.3.3+ + // - AND entropy_length is > 0 + // We treat it as disabled if it doesn't have an entropy length of at least 32 $entropyEnabled = wfCheckEntropy(); // If built-in entropy is not enabled or not sufficient override php's built in session id generation code @@ -3334,21 +3291,10 @@ function wfFixSessionID() { * @param $sessionId Bool */ function wfSetupSession( $sessionId = false ) { - global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, + global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler; - if( $wgSessionsInMemcached ) { - if ( !defined( 'MW_COMPILED' ) ) { - global $IP; - require_once( "$IP/includes/cache/MemcachedSessions.php" ); - } - session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', - 'memsess_write', 'memsess_destroy', 'memsess_gc' ); - - // It's necessary to register a shutdown function to call session_write_close(), - // because by the time the request shutdown function for the session module is - // called, $wgMemc has already been destroyed. Shutdown functions registered - // this way are called before object destruction. - register_shutdown_function( 'memsess_write_close' ); + if( $wgSessionsInObjectCache || $wgSessionsInMemcached ) { + ObjectCacheSessionHandler::install(); } elseif( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) { # Only set this if $wgSessionHandler isn't null and session.save_handler # hasn't already been set to the desired value (that causes errors) @@ -3507,7 +3453,7 @@ function &wfGetLBFactory() { * Shortcut for RepoGroup::singleton()->findFile() * * @param $title String or Title object - * @param $options Associative array of options: + * @param $options array Associative array of options: * time: requested time for an archived image, or false for the * current version. An image object will be returned which was * created at the specified time. @@ -3531,7 +3477,7 @@ function wfFindFile( $title, $options = array() ) { * Returns a valid placeholder object if the file does not exist. * * @param $title Title|String - * @return File|null A File, or null if passed an invalid Title + * @return LocalFile|null A File, or null if passed an invalid Title */ function wfLocalFile( $title ) { return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); @@ -3563,19 +3509,26 @@ function wfQueriesMustScale() { /** * Get the path to a specified script file, respecting file * extensions; this is a wrapper around $wgScriptExtension etc. + * except for 'index' and 'load' which use $wgScript/$wgLoadScript * * @param $script String: script filename, sans extension * @return String */ function wfScript( $script = 'index' ) { - global $wgScriptPath, $wgScriptExtension; - return "{$wgScriptPath}/{$script}{$wgScriptExtension}"; + global $wgScriptPath, $wgScriptExtension, $wgScript, $wgLoadScript; + if ( $script === 'index' ) { + return $wgScript; + } else if ( $script === 'load' ) { + return $wgLoadScript; + } else { + return "{$wgScriptPath}/{$script}{$wgScriptExtension}"; + } } /** * Get the script URL. * - * @return script URL + * @return string script URL */ function wfGetScriptUrl() { if( isset( $_SERVER['SCRIPT_NAME'] ) ) { @@ -3689,25 +3642,29 @@ function wfCountDown( $n ) { * characters before hashing. * @return string * @codeCoverageIgnore + * @deprecated since 1.20; Please use MWCryptRand for security purposes and wfRandomString for pesudo-random strings + * @warning This method is NOT secure. Additionally it has many callers that use it for pesudo-random purposes. */ function wfGenerateToken( $salt = '' ) { + wfDeprecated( __METHOD__, '1.20' ); $salt = serialize( $salt ); return md5( mt_rand( 0, 0x7fffffff ) . $salt ); } /** * Replace all invalid characters with - + * Additional characters can be defined in $wgIllegalFileChars (see bug 20489) + * By default, $wgIllegalFileChars = ':' * * @param $name Mixed: filename to process * @return String */ function wfStripIllegalFilenameChars( $name ) { global $wgIllegalFileChars; + $illegalFileChars = $wgIllegalFileChars ? "|[" . $wgIllegalFileChars . "]" : ''; $name = wfBaseName( $name ); $name = preg_replace( - "/[^" . Title::legalChars() . "]" . - ( $wgIllegalFileChars ? "|[" . $wgIllegalFileChars . "]" : '' ) . - "/", + "/[^" . Title::legalChars() . "]" . $illegalFileChars . "/", '-', $name ); @@ -3846,6 +3803,16 @@ function wfGetParserCacheStorage() { } /** + * Get the cache object used by the language converter + * + * @return BagOStuff + */ +function wfGetLangConverterCacheStorage() { + global $wgLanguageConverterCacheType; + return ObjectCache::getInstance( $wgLanguageConverterCacheType ); +} + +/** * Call hook functions defined in $wgHooks * * @param $event String: event name @@ -3868,7 +3835,7 @@ function wfRunHooks( $event, $args = array() ) { * because php might make it negative. * * @throws MWException if $data not long enough, or if unpack fails - * @return Associative array of the extracted data + * @return array Associative array of the extracted data */ function wfUnpack( $format, $data, $length=false ) { if ( $length !== false ) { @@ -3891,3 +3858,84 @@ function wfUnpack( $format, $data, $length=false ) { } return $result; } + +/** + * Determine if an image exists on the 'bad image list'. + * + * The format of MediaWiki:Bad_image_list is as follows: + * * Only list items (lines starting with "*") are considered + * * The first link on a line must be a link to a bad image + * * Any subsequent links on the same line are considered to be exceptions, + * i.e. articles where the image may occur inline. + * + * @param $name string the image name to check + * @param $contextTitle Title|bool the page on which the image occurs, if known + * @param $blacklist string wikitext of a file blacklist + * @return bool + */ +function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { + static $badImageCache = null; // based on bad_image_list msg + wfProfileIn( __METHOD__ ); + + # Handle redirects + $redirectTitle = RepoGroup::singleton()->checkRedirect( Title::makeTitle( NS_FILE, $name ) ); + if( $redirectTitle ) { + $name = $redirectTitle->getDbKey(); + } + + # Run the extension hook + $bad = false; + if( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) { + wfProfileOut( __METHOD__ ); + return $bad; + } + + $cacheable = ( $blacklist === null ); + if( $cacheable && $badImageCache !== null ) { + $badImages = $badImageCache; + } else { // cache miss + if ( $blacklist === null ) { + $blacklist = wfMessage( 'bad_image_list' )->inContentLanguage()->plain(); // site list + } + # Build the list now + $badImages = array(); + $lines = explode( "\n", $blacklist ); + foreach( $lines as $line ) { + # List items only + if ( substr( $line, 0, 1 ) !== '*' ) { + continue; + } + + # Find all links + $m = array(); + if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { + continue; + } + + $exceptions = array(); + $imageDBkey = false; + foreach ( $m[1] as $i => $titleText ) { + $title = Title::newFromText( $titleText ); + if ( !is_null( $title ) ) { + if ( $i == 0 ) { + $imageDBkey = $title->getDBkey(); + } else { + $exceptions[$title->getPrefixedDBkey()] = true; + } + } + } + + if ( $imageDBkey !== false ) { + $badImages[$imageDBkey] = $exceptions; + } + } + if ( $cacheable ) { + $badImageCache = $badImages; + } + } + + $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; + $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] ); + wfProfileOut( __METHOD__ ); + return $bad; +} diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 7326bf5c..5c00b9f6 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -1,5 +1,26 @@ <?php /** + * HTML form generation and submission handling. + * + * 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 + */ + +/** * Object handling generic submission, CSRF protection, layout and * other logic for UI forms. in a reusable manner. * @@ -13,6 +34,10 @@ * object, and typically implement at least getInputHTML, which generates * the HTML for the input field to be placed in the table. * + * You can find extensive documentation on the www.mediawiki.org wiki: + * - http://www.mediawiki.org/wiki/HTMLForm + * - http://www.mediawiki.org/wiki/HTMLForm/tutorial + * * The constructor input is an associative array of $fieldname => $info, * where $info is an Associative Array with any of the following: * @@ -30,13 +55,14 @@ * the message. * 'label' -- alternatively, a raw text message. Overridden by * label-message + * 'help' -- message text for a message to use as a help text. * 'help-message' -- message key for a message to use as a help text. * can be an array of msg key and then parameters to * the message. - * Overwrites 'help-messages'. + * Overwrites 'help-messages' and 'help'. * 'help-messages' -- array of message key. As above, each item can * be an array of msg key and then parameters. - * Overwrites 'help-message'. + * Overwrites 'help'. * 'required' -- passed through to the object, indicating that it * is a required field. * 'size' -- the length of text fields @@ -51,6 +77,19 @@ * (eg one without the "wp" prefix), specify it here and * it will be used without modification. * + * Since 1.20, you can chain mutators to ease the form generation: + * @par Example: + * @code + * $form = new HTMLForm( $someFields ); + * $form->setMethod( 'get' ) + * ->setWrapperLegendMsg( 'message-key' ) + * ->suppressReset() + * ->prepareForm() + * ->displayForm(); + * @endcode + * Note that you will have prepareForm and displayForm at the end. Other + * methods call done after that would simply not be part of the form :( + * * TODO: Document 'section' / 'subsection' stuff */ class HTMLForm extends ContextSource { @@ -111,7 +150,7 @@ class HTMLForm extends ContextSource { /** * Form action URL. false means we will use the URL to set Title * @since 1.19 - * @var false|string + * @var bool|string */ protected $mAction = false; @@ -120,17 +159,34 @@ class HTMLForm extends ContextSource { protected $mButtons = array(); protected $mWrapperLegend = false; - + /** * If true, sections that contain both fields and subsections will * render their subsections before their fields. - * + * * Subclasses may set this to false to render subsections after fields * instead. */ protected $mSubSectionBeforeFields = true; /** + * Format in which to display form. For viable options, + * @see $availableDisplayFormats + * @var String + */ + protected $displayFormat = 'table'; + + /** + * Available formats in which to display the form + * @var Array + */ + protected $availableDisplayFormats = array( + 'table', + 'div', + 'raw', + ); + + /** * Build a new HTMLForm from an array of field attributes * @param $descriptor Array of Field constructs, as described above * @param $context IContextSource available since 1.18, will become compulsory in 1.18. @@ -138,13 +194,13 @@ class HTMLForm extends ContextSource { * @param $messagePrefix String a prefix to go in front of default messages */ public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) { - if( $context instanceof IContextSource ){ + if ( $context instanceof IContextSource ) { $this->setContext( $context ); $this->mTitle = false; // We don't need them to set a title $this->mMessagePrefix = $messagePrefix; } else { // B/C since 1.18 - if( is_string( $context ) && $messagePrefix === '' ){ + if ( is_string( $context ) && $messagePrefix === '' ) { // it's actually $messagePrefix $this->mMessagePrefix = $context; } @@ -189,6 +245,30 @@ class HTMLForm extends ContextSource { } /** + * Set format in which to display the form + * @param $format String the name of the format to use, must be one of + * $this->availableDisplayFormats + * @since 1.20 + * @return HTMLForm $this for chaining calls (since 1.20) + */ + public function setDisplayFormat( $format ) { + if ( !in_array( $format, $this->availableDisplayFormats ) ) { + throw new MWException ( 'Display format must be one of ' . print_r( $this->availableDisplayFormats, true ) ); + } + $this->displayFormat = $format; + return $this; + } + + /** + * Getter for displayFormat + * @since 1.20 + * @return String + */ + public function getDisplayFormat() { + return $this->displayFormat; + } + + /** * Add the HTMLForm-specific JavaScript, if it hasn't been * done already. * @deprecated since 1.18 load modules with ResourceLoader instead @@ -217,13 +297,22 @@ class HTMLForm extends ContextSource { $descriptor['fieldname'] = $fieldname; + # TODO + # This will throw a fatal error whenever someone try to use + # 'class' to feed a CSS class instead of 'cssclass'. Would be + # great to avoid the fatal error and show a nice error. $obj = new $class( $descriptor ); return $obj; } /** - * Prepare form for submission + * Prepare form for submission. + * + * @attention When doing method chaining, that should be the very last + * method call before displayForm(). + * + * @return HTMLForm $this for chaining calls (since 1.20) */ function prepareForm() { # Check if we have the info we need @@ -233,6 +322,7 @@ class HTMLForm extends ContextSource { # Load data from the request. $this->loadData(); + return $this; } /** @@ -249,7 +339,7 @@ class HTMLForm extends ContextSource { $editToken = $this->getRequest()->getVal( 'wpEditToken' ); if ( $this->getUser()->isLoggedIn() || $editToken != null ) { // Session tokens for logged-out users have no security value. - // However, if the user gave one, check it in order to give a nice + // However, if the user gave one, check it in order to give a nice // "session expired" error instead of "permission denied" or such. $submit = $this->getUser()->matchEditToken( $editToken ); } else { @@ -266,7 +356,7 @@ class HTMLForm extends ContextSource { /** * The here's-one-I-made-earlier option: do the submission if - * posted, or display the form with or without funky valiation + * posted, or display the form with or without funky validation * errors * @return Bool or Status whether submission was successful. */ @@ -274,7 +364,7 @@ class HTMLForm extends ContextSource { $this->prepareForm(); $result = $this->tryAuthorizedSubmit(); - if ( $result === true || ( $result instanceof Status && $result->isGood() ) ){ + if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) { return $result; } @@ -307,6 +397,9 @@ class HTMLForm extends ContextSource { } $callback = $this->mSubmitCallback; + if ( !is_callable( $callback ) ) { + throw new MWException( 'HTMLForm: no submit callback provided. Use setSubmitCallback() to set one.' ); + } $data = $this->filterDataForSubmit( $this->mFieldData ); @@ -322,45 +415,60 @@ class HTMLForm extends ContextSource { * the output from HTMLForm::filterDataForSubmit, and must * return Bool true on success, Bool false if no submission * was attempted, or String HTML output to display on error. + * @return HTMLForm $this for chaining calls (since 1.20) */ function setSubmitCallback( $cb ) { $this->mSubmitCallback = $cb; + return $this; } /** * Set a message to display on a validation error. - * @param $msg Mixed String or Array of valid inputs to wfMsgExt() + * @param $msg Mixed String or Array of valid inputs to wfMessage() * (so each entry can be either a String or Array) + * @return HTMLForm $this for chaining calls (since 1.20) */ function setValidationErrorMessage( $msg ) { $this->mValidationErrorMessage = $msg; + return $this; } /** * Set the introductory message, overwriting any existing message. * @param $msg String complete text of message to display + * @return HTMLForm $this for chaining calls (since 1.20) */ function setIntro( $msg ) { $this->setPreText( $msg ); + return $this; } /** * Set the introductory message, overwriting any existing message. * @since 1.19 * @param $msg String complete text of message to display + * @return HTMLForm $this for chaining calls (since 1.20) */ - function setPreText( $msg ) { $this->mPre = $msg; } + function setPreText( $msg ) { + $this->mPre = $msg; + return $this; + } /** * Add introductory text. * @param $msg String complete text of message to display + * @return HTMLForm $this for chaining calls (since 1.20) */ - function addPreText( $msg ) { $this->mPre .= $msg; } + function addPreText( $msg ) { + $this->mPre .= $msg; + return $this; + } /** * Add header text, inside the form. * @param $msg String complete text of message to display * @param $section string The section to add the header to + * @return HTMLForm $this for chaining calls (since 1.20) */ function addHeaderText( $msg, $section = null ) { if ( is_null( $section ) ) { @@ -371,6 +479,7 @@ class HTMLForm extends ContextSource { } $this->mSectionHeaders[$section] .= $msg; } + return $this; } /** @@ -378,6 +487,7 @@ class HTMLForm extends ContextSource { * @since 1.19 * @param $msg String complete text of message to display * @param $section The section to add the header to + * @return HTMLForm $this for chaining calls (since 1.20) */ function setHeaderText( $msg, $section = null ) { if ( is_null( $section ) ) { @@ -385,12 +495,14 @@ class HTMLForm extends ContextSource { } else { $this->mSectionHeaders[$section] = $msg; } + return $this; } /** * Add footer text, inside the form. * @param $msg String complete text of message to display * @param $section string The section to add the footer text to + * @return HTMLForm $this for chaining calls (since 1.20) */ function addFooterText( $msg, $section = null ) { if ( is_null( $section ) ) { @@ -401,6 +513,7 @@ class HTMLForm extends ContextSource { } $this->mSectionFooters[$section] .= $msg; } + return $this; } /** @@ -408,6 +521,7 @@ class HTMLForm extends ContextSource { * @since 1.19 * @param $msg String complete text of message to display * @param $section string The section to add the footer text to + * @return HTMLForm $this for chaining calls (since 1.20) */ function setFooterText( $msg, $section = null ) { if ( is_null( $section ) ) { @@ -415,39 +529,65 @@ class HTMLForm extends ContextSource { } else { $this->mSectionFooters[$section] = $msg; } + return $this; } /** * Add text to the end of the display. * @param $msg String complete text of message to display + * @return HTMLForm $this for chaining calls (since 1.20) */ - function addPostText( $msg ) { $this->mPost .= $msg; } + function addPostText( $msg ) { + $this->mPost .= $msg; + return $this; + } /** * Set text at the end of the display. * @param $msg String complete text of message to display + * @return HTMLForm $this for chaining calls (since 1.20) */ - function setPostText( $msg ) { $this->mPost = $msg; } + function setPostText( $msg ) { + $this->mPost = $msg; + return $this; + } /** * Add a hidden field to the output * @param $name String field name. This will be used exactly as entered * @param $value String field value * @param $attribs Array + * @return HTMLForm $this for chaining calls (since 1.20) */ public function addHiddenField( $name, $value, $attribs = array() ) { $attribs += array( 'name' => $name ); $this->mHiddenFields[] = array( $value, $attribs ); + return $this; } + /** + * Add a button to the form + * @param $name String field name. + * @param $value String field value + * @param $id String DOM id for the button (default: null) + * @param $attribs Array + * @return HTMLForm $this for chaining calls (since 1.20) + */ public function addButton( $name, $value, $id = null, $attribs = null ) { $this->mButtons[] = compact( 'name', 'value', 'id', 'attribs' ); + return $this; } /** * Display the form (sending to $wgOut), with an appropriate error * message or stack of messages, and any validation errors, etc. + * + * @attention You should call prepareForm() before calling this function. + * Moreover, when doing method chaining this should be the very last method + * call just after prepareForm(). + * * @param $submitResult Mixed output from HTMLForm::trySubmit() + * @return Nothing, should be last call */ function displayForm( $submitResult ) { $this->getOutput()->addHTML( $this->getHTML( $submitResult ) ); @@ -478,7 +618,7 @@ class HTMLForm extends ContextSource { } /** - * Wrap the form innards in an actual <form> element + * Wrap the form innards in an actual "<form>" element * @param $html String HTML contents to wrap. * @return String wrapped HTML. */ @@ -511,15 +651,15 @@ class HTMLForm extends ContextSource { * @return String HTML. */ function getHiddenFields() { - global $wgUsePathInfo; + global $wgArticlePath; $html = ''; - if( $this->getMethod() == 'post' ){ + if ( $this->getMethod() == 'post' ) { $html .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; } - if ( !$wgUsePathInfo && $this->getMethod() == 'get' ) { + if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() == 'get' ) { $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; } @@ -560,7 +700,7 @@ class HTMLForm extends ContextSource { 'input', array( 'type' => 'reset', - 'value' => wfMsg( 'htmlform-reset' ) + 'value' => $this->msg( 'htmlform-reset' )->text() ) ) . "\n"; } @@ -620,7 +760,7 @@ class HTMLForm extends ContextSource { /** * Format a stack of error messages into a single HTML string * @param $errors Array of message keys/values - * @return String HTML, a <ul> list of errors + * @return String HTML, a "<ul>" list of errors */ public static function formatErrors( $errors ) { $errorstr = ''; @@ -636,7 +776,7 @@ class HTMLForm extends ContextSource { $errorstr .= Html::rawElement( 'li', array(), - wfMsgExt( $msg, array( 'parseinline' ), $error ) + wfMessage( $msg, $error )->parse() ); } @@ -648,84 +788,115 @@ class HTMLForm extends ContextSource { /** * Set the text for the submit button * @param $t String plaintext. + * @return HTMLForm $this for chaining calls (since 1.20) */ function setSubmitText( $t ) { $this->mSubmitText = $t; + return $this; } /** * Set the text for the submit button to a message * @since 1.19 * @param $msg String message key + * @return HTMLForm $this for chaining calls (since 1.20) */ public function setSubmitTextMsg( $msg ) { - return $this->setSubmitText( $this->msg( $msg )->escaped() ); + $this->setSubmitText( $this->msg( $msg )->text() ); + return $this; } /** * Get the text for the submit button, either customised or a default. - * @return unknown_type + * @return string */ function getSubmitText() { return $this->mSubmitText ? $this->mSubmitText - : wfMsg( 'htmlform-submit' ); + : $this->msg( 'htmlform-submit' )->text(); } + /** + * @param $name String Submit button name + * @return HTMLForm $this for chaining calls (since 1.20) + */ public function setSubmitName( $name ) { $this->mSubmitName = $name; + return $this; } + /** + * @param $name String Tooltip for the submit button + * @return HTMLForm $this for chaining calls (since 1.20) + */ public function setSubmitTooltip( $name ) { $this->mSubmitTooltip = $name; + return $this; } /** * Set the id for the submit button. * @param $t String. * @todo FIXME: Integrity of $t is *not* validated + * @return HTMLForm $this for chaining calls (since 1.20) */ function setSubmitID( $t ) { $this->mSubmitID = $t; + return $this; } + /** + * @param $id String DOM id for the form + * @return HTMLForm $this for chaining calls (since 1.20) + */ public function setId( $id ) { $this->mId = $id; + return $this; } /** - * Prompt the whole form to be wrapped in a <fieldset>, with - * this text as its <legend> element. - * @param $legend String HTML to go inside the <legend> element. + * Prompt the whole form to be wrapped in a "<fieldset>", with + * this text as its "<legend>" element. + * @param $legend String HTML to go inside the "<legend>" element. * Will be escaped + * @return HTMLForm $this for chaining calls (since 1.20) */ - public function setWrapperLegend( $legend ) { $this->mWrapperLegend = $legend; } + public function setWrapperLegend( $legend ) { + $this->mWrapperLegend = $legend; + return $this; + } /** - * Prompt the whole form to be wrapped in a <fieldset>, with - * this message as its <legend> element. + * Prompt the whole form to be wrapped in a "<fieldset>", with + * this message as its "<legend>" element. * @since 1.19 * @param $msg String message key + * @return HTMLForm $this for chaining calls (since 1.20) */ public function setWrapperLegendMsg( $msg ) { - return $this->setWrapperLegend( $this->msg( $msg )->escaped() ); + $this->setWrapperLegend( $this->msg( $msg )->text() ); + return $this; } /** * Set the prefix for various default messages - * TODO: currently only used for the <fieldset> legend on forms + * @todo currently only used for the "<fieldset>" legend on forms * with multiple sections; should be used elsewhre? * @param $p String + * @return HTMLForm $this for chaining calls (since 1.20) */ function setMessagePrefix( $p ) { $this->mMessagePrefix = $p; + return $this; } /** * Set the title for form submission * @param $t Title of page the form is on/should be posted to + * @return HTMLForm $this for chaining calls (since 1.20) */ function setTitle( $t ) { $this->mTitle = $t; + return $this; } /** @@ -741,36 +912,43 @@ class HTMLForm extends ContextSource { /** * Set the method used to submit the form * @param $method String + * @return HTMLForm $this for chaining calls (since 1.20) */ - public function setMethod( $method='post' ){ + public function setMethod( $method = 'post' ) { $this->mMethod = $method; + return $this; } - public function getMethod(){ + public function getMethod() { return $this->mMethod; } /** - * TODO: Document + * @todo Document * @param $fields array[]|HTMLFormField[] array of fields (either arrays or objects) - * @param $sectionName string ID attribute of the <table> tag for this section, ignored if empty - * @param $fieldsetIDPrefix string ID prefix for the <fieldset> tag of each subsection, ignored if empty + * @param $sectionName string ID attribute of the "<table>" tag for this section, ignored if empty + * @param $fieldsetIDPrefix string ID prefix for the "<fieldset>" tag of each subsection, ignored if empty * @return String */ - function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) { - $tableHtml = ''; + public function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) { + $displayFormat = $this->getDisplayFormat(); + + $html = ''; $subsectionHtml = ''; - $hasLeftColumn = false; + $hasLabel = false; + + $getFieldHtmlMethod = ( $displayFormat == 'table' ) ? 'getTableRow' : 'get' . ucfirst( $displayFormat ); foreach ( $fields as $key => $value ) { - if ( is_object( $value ) ) { + if ( $value instanceof HTMLFormField ) { $v = empty( $value->mParams['nodata'] ) ? $this->mFieldData[$key] : $value->getDefault(); - $tableHtml .= $value->getTableRow( $v ); + $html .= $value->$getFieldHtmlMethod( $v ); - if ( $value->getLabel() != ' ' ) { - $hasLeftColumn = true; + $labelValue = trim( $value->getLabel() ); + if ( $labelValue != ' ' && $labelValue !== '' ) { + $hasLabel = true; } } elseif ( is_array( $value ) ) { $section = $this->displaySection( $value, $key ); @@ -789,27 +967,33 @@ class HTMLForm extends ContextSource { } } - $classes = array(); + if ( $displayFormat !== 'raw' ) { + $classes = array(); - if ( !$hasLeftColumn ) { // Avoid strange spacing when no labels exist - $classes[] = 'mw-htmlform-nolabel'; - } + if ( !$hasLabel ) { // Avoid strange spacing when no labels exist + $classes[] = 'mw-htmlform-nolabel'; + } - $attribs = array( - 'class' => implode( ' ', $classes ), - ); + $attribs = array( + 'class' => implode( ' ', $classes ), + ); - if ( $sectionName ) { - $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" ); - } + if ( $sectionName ) { + $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" ); + } - $tableHtml = Html::rawElement( 'table', $attribs, - Html::rawElement( 'tbody', array(), "\n$tableHtml\n" ) ) . "\n"; + if ( $displayFormat === 'table' ) { + $html = Html::rawElement( 'table', $attribs, + Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n"; + } elseif ( $displayFormat === 'div' ) { + $html = Html::rawElement( 'div', $attribs, "\n$html\n" ); + } + } if ( $this->mSubSectionBeforeFields ) { - return $subsectionHtml . "\n" . $tableHtml; + return $subsectionHtml . "\n" . $html; } else { - return $tableHtml . "\n" . $subsectionHtml; + return $html . "\n" . $subsectionHtml; } } @@ -842,9 +1026,11 @@ class HTMLForm extends ContextSource { * Stop a reset button being shown for this form * @param $suppressReset Bool set to false to re-enable the * button again + * @return HTMLForm $this for chaining calls (since 1.20) */ function suppressReset( $suppressReset = true ) { $this->mShowReset = !$suppressReset; + return $this; } /** @@ -852,20 +1038,20 @@ class HTMLForm extends ContextSource { * to the form as a whole, after it's submitted but before it's * processed. * @param $data - * @return unknown_type + * @return */ function filterDataForSubmit( $data ) { return $data; } /** - * Get a string to go in the <legend> of a section fieldset. Override this if you - * want something more complicated + * Get a string to go in the "<legend>" of a section fieldset. + * Override this if you want something more complicated. * @param $key String * @return String */ public function getLegend( $key ) { - return wfMsg( "{$this->mMessagePrefix}-$key" ); + return $this->msg( "{$this->mMessagePrefix}-$key" )->text(); } /** @@ -874,10 +1060,12 @@ class HTMLForm extends ContextSource { * * @since 1.19 * - * @param string|false $action + * @param string|bool $action + * @return HTMLForm $this for chaining calls (since 1.20) */ public function setAction( $action ) { $this->mAction = $action; + return $this; } } @@ -913,6 +1101,28 @@ abstract class HTMLFormField { abstract function getInputHTML( $value ); /** + * Get a translated interface message + * + * This is a wrapper arround $this->mParent->msg() if $this->mParent is set + * and wfMessage() otherwise. + * + * Parameters are the same as wfMessage(). + * + * @return Message object + */ + function msg() { + $args = func_get_args(); + + if ( $this->mParent ) { + $callback = array( $this->mParent, 'msg' ); + } else { + $callback = 'wfMessage'; + } + + return call_user_func_array( $callback, $args ); + } + + /** * Override this function to add specific validation checks on the * field input. Don't forget to call parent::validate() to ensure * that the user-defined callback mValidationCallback is still run @@ -921,8 +1131,8 @@ abstract class HTMLFormField { * @return Mixed Bool true on success, or String error to display. */ function validate( $value, $alldata ) { - if ( isset( $this->mParams['required'] ) && $value === '' ) { - return wfMsgExt( 'htmlform-required', 'parseinline' ); + if ( isset( $this->mParams['required'] ) && $this->mParams['required'] !== false && $value === '' ) { + return $this->msg( 'htmlform-required' )->parse(); } if ( isset( $this->mValidationCallback ) ) { @@ -982,7 +1192,7 @@ abstract class HTMLFormField { $msgInfo = array(); } - $this->mLabel = wfMsgExt( $msg, 'parseinline', $msgInfo ); + $this->mLabel = wfMessage( $msg, $msgInfo )->parse(); } elseif ( isset( $params['label'] ) ) { $this->mLabel = $params['label']; } @@ -1026,7 +1236,7 @@ abstract class HTMLFormField { $this->mFilterCallback = $params['filter-callback']; } - if ( isset( $params['flatlist'] ) ){ + if ( isset( $params['flatlist'] ) ) { $this->mClass .= ' mw-htmlform-flatlist'; } } @@ -1038,35 +1248,27 @@ abstract class HTMLFormField { * @return String complete HTML table row. */ function getTableRow( $value ) { - # Check for invalid data. - - $errors = $this->validate( $value, $this->mParent->mFieldData ); - + list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); + $inputHtml = $this->getInputHTML( $value ); + $fieldType = get_class( $this ); + $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); $cellAttributes = array(); - $verticalLabel = false; - if ( !empty($this->mParams['vertical-label']) ) { + if ( !empty( $this->mParams['vertical-label'] ) ) { $cellAttributes['colspan'] = 2; $verticalLabel = true; - } - - if ( $errors === true || ( !$this->mParent->getRequest()->wasPosted() && ( $this->mParent->getMethod() == 'post' ) ) ) { - $errors = ''; - $errorClass = ''; } else { - $errors = self::formatErrors( $errors ); - $errorClass = 'mw-htmlform-invalid-input'; + $verticalLabel = false; } $label = $this->getLabelHtml( $cellAttributes ); + $field = Html::rawElement( 'td', array( 'class' => 'mw-input' ) + $cellAttributes, - $this->getInputHTML( $value ) . "\n$errors" + $inputHtml . "\n$errors" ); - $fieldType = get_class( $this ); - if ( $verticalLabel ) { $html = Html::rawElement( 'tr', array( 'class' => 'mw-htmlform-vertical-label' ), $label ); @@ -1079,40 +1281,159 @@ abstract class HTMLFormField { $label . $field ); } + return $html . $helptext; + } + + /** + * Get the complete div for the input, including help text, + * labels, and whatever. + * @since 1.20 + * @param $value String the value to set the input to. + * @return String complete HTML table row. + */ + public function getDiv( $value ) { + list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); + $inputHtml = $this->getInputHTML( $value ); + $fieldType = get_class( $this ); + $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() ); + $cellAttributes = array(); + $label = $this->getLabelHtml( $cellAttributes ); + + $field = Html::rawElement( + 'div', + array( 'class' => 'mw-input' ) + $cellAttributes, + $inputHtml . "\n$errors" + ); + $html = Html::rawElement( 'div', + array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), + $label . $field ); + $html .= $helptext; + return $html; + } + + /** + * Get the complete raw fields for the input, including help text, + * labels, and whatever. + * @since 1.20 + * @param $value String the value to set the input to. + * @return String complete HTML table row. + */ + public function getRaw( $value ) { + list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); + $inputHtml = $this->getInputHTML( $value ); + $fieldType = get_class( $this ); + $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() ); + $cellAttributes = array(); + $label = $this->getLabelHtml( $cellAttributes ); + + $html = "\n$errors"; + $html .= $label; + $html .= $inputHtml; + $html .= $helptext; + return $html; + } + + /** + * Generate help text HTML in table format + * @since 1.20 + * @param $helptext String|null + * @return String + */ + public function getHelpTextHtmlTable( $helptext ) { + if ( is_null( $helptext ) ) { + return ''; + } + + $row = Html::rawElement( + 'td', + array( 'colspan' => 2, 'class' => 'htmlform-tip' ), + $helptext + ); + $row = Html::rawElement( 'tr', array(), $row ); + return $row; + } + + /** + * Generate help text HTML in div format + * @since 1.20 + * @param $helptext String|null + * @return String + */ + public function getHelpTextHtmlDiv( $helptext ) { + if ( is_null( $helptext ) ) { + return ''; + } + + $div = Html::rawElement( 'div', array( 'class' => 'htmlform-tip' ), $helptext ); + return $div; + } + + /** + * Generate help text HTML formatted for raw output + * @since 1.20 + * @param $helptext String|null + * @return String + */ + public function getHelpTextHtmlRaw( $helptext ) { + return $this->getHelpTextHtmlDiv( $helptext ); + } + + /** + * Determine the help text to display + * @since 1.20 + * @return String + */ + public function getHelpText() { $helptext = null; if ( isset( $this->mParams['help-message'] ) ) { - $msg = wfMessage( $this->mParams['help-message'] ); - if ( $msg->exists() ) { - $helptext = $msg->parse(); - } - } elseif ( isset( $this->mParams['help-messages'] ) ) { - # help-message can be passed a message key (string) or an array containing - # a message key and additional parameters. This makes it impossible to pass - # an array of message key - foreach( $this->mParams['help-messages'] as $name ) { - $msg = wfMessage( $name ); - if( $msg->exists() ) { - $helptext .= $msg->parse(); // append message + $this->mParams['help-messages'] = array( $this->mParams['help-message'] ); + } + + if ( isset( $this->mParams['help-messages'] ) ) { + foreach ( $this->mParams['help-messages'] as $name ) { + $helpMessage = (array)$name; + $msg = $this->msg( array_shift( $helpMessage ), $helpMessage ); + + if ( $msg->exists() ) { + if ( is_null( $helptext ) ) { + $helptext = ''; + } else { + $helptext .= $this->msg( 'word-separator' )->escaped(); // some space + } + $helptext .= $msg->parse(); // Append message } } - } elseif ( isset( $this->mParams['help'] ) ) { + } + elseif ( isset( $this->mParams['help'] ) ) { $helptext = $this->mParams['help']; } + return $helptext; + } - if ( !is_null( $helptext ) ) { - $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ), - $helptext ); - $row = Html::rawElement( 'tr', array(), $row ); - $html .= "$row\n"; - } + /** + * Determine form errors to display and their classes + * @since 1.20 + * @param $value String the value of the input + * @return Array + */ + public function getErrorsAndErrorClass( $value ) { + $errors = $this->validate( $value, $this->mParent->mFieldData ); - return $html; + if ( $errors === true || ( !$this->mParent->getRequest()->wasPosted() && ( $this->mParent->getMethod() == 'post' ) ) ) { + $errors = ''; + $errorClass = ''; + } else { + $errors = self::formatErrors( $errors ); + $errorClass = 'mw-htmlform-invalid-input'; + } + return array( $errors, $errorClass ); } function getLabel() { return $this->mLabel; } + function getLabelHtml( $cellAttributes = array() ) { # Don't output a for= attribute for labels with no associated input. # Kind of hacky here, possibly we don't want these to be <label>s at all. @@ -1122,9 +1443,20 @@ abstract class HTMLFormField { $for['for'] = $this->mID; } - return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes, - Html::rawElement( 'label', $for, $this->getLabel() ) - ); + $displayFormat = $this->mParent->getDisplayFormat(); + $labelElement = Html::rawElement( 'label', $for, $this->getLabel() ); + + if ( $displayFormat == 'table' ) { + return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes, + Html::rawElement( 'label', $for, $this->getLabel() ) + ); + } elseif ( $displayFormat == 'div' ) { + return Html::rawElement( 'div', array( 'class' => 'mw-label' ) + $cellAttributes, + Html::rawElement( 'label', $for, $this->getLabel() ) + ); + } else { + return $labelElement; + } } function getDefault() { @@ -1149,7 +1481,7 @@ abstract class HTMLFormField { /** * flatten an array of options to a single array, for instance, - * a set of <options> inside <optgroups>. + * a set of "<options>" inside "<optgroups>". * @param $options array Associative Array with values either Strings * or Arrays * @return Array flattened input @@ -1216,10 +1548,6 @@ class HTMLTextField extends HTMLFormField { if ( $this->mClass !== '' ) { $attribs['class'] = $this->mClass; } - - if ( isset( $this->mParams['maxlength'] ) ) { - $attribs['maxlength'] = $this->mParams['maxlength']; - } if ( !empty( $this->mParams['disabled'] ) ) { $attribs['disabled'] = 'disabled'; @@ -1227,8 +1555,9 @@ class HTMLTextField extends HTMLFormField { # TODO: Enforce pattern, step, required, readonly on the server side as # well - foreach ( array( 'min', 'max', 'pattern', 'title', 'step', - 'placeholder' ) as $param ) { + $allowedParams = array( 'min', 'max', 'pattern', 'title', 'step', + 'placeholder', 'list', 'maxlength' ); + foreach ( $allowedParams as $param ) { if ( isset( $this->mParams[$param] ) ) { $attribs[$param] = $this->mParams[$param]; } @@ -1290,7 +1619,7 @@ class HTMLTextAreaField extends HTMLFormField { if ( $this->mClass !== '' ) { $attribs['class'] = $this->mClass; } - + if ( !empty( $this->mParams['disabled'] ) ) { $attribs['disabled'] = 'disabled'; } @@ -1299,6 +1628,10 @@ class HTMLTextAreaField extends HTMLFormField { $attribs['readonly'] = 'readonly'; } + if ( isset( $this->mParams['placeholder'] ) ) { + $attribs['placeholder'] = $this->mParams['placeholder']; + } + foreach ( array( 'required', 'autofocus' ) as $param ) { if ( isset( $this->mParams[$param] ) ) { $attribs[$param] = ''; @@ -1331,7 +1664,7 @@ class HTMLFloatField extends HTMLTextField { # http://dev.w3.org/html5/spec/common-microsyntaxes.html#real-numbers # with the addition that a leading '+' sign is ok. if ( !preg_match( '/^((\+|\-)?\d+(\.\d+)?(E(\+|\-)?\d+)?)?$/i', $value ) ) { - return wfMsgExt( 'htmlform-float-invalid', 'parse' ); + return $this->msg( 'htmlform-float-invalid' )->parseAsBlock(); } # The "int" part of these message names is rather confusing. @@ -1340,7 +1673,7 @@ class HTMLFloatField extends HTMLTextField { $min = $this->mParams['min']; if ( $min > $value ) { - return wfMsgExt( 'htmlform-int-toolow', 'parse', array( $min ) ); + return $this->msg( 'htmlform-int-toolow', $min )->parseAsBlock(); } } @@ -1348,7 +1681,7 @@ class HTMLFloatField extends HTMLTextField { $max = $this->mParams['max']; if ( $max < $value ) { - return wfMsgExt( 'htmlform-int-toohigh', 'parse', array( $max ) ); + return $this->msg( 'htmlform-int-toohigh', $max )->parseAsBlock(); } } @@ -1375,7 +1708,7 @@ class HTMLIntField extends HTMLFloatField { # value to, eg, save in the DB, clean it up with intval(). if ( !preg_match( '/^((\+|\-)?\d+)?$/', trim( $value ) ) ) { - return wfMsgExt( 'htmlform-int-invalid', 'parse' ); + return $this->msg( 'htmlform-int-invalid' )->parseAsBlock(); } return true; @@ -1397,7 +1730,7 @@ class HTMLCheckField extends HTMLFormField { if ( !empty( $this->mParams['disabled'] ) ) { $attr['disabled'] = 'disabled'; } - + if ( $this->mClass !== '' ) { $attr['class'] = $this->mClass; } @@ -1429,7 +1762,7 @@ class HTMLCheckField extends HTMLFormField { // Fetch the value in either one of the two following case: // - we have a valid token (form got posted or GET forged by the user) // - checkbox name has a value (false or true), ie is not null - if ( $request->getCheck( 'wpEditToken' ) || $request->getVal( $this->mName )!== null ) { + if ( $request->getCheck( 'wpEditToken' ) || $request->getVal( $this->mName ) !== null ) { // XOR has the following truth table, which is what we want // INVERT VALUE | OUTPUT // true true | false @@ -1459,7 +1792,7 @@ class HTMLSelectField extends HTMLFormField { if ( in_array( $value, $validOptions ) ) return true; else - return wfMsgExt( 'htmlform-select-badoption', 'parseinline' ); + return $this->msg( 'htmlform-select-badoption' )->parse(); } function getInputHTML( $value ) { @@ -1468,8 +1801,8 @@ class HTMLSelectField extends HTMLFormField { # If one of the options' 'name' is int(0), it is automatically selected. # because PHP sucks and thinks int(0) == 'some string'. # Working around this by forcing all of them to strings. - foreach( $this->mParams['options'] as &$opt ){ - if( is_int( $opt ) ){ + foreach ( $this->mParams['options'] as &$opt ) { + if ( is_int( $opt ) ) { $opt = strval( $opt ); } } @@ -1478,7 +1811,7 @@ class HTMLSelectField extends HTMLFormField { if ( !empty( $this->mParams['disabled'] ) ) { $select->setAttribute( 'disabled', 'disabled' ); } - + if ( $this->mClass !== '' ) { $select->setAttribute( 'class', $this->mClass ); } @@ -1497,7 +1830,9 @@ class HTMLSelectOrOtherField extends HTMLTextField { function __construct( $params ) { if ( !in_array( 'other', $params['options'], true ) ) { - $msg = isset( $params['other'] ) ? $params['other'] : wfMsg( 'htmlform-selectorother-other' ); + $msg = isset( $params['other'] ) ? + $params['other'] : + wfMessage( 'htmlform-selectorother-other' )->text(); $params['options'][$msg] = 'other'; } @@ -1543,7 +1878,7 @@ class HTMLSelectOrOtherField extends HTMLTextField { if ( isset( $this->mParams['maxlength'] ) ) { $tbAttribs['maxlength'] = $this->mParams['maxlength']; } - + if ( $this->mClass !== '' ) { $tbAttribs['class'] = $this->mClass; } @@ -1601,7 +1936,7 @@ class HTMLMultiSelectField extends HTMLFormField { if ( count( $validValues ) == count( $value ) ) { return true; } else { - return wfMsgExt( 'htmlform-select-badoption', 'parseinline' ); + return $this->msg( 'htmlform-select-badoption' )->parse(); } } @@ -1646,7 +1981,7 @@ class HTMLMultiSelectField extends HTMLFormField { */ function loadDataFromRequest( $request ) { if ( $this->mParent->getMethod() == 'post' ) { - if( $request->wasPosted() ){ + if ( $request->wasPosted() ) { # Checkboxes are just not added to the request arrays if they're not checked, # so it's perfectly possible for there not to be an entry at all return $request->getArray( $this->mName, array() ); @@ -1684,7 +2019,7 @@ class HTMLMultiSelectField extends HTMLFormField { * ** <option value> * * New Optgroup header * Plus a text field underneath for an additional reason. The 'value' of the field is - * ""<select>: <extra reason>"", or "<extra reason>" if nothing has been selected in the + * "<select>: <extra reason>", or "<extra reason>" if nothing has been selected in the * select dropdown. * @todo FIXME: If made 'required', only the text field should be compulsory. */ @@ -1692,7 +2027,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField { function __construct( $params ) { if ( array_key_exists( 'other', $params ) ) { - } elseif( array_key_exists( 'other-message', $params ) ){ + } elseif ( array_key_exists( 'other-message', $params ) ) { $params['other'] = wfMessage( $params['other-message'] )->plain(); } else { $params['other'] = null; @@ -1700,7 +2035,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField { if ( array_key_exists( 'options', $params ) ) { # Options array already specified - } elseif( array_key_exists( 'options-message', $params ) ){ + } elseif ( array_key_exists( 'options-message', $params ) ) { # Generate options array from a system message $params['options'] = self::parseMessage( wfMessage( $params['options-message'] )->inContentLanguage()->plain(), @@ -1722,8 +2057,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField { * @return Array * TODO: this is copied from Xml::listDropDown(), deprecate/avoid duplication? */ - public static function parseMessage( $string, $otherName=null ) { - if( $otherName === null ){ + public static function parseMessage( $string, $otherName = null ) { + if ( $otherName === null ) { $otherName = wfMessage( 'htmlform-selectorother-other' )->plain(); } @@ -1734,14 +2069,14 @@ class HTMLSelectAndOtherField extends HTMLSelectField { $value = trim( $option ); if ( $value == '' ) { continue; - } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) { + } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) { # A new group is starting... $value = trim( substr( $value, 1 ) ); $optgroup = $value; - } elseif ( substr( $value, 0, 2) == '**' ) { + } elseif ( substr( $value, 0, 2 ) == '**' ) { # groupmember $opt = trim( substr( $value, 2 ) ); - if( $optgroup === false ){ + if ( $optgroup === false ) { $options[$opt] = $opt; } else { $options[$optgroup][$opt] = $opt; @@ -1763,7 +2098,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField { 'id' => $this->mID . '-other', 'size' => $this->getSize(), ); - + if ( $this->mClass !== '' ) { $textAttribs['class'] = $this->mClass; } @@ -1786,7 +2121,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField { /** * @param $request WebRequest - * @return Array( <overall message>, <select value>, <text field value> ) + * @return Array("<overall message>","<select value>","<text field value>") */ function loadDataFromRequest( $request ) { if ( $request->getCheck( $this->mName ) ) { @@ -1796,14 +2131,14 @@ class HTMLSelectAndOtherField extends HTMLSelectField { if ( $list == 'other' ) { $final = $text; - } elseif( !in_array( $list, $this->mFlatOptions ) ){ + } elseif ( !in_array( $list, $this->mFlatOptions ) ) { # User has spoofed the select form to give an option which wasn't # in the original offer. Sulk... $final = $text; - } elseif( $text == '' ) { + } elseif ( $text == '' ) { $final = $list; } else { - $final = $list . wfMsgForContent( 'colon-separator' ) . $text; + $final = $list . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $text; } } else { @@ -1812,8 +2147,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField { $list = 'other'; $text = $final; foreach ( $this->mFlatOptions as $option ) { - $match = $option . wfMsgForContent( 'colon-separator' ); - if( strpos( $text, $match ) === 0 ) { + $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text(); + if ( strpos( $text, $match ) === 0 ) { $list = $option; $text = substr( $text, strlen( $match ) ); break; @@ -1839,8 +2174,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField { return $p; } - if( isset( $this->mParams['required'] ) && $value[1] === '' ){ - return wfMsgExt( 'htmlform-required', 'parseinline' ); + if ( isset( $this->mParams['required'] ) && $this->mParams['required'] !== false && $value[1] === '' ) { + return $this->msg( 'htmlform-required' )->parse(); } return true; @@ -1869,7 +2204,7 @@ class HTMLRadioField extends HTMLFormField { if ( in_array( $value, $validOptions ) ) { return true; } else { - return wfMsgExt( 'htmlform-select-badoption', 'parseinline' ); + return $this->msg( 'htmlform-select-badoption' )->parse(); } } @@ -1925,17 +2260,17 @@ class HTMLRadioField extends HTMLFormField { * An information field (text blob), not a proper input. */ class HTMLInfoField extends HTMLFormField { - function __construct( $info ) { + public function __construct( $info ) { $info['nodata'] = true; parent::__construct( $info ); } - function getInputHTML( $value ) { + public function getInputHTML( $value ) { return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value ); } - function getTableRow( $value ) { + public function getTableRow( $value ) { if ( !empty( $this->mParams['rawrow'] ) ) { return $value; } @@ -1943,6 +2278,28 @@ class HTMLInfoField extends HTMLFormField { return parent::getTableRow( $value ); } + /** + * @since 1.20 + */ + public function getDiv( $value ) { + if ( !empty( $this->mParams['rawrow'] ) ) { + return $value; + } + + return parent::getDiv( $value ); + } + + /** + * @since 1.20 + */ + public function getRaw( $value ) { + if ( !empty( $this->mParams['rawrow'] ) ) { + return $value; + } + + return parent::getRaw( $value ); + } + protected function needsLabel() { return false; } @@ -1972,6 +2329,20 @@ class HTMLHiddenField extends HTMLFormField { return ''; } + /** + * @since 1.20 + */ + public function getDiv( $value ) { + return $this->getTableRow( $value ); + } + + /** + * @since 1.20 + */ + public function getRaw( $value ) { + return $this->getTableRow( $value ); + } + public function getInputHTML( $value ) { return ''; } } @@ -1981,12 +2352,12 @@ class HTMLHiddenField extends HTMLFormField { */ class HTMLSubmitField extends HTMLFormField { - function __construct( $info ) { + public function __construct( $info ) { $info['nodata'] = true; parent::__construct( $info ); } - function getInputHTML( $value ) { + public function getInputHTML( $value ) { return Xml::submitButton( $value, array( @@ -2007,7 +2378,7 @@ class HTMLSubmitField extends HTMLFormField { * @param $alldata Array * @return Bool */ - public function validate( $value, $alldata ){ + public function validate( $value, $alldata ) { return true; } } @@ -2018,20 +2389,39 @@ class HTMLEditTools extends HTMLFormField { } public function getTableRow( $value ) { + $msg = $this->formatMsg(); + + return '<tr><td></td><td class="mw-input">' + . '<div class="mw-editTools">' + . $msg->parseAsBlock() + . "</div></td></tr>\n"; + } + + /** + * @since 1.20 + */ + public function getDiv( $value ) { + $msg = $this->formatMsg(); + return '<div class="mw-editTools">' . $msg->parseAsBlock() . '</div>'; + } + + /** + * @since 1.20 + */ + public function getRaw( $value ) { + return $this->getDiv( $value ); + } + + protected function formatMsg() { if ( empty( $this->mParams['message'] ) ) { - $msg = wfMessage( 'edittools' ); + $msg = $this->msg( 'edittools' ); } else { - $msg = wfMessage( $this->mParams['message'] ); + $msg = $this->msg( $this->mParams['message'] ); if ( $msg->isDisabled() ) { - $msg = wfMessage( 'edittools' ); + $msg = $this->msg( 'edittools' ); } } $msg->inContentLanguage(); - - - return '<tr><td></td><td class="mw-input">' - . '<div class="mw-editTools">' - . $msg->parseAsBlock() - . "</div></td></tr>\n"; + return $msg; } } diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index f707b3f6..bb8ec5e3 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -1,5 +1,25 @@ <?php - +/** + * Efficient concatenated text storage. + * + * 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 + */ + /** * Base class for general text storage via the "object" flag in old_flags, or * two-part external storage URLs. Used for represent efficient concatenated @@ -179,8 +199,8 @@ class HistoryBlobStub { var $mOldId, $mHash, $mRef; /** - * @param $hash Strng: the content hash of the text - * @param $oldid Integer: the old_id for the CGZ object + * @param $hash string the content hash of the text + * @param $oldid Integer the old_id for the CGZ object */ function __construct( $hash = '', $oldid = 0 ) { $this->mHash = $hash; @@ -298,7 +318,7 @@ class HistoryBlobCurStub { } /** - * @return string|false + * @return string|bool */ function getText() { $dbr = wfGetDB( DB_SLAVE ); @@ -515,13 +535,11 @@ class DiffHistoryBlob implements HistoryBlob { $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) ); - # Check the checksum if mhash is available - if ( extension_loaded( 'mhash' ) ) { - $ofp = mhash( MHASH_ADLER32, $base ); - if ( $ofp !== substr( $diff, 0, 4 ) ) { - wfDebug( __METHOD__. ": incorrect base checksum\n" ); - return false; - } + # Check the checksum if hash/mhash is available + $ofp = $this->xdiffAdler32( $base ); + if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) { + wfDebug( __METHOD__. ": incorrect base checksum\n" ); + return false; } if ( $header['csize'] != strlen( $base ) ) { wfDebug( __METHOD__. ": incorrect base length\n" ); @@ -560,6 +578,30 @@ class DiffHistoryBlob implements HistoryBlob { return $out; } + /** + * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with + * the bytes backwards and initialised with 0 instead of 1. See bug 34428. + * + * Returns false if no hashing library is available + */ + function xdiffAdler32( $s ) { + static $init; + if ( $init === null ) { + $init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02"; + } + // The real Adler-32 checksum of $init is zero, so it initialises the + // state to zero, as it is at the start of LibXDiff's checksum + // algorithm. Appending the subject string then simulates LibXDiff. + if ( function_exists( 'hash' ) ) { + $hash = hash( 'adler32', $init . $s, true ); + } elseif ( function_exists( 'mhash' ) ) { + $hash = mhash( MHASH_ADLER32, $init . $s ); + } else { + return false; + } + return strrev( $hash ); + } + function uncompress() { if ( !$this->mDiffs ) { return; diff --git a/includes/Hooks.php b/includes/Hooks.php index e1c1d50b..bc39f2fc 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -259,7 +259,7 @@ class Hooks { /** * This REALLY should be protected... but it's public for compatibility * - * @param $errno Unused + * @param $errno int Unused * @param $errstr String: error message * @return Boolean: false */ diff --git a/includes/Html.php b/includes/Html.php index c61a1baf..83af24af 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -211,6 +211,23 @@ class Html { 'search', ); + if( $wgHtml5 ) { + $validTypes = array_merge( $validTypes, array( + 'datetime', + 'datetime-local', + 'date', + 'month', + 'time', + 'week', + 'number', + 'range', + 'email', + 'url', + 'search', + 'tel', + 'color', + ) ); + } if ( isset( $attribs['type'] ) && !in_array( $attribs['type'], $validTypes ) ) { unset( $attribs['type'] ); @@ -286,6 +303,8 @@ class Html { return $attribs; } + # Whenever altering this array, please provide a covering test case + # in HtmlTest::provideElementsWithAttributesHavingDefaultValues static $attribDefaults = array( 'area' => array( 'shape' => 'rect' ), 'button' => array( @@ -306,7 +325,6 @@ class Html { 'input' => array( 'formaction' => 'GET', 'type' => 'text', - 'value' => '', ), 'keygen' => array( 'keytype' => 'rsa' ), 'link' => array( 'media' => 'all' ), @@ -325,7 +343,11 @@ class Html { foreach ( $attribs as $attrib => $value ) { $lcattrib = strtolower( $attrib ); - $value = strval( $value ); + if( is_array( $value ) ) { + $value = implode( ' ', $value ); + } else { + $value = strval( $value ); + } # Simple checks using $attribDefaults if ( isset( $attribDefaults[$element][$lcattrib] ) && @@ -343,6 +365,29 @@ class Html { && strval( $attribs['type'] ) == 'text/css' ) { unset( $attribs['type'] ); } + if ( $element === 'input' ) { + $type = isset( $attribs['type'] ) ? $attribs['type'] : null; + $value = isset( $attribs['value'] ) ? $attribs['value'] : null; + if ( $type === 'checkbox' || $type === 'radio' ) { + // The default value for checkboxes and radio buttons is 'on' + // not ''. By stripping value="" we break radio boxes that + // actually wants empty values. + if ( $value === 'on' ) { + unset( $attribs['value'] ); + } + } elseif ( $type === 'submit' ) { + // The default value for submit appears to be "Submit" but + // let's not bother stripping out localized text that matches + // that. + } else { + // The default value for nearly every other field type is '' + // The 'range' and 'color' types use different defaults but + // stripping a value="" does not hurt them. + if ( $value === '' ) { + unset( $attribs['value'] ); + } + } + } if ( $element === 'select' && isset( $attribs['size'] ) ) { if ( in_array( 'multiple', $attribs ) || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false ) @@ -548,9 +593,10 @@ class Html { } /** - * Output a <script> tag with the given contents. TODO: do some useful - * escaping as well, like if $contents contains literal '</script>' or (for - * XML) literal "]]>". + * Output a "<script>" tag with the given contents. + * + * @todo do some useful escaping as well, like if $contents contains + * literal "</script>" or (for XML) literal "]]>". * * @param $contents string JavaScript * @return string Raw HTML @@ -572,8 +618,8 @@ class Html { } /** - * Output a <script> tag linking to the given URL, e.g., - * <script src=foo.js></script>. + * Output a "<script>" tag linking to the given URL, e.g., + * "<script src=foo.js></script>". * * @param $url string * @return string Raw HTML @@ -591,9 +637,9 @@ class Html { } /** - * Output a <style> tag with the given contents for the given media type + * Output a "<style>" tag with the given contents for the given media type * (if any). TODO: do some useful escaping as well, like if $contents - * contains literal '</style>' (admittedly unlikely). + * contains literal "</style>" (admittedly unlikely). * * @param $contents string CSS * @param $media mixed A media type string, like 'screen' @@ -613,7 +659,7 @@ class Html { } /** - * Output a <link rel=stylesheet> linking to the given URL for the given + * Output a "<link rel=stylesheet>" linking to the given URL for the given * media type (if any). * * @param $url string @@ -630,7 +676,7 @@ class Html { } /** - * Convenience function to produce an <input> element. This supports the + * Convenience function to produce an "<input>" element. This supports the * new HTML5 input types and attributes, and will silently strip them if * $wgHtml5 is false. * @@ -663,11 +709,12 @@ class Html { } /** - * Convenience function to produce an <input> element. This supports leaving - * out the cols= and rows= which Xml requires and are required by HTML4/XHTML - * but not required by HTML5 and will silently set cols="" and rows="" if - * $wgHtml5 is false and cols and rows are omitted (HTML4 validates present - * but empty cols="" and rows="" as valid). + * Convenience function to produce an "<input>" element. + * + * This supports leaving out the cols= and rows= which Xml requires and are + * required by HTML4/XHTML but not required by HTML5 and will silently set + * cols="" and rows="" if $wgHtml5 is false and cols and rows are omitted + * (HTML4 validates present but empty cols="" and rows="" as valid). * * @param $name string name attribute * @param $value string value attribute @@ -706,8 +753,10 @@ class Html { * * @param $params array: * - selected: [optional] Id of namespace which should be pre-selected - * - all: [optional] Value of item for "all namespaces". If null or unset, no <option> is generated to select all namespaces + * - all: [optional] Value of item for "all namespaces". If null or unset, no "<option>" is generated to select all namespaces * - label: text for label to add before the field + * - exclude: [optional] Array of namespace ids to exclude + * - disable: [optional] Array of namespace ids for which the option should be disabled in the selector * @param $selectAttribs array HTML attributes for the generated select element. * - id: [optional], default: 'namespace' * - name: [optional], default: 'namespace' @@ -716,11 +765,6 @@ class Html { public static function namespaceSelector( Array $params = array(), Array $selectAttribs = array() ) { global $wgContLang; - // Default 'id' & 'name' <select> attributes - $selectAttribs = $selectAttribs + array( - 'id' => 'namespace', - 'name' => 'namespace', - ); ksort( $selectAttribs ); // Is a namespace selected? @@ -737,39 +781,60 @@ class Html { $params['selected'] = ''; } - // Array holding the <option> elements + if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { + $params['exclude'] = array(); + } + if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { + $params['disable'] = array(); + } + + // Associative array between option-values and option-labels $options = array(); if ( isset( $params['all'] ) ) { - // add an <option> that would let the user select all namespaces. - // Value is provided by user, the name shown is localized. - $options[$params['all']] = wfMsg( 'namespacesall' ); + // add an option that would let the user select all namespaces. + // Value is provided by user, the name shown is localized for the user. + $options[$params['all']] = wfMessage( 'namespacesall' )->text(); } - // Add defaults <option> according to content language + // Add all namespaces as options (in the content langauge) $options += $wgContLang->getFormattedNamespaces(); - // Convert $options to HTML + // Convert $options to HTML and filter out namespaces below 0 $optionsHtml = array(); foreach ( $options as $nsId => $nsName ) { - if ( $nsId < NS_MAIN ) { + if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { continue; } if ( $nsId === 0 ) { - $nsName = wfMsg( 'blanknamespace' ); + // For other namespaces use use the namespace prefix as label, but for + // main we don't use "" but the user message descripting it (e.g. "(Main)" or "(Article)") + $nsName = wfMessage( 'blanknamespace' )->text(); } - $optionsHtml[] = Xml::option( $nsName, $nsId, $nsId === $params['selected'] ); + $optionsHtml[] = Html::element( + 'option', array( + 'disabled' => in_array( $nsId, $params['disable'] ), + 'value' => $nsId, + 'selected' => $nsId === $params['selected'], + ), $nsName + ); } - // Forge a <select> element and returns it $ret = ''; if ( isset( $params['label'] ) ) { - $ret .= Xml::label( $params['label'], $selectAttribs['id'] ) . ' '; + $ret .= Html::element( + 'label', array( + 'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null, + ), $params['label'] + ) . ' '; } + + // Wrap options in a <select> $ret .= Html::openElement( 'select', $selectAttribs ) . "\n" . implode( "\n", $optionsHtml ) . "\n" . Html::closeElement( 'select' ); + return $ret; } @@ -839,7 +904,7 @@ class Html { /** * Get HTML for an info box with an icon. * - * @param $text String: wikitext, get this with wfMsgNoTrans() + * @param $text String: wikitext, get this with wfMessage()->plain() * @param $icon String: icon name, file in skins/common/images * @param $alt String: alternate text for the icon * @param $class String: additional class name to add to the wrapper div diff --git a/includes/HttpFunctions.old.php b/includes/HttpFunctions.old.php index 479b4d23..feb9b93c 100644 --- a/includes/HttpFunctions.old.php +++ b/includes/HttpFunctions.old.php @@ -1,4 +1,25 @@ <?php +/** + * Class alias kept for backward compatibility. + * + * 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 HTTP + */ /** * HttpRequest was renamed to MWHttpRequest in order diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index 147823fe..8453e62c 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -1,5 +1,27 @@ <?php /** + * Various HTTP related functions. + * + * 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 HTTP + */ + +/** * @defgroup HTTP HTTP */ @@ -20,8 +42,9 @@ class Http { * - timeout Timeout length in seconds * - postData An array of key-value pairs or a url-encoded form data * - proxy The proxy to use. - * Will use $wgHTTPProxy (if set) otherwise. - * - noProxy Override $wgHTTPProxy (if set) and don't use any proxy at all. + * Otherwise it will use $wgHTTPProxy (if set) + * Otherwise it will use the environment variable "http_proxy" (if set) + * - noProxy Don't use any proxy at all. Takes precedence over proxy value(s). * - sslVerifyHost (curl only) Verify hostname against certificate * - sslVerifyCert (curl only) Verify SSL certificate * - caInfo (curl only) Provide CA information @@ -42,9 +65,6 @@ class Http { } $req = MWHttpRequest::factory( $url, $options ); - if( isset( $options['userAgent'] ) ) { - $req->setUserAgent( $options['userAgent'] ); - } $status = $req->execute(); if ( $status->isOK() ) { @@ -136,7 +156,7 @@ class Http { * * file:// should not be allowed here for security purpose (r67684) * - * @fixme this is wildly inaccurate and fails to actually check most stuff + * @todo FIXME this is wildly inaccurate and fails to actually check most stuff * * @param $uri Mixed: URI to check for validity * @return Boolean @@ -192,7 +212,7 @@ class MWHttpRequest { * @param $url String: url to use. If protocol-relative, will be expanded to an http:// URL * @param $options Array: (optional) extra params to pass (see Http::request()) */ - function __construct( $url, $options = array() ) { + protected function __construct( $url, $options = array() ) { global $wgHTTPTimeout; $this->url = wfExpandUrl( $url, PROTO_HTTP ); @@ -209,15 +229,27 @@ class MWHttpRequest { } else { $this->timeout = $wgHTTPTimeout; } + if( isset( $options['userAgent'] ) ) { + $this->setUserAgent( $options['userAgent'] ); + } $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ); foreach ( $members as $o ) { if ( isset( $options[$o] ) ) { + // ensure that MWHttpRequest::method is always + // uppercased. Bug 36137 + if ( $o == 'method' ) { + $options[$o] = strtoupper( $options[$o] ); + } $this->$o = $options[$o]; } } + + if ( $this->noProxy ) { + $this->proxy = ''; // noProxy takes precedence + } } /** @@ -278,19 +310,18 @@ class MWHttpRequest { } /** - * Take care of setting up the proxy - * (override in subclass) + * Take care of setting up the proxy (do nothing if "noProxy" is set) * - * @return String + * @return void */ public function proxySetup() { global $wgHTTPProxy; - if ( $this->proxy ) { + if ( $this->proxy || !$this->noProxy ) { return; } - if ( Http::isLocalURL( $this->url ) ) { + if ( Http::isLocalURL( $this->url ) || $this->noProxy ) { $this->proxy = ''; } elseif ( $wgHTTPProxy ) { $this->proxy = $wgHTTPProxy ; @@ -376,6 +407,7 @@ class MWHttpRequest { * * @param $fh handle * @param $content String + * @return int */ public function read( $fh, $content ) { $this->content .= $content; @@ -400,9 +432,7 @@ class MWHttpRequest { $this->setReferer( wfExpandUrl( $wgTitle->getFullURL(), PROTO_CURRENT ) ); } - if ( !$this->noProxy ) { - $this->proxySetup(); - } + $this->proxySetup(); // set up any proxy as needed if ( !$this->callback ) { $this->setCallback( array( $this, 'read' ) ); @@ -417,8 +447,6 @@ class MWHttpRequest { * Parses the headers, including the HTTP status code and any * Set-Cookie headers. This function expectes the headers to be * found in an array in the member variable headerList. - * - * @return nothing */ protected function parseHeader() { $lastname = ""; @@ -446,8 +474,6 @@ class MWHttpRequest { * RFC2616, section 10, * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a * list of status codes.) - * - * @return nothing */ protected function setStatus() { if ( !$this->respHeaders ) { @@ -801,11 +827,13 @@ class PhpHttpRequest extends MWHttpRequest { if ( $this->method == 'POST' ) { // Required for HTTP 1.0 POSTs $this->reqHeaders['Content-Length'] = strlen( $this->postData ); - $this->reqHeaders['Content-type'] = "application/x-www-form-urlencoded"; + if( !isset( $this->reqHeaders['Content-Type'] ) ) { + $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded"; + } } $options = array(); - if ( $this->proxy && !$this->noProxy ) { + if ( $this->proxy ) { $options['proxy'] = $this->urlToTCP( $this->proxy ); $options['request_fulluri'] = true; } @@ -884,7 +912,7 @@ class PhpHttpRequest extends MWHttpRequest { return $this->status; } - // If everything went OK, or we recieved some error code + // If everything went OK, or we received some error code // get the response body content. if ( $this->status->isOK() || (int)$this->respStatus >= 300) { diff --git a/includes/IP.php b/includes/IP.php index e3f61214..10c707e7 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -133,7 +133,7 @@ class IP { } /** - * Convert an IP into a nice standard form. + * Convert an IP into a verbose, uppercase, normalized form. * IPv6 addresses in octet notation are expanded to 8 words. * IPv4 addresses are just trimmed. * @@ -186,6 +186,49 @@ class IP { } /** + * Prettify an IP for display to end users. + * This will make it more compact and lower-case. + * + * @param $ip string + * @return string + */ + public static function prettifyIP( $ip ) { + $ip = self::sanitizeIP( $ip ); // normalize (removes '::') + if ( self::isIPv6( $ip ) ) { + // Split IP into an address and a CIDR + if ( strpos( $ip, '/' ) !== false ) { + list( $ip, $cidr ) = explode( '/', $ip, 2 ); + } else { + list( $ip, $cidr ) = array( $ip, '' ); + } + // Get the largest slice of words with multiple zeros + $offset = 0; + $longest = $longestPos = false; + while ( preg_match( + '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset + ) ) { + list( $match, $pos ) = $m[0]; // full match + if ( strlen( $match ) > strlen( $longest ) ) { + $longest = $match; + $longestPos = $pos; + } + $offset += ( $pos + strlen( $match ) ); // advance + } + if ( $longest !== false ) { + // Replace this portion of the string with the '::' abbreviation + $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) ); + } + // Add any CIDR back on + if ( $cidr !== '' ) { + $ip = "{$ip}/{$cidr}"; + } + // Convert to lower case to make it more readable + $ip = strtolower( $ip ); + } + return $ip; + } + + /** * Given a host/port string, like one might find in the host part of a URL * per RFC 2732, split the hostname part and the port part and return an * array with an element for each. If there is no port part, the array will @@ -198,7 +241,7 @@ class IP { * * A bare IPv6 address is accepted despite the lack of square brackets. * - * @param $both The string with the host and port + * @param $both string The string with the host and port * @return array */ public static function splitHostAndPort( $both ) { @@ -671,6 +714,7 @@ class IP { * @return String: valid dotted quad IPv4 address or null */ public static function canonicalize( $addr ) { + $addr = preg_replace( '/\%.*/','', $addr ); // remove zone info (bug 35738) if ( self::isValid( $addr ) ) { return $addr; } diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php deleted file mode 100644 index 4b90e24a..00000000 --- a/includes/ImageFunctions.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php -/** - * Global functions related to images - * - * @file - */ - -/** - * Determine if an image exists on the 'bad image list'. - * - * The format of MediaWiki:Bad_image_list is as follows: - * * Only list items (lines starting with "*") are considered - * * The first link on a line must be a link to a bad image - * * Any subsequent links on the same line are considered to be exceptions, - * i.e. articles where the image may occur inline. - * - * @param $name string the image name to check - * @param $contextTitle Title|bool the page on which the image occurs, if known - * @param $blacklist string wikitext of a file blacklist - * @return bool - */ -function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { - static $badImageCache = null; // based on bad_image_list msg - wfProfileIn( __METHOD__ ); - - # Handle redirects - $redirectTitle = RepoGroup::singleton()->checkRedirect( Title::makeTitle( NS_FILE, $name ) ); - if( $redirectTitle ) { - $name = $redirectTitle->getDbKey(); - } - - # Run the extension hook - $bad = false; - if( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) { - wfProfileOut( __METHOD__ ); - return $bad; - } - - $cacheable = ( $blacklist === null ); - if( $cacheable && $badImageCache !== null ) { - $badImages = $badImageCache; - } else { // cache miss - if ( $blacklist === null ) { - $blacklist = wfMsgForContentNoTrans( 'bad_image_list' ); // site list - } - # Build the list now - $badImages = array(); - $lines = explode( "\n", $blacklist ); - foreach( $lines as $line ) { - # List items only - if ( substr( $line, 0, 1 ) !== '*' ) { - continue; - } - - # Find all links - $m = array(); - if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { - continue; - } - - $exceptions = array(); - $imageDBkey = false; - foreach ( $m[1] as $i => $titleText ) { - $title = Title::newFromText( $titleText ); - if ( !is_null( $title ) ) { - if ( $i == 0 ) { - $imageDBkey = $title->getDBkey(); - } else { - $exceptions[$title->getPrefixedDBkey()] = true; - } - } - } - - if ( $imageDBkey !== false ) { - $badImages[$imageDBkey] = $exceptions; - } - } - if ( $cacheable ) { - $badImageCache = $badImages; - } - } - - $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; - $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] ); - wfProfileOut( __METHOD__ ); - return $bad; -} diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index 1106124a..91c3190f 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -1,6 +1,24 @@ <?php -if ( ! defined( 'MEDIAWIKI' ) ) - die( 1 ); +/** + * Image gallery. + * + * 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 + */ /** * Image gallery @@ -75,7 +93,7 @@ class ImageGallery { /** * Set the caption (as plain text) * - * @param $caption Caption + * @param $caption string Caption */ function setCaption( $caption ) { $this->mCaption = htmlspecialchars( $caption ); @@ -141,23 +159,24 @@ class ImageGallery { * @param $title Title object of the image that is added to the gallery * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown. * @param $alt String: Alt text for the image + * @param $link String: Override image link (optional) */ - function add( $title, $html = '', $alt = '' ) { + function add( $title, $html = '', $alt = '', $link = '') { if ( $title instanceof File ) { // Old calling convention $title = $title->getTitle(); } - $this->mImages[] = array( $title, $html, $alt ); + $this->mImages[] = array( $title, $html, $alt, $link ); wfDebug( 'ImageGallery::add ' . $title->getText() . "\n" ); } /** - * Add an image at the beginning of the gallery. - * - * @param $title Title object of the image that is added to the gallery - * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown. - * @param $alt String: Alt text for the image - */ + * Add an image at the beginning of the gallery. + * + * @param $title Title object of the image that is added to the gallery + * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown. + * @param $alt String: Alt text for the image + */ function insert( $title, $html = '', $alt = '' ) { if ( $title instanceof File ) { // Old calling convention @@ -168,6 +187,7 @@ class ImageGallery { /** * isEmpty() returns true if the gallery contains no images + * @return bool */ function isEmpty() { return empty( $this->mImages ); @@ -215,6 +235,7 @@ class ImageGallery { * - the additional text provided when adding the image * - the size of the image * + * @return string */ function toHTML() { global $wgLang; @@ -243,6 +264,7 @@ class ImageGallery { $nt = $pair[0]; $text = $pair[1]; # "text" means "caption" here $alt = $pair[2]; + $link = $pair[3]; $descQuery = false; if ( $nt->getNamespace() == NS_FILE ) { @@ -287,6 +309,7 @@ class ImageGallery { 'desc-link' => true, 'desc-query' => $descQuery, 'alt' => $alt, + 'custom-url-link' => $link ); # In the absence of both alt text and caption, fall back on providing screen readers with the filename as alt text if ( $alt == '' && $text == '' ) { @@ -316,7 +339,7 @@ class ImageGallery { if( $img ) { $fileSize = htmlspecialchars( $wgLang->formatSize( $img->getSize() ) ); } else { - $fileSize = wfMsgHtml( 'filemissing' ); + $fileSize = wfMessage( 'filemissing' )->escaped(); } $fileSize = "$fileSize<br />\n"; } else { @@ -344,9 +367,9 @@ class ImageGallery { . '<div style="width: ' . ( $this->mWidths + self::THUMB_PADDING + self::GB_PADDING ) . 'px">' . $thumbhtml . "\n\t\t\t" . '<div class="gallerytext">' . "\n" - . $textlink . $text . $fileSize + . $textlink . $text . $fileSize . "\n\t\t\t</div>" - . "\n\t\t</div></li>"; + . "\n\t\t</div></li>"; } $output .= "\n</ul>"; @@ -376,8 +399,8 @@ class ImageGallery { */ public function getContextTitle() { return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title - ? $this->contextTitle - : false; + ? $this->contextTitle + : false; } } //class diff --git a/includes/ImagePage.php b/includes/ImagePage.php index dcb09a41..6f1b1a15 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -1,5 +1,26 @@ <?php /** + * Special handling for file description pages. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** * Class for viewing MediaWiki file description pages * * @ingroup Media @@ -30,6 +51,7 @@ class ImagePage extends Article { /** * Constructor from a page id * @param $id Int article ID to load + * @return ImagePage|null */ public static function newFromID( $id ) { $t = Title::newFromID( $id ); @@ -50,7 +72,7 @@ class ImagePage extends Article { protected function loadFile() { if ( $this->fileLoaded ) { - return true; + return; } $this->fileLoaded = true; @@ -74,19 +96,21 @@ class ImagePage extends Article { * Include body text only; none of the image extras */ public function render() { - global $wgOut; - $wgOut->setArticleBodyOnly( true ); + $this->getContext()->getOutput()->setArticleBodyOnly( true ); parent::view(); } public function view() { - global $wgOut, $wgShowEXIF, $wgRequest, $wgUser; + global $wgShowEXIF; - $diff = $wgRequest->getVal( 'diff' ); - $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); + $out = $this->getContext()->getOutput(); + $request = $this->getContext()->getRequest(); + $diff = $request->getVal( 'diff' ); + $diffOnly = $request->getBool( 'diffonly', $this->getContext()->getUser()->getOption( 'diffonly' ) ); if ( $this->getTitle()->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) ) { - return parent::view(); + parent::view(); + return; } $this->loadFile(); @@ -95,13 +119,14 @@ class ImagePage extends Article { if ( $this->getTitle()->getDBkey() == $this->mPage->getFile()->getName() || isset( $diff ) ) { // mTitle is the same as the redirect target so ask Article // to perform the redirect for us. - $wgRequest->setVal( 'diffonly', 'true' ); - return parent::view(); + $request->setVal( 'diffonly', 'true' ); + parent::view(); + return; } else { // mTitle is not the same as the redirect target so it is // probably the redirect page itself. Fake the redirect symbol - $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); - $wgOut->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ), + $out->setPageTitle( $this->getTitle()->getPrefixedText() ); + $out->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ), /* $appendSubtitle */ true, /* $forceKnown */ true ) ); $this->mPage->doViewUpdates( $this->getContext()->getUser() ); return; @@ -117,7 +142,7 @@ class ImagePage extends Article { } if ( !$diff && $this->displayImg->exists() ) { - $wgOut->addHTML( $this->showTOC( $showmeta ) ); + $out->addHTML( $this->showTOC( $showmeta ) ); } if ( !$diff ) { @@ -128,16 +153,16 @@ class ImagePage extends Article { if ( $this->mPage->getID() ) { # NS_FILE is in the user language, but this section (the actual wikitext) # should be in page content language - $pageLang = $this->getTitle()->getPageLanguage(); - $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content', - 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), + $pageLang = $this->getTitle()->getPageViewLanguage(); + $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content', + 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(), 'class' => 'mw-content-'.$pageLang->getDir() ) ) ); parent::view(); - $wgOut->addHTML( Xml::closeElement( 'div' ) ); + $out->addHTML( Xml::closeElement( 'div' ) ); } else { # Just need to set the right headers - $wgOut->setArticleFlag( true ); - $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); + $out->setArticleFlag( true ); + $out->setPageTitle( $this->getTitle()->getPrefixedText() ); $this->mPage->doViewUpdates( $this->getContext()->getUser() ); } @@ -145,18 +170,18 @@ class ImagePage extends Article { if ( $this->mExtraDescription ) { $fol = wfMessage( 'shareddescriptionfollows' ); if ( !$fol->isDisabled() ) { - $wgOut->addWikiText( $fol->plain() ); + $out->addWikiText( $fol->plain() ); } - $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" ); + $out->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" ); } $this->closeShowImage(); $this->imageHistory(); // TODO: Cleanup the following - $wgOut->addHTML( Xml::element( 'h2', + $out->addHTML( Xml::element( 'h2', array( 'id' => 'filelinks' ), - wfMsg( 'imagelinks' ) ) . "\n" ); + wfMessage( 'imagelinks' )->text() ) . "\n" ); $this->imageDupes(); # @todo FIXME: For some freaky reason, we can't redirect to foreign images. # Yet we return metadata about the target. Definitely an issue in the FileRepo @@ -166,24 +191,27 @@ class ImagePage extends Article { $html = ''; wfRunHooks( 'ImagePageAfterImageLinks', array( $this, &$html ) ); if ( $html ) { - $wgOut->addHTML( $html ); + $out->addHTML( $html ); } if ( $showmeta ) { - $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ) . "\n" ); - $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); - $wgOut->addModules( array( 'mediawiki.action.view.metadata' ) ); + $out->addHTML( Xml::element( + 'h2', + array( 'id' => 'metadata' ), + wfMessage( 'metadata' )->text() ) . "\n" ); + $out->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); + $out->addModules( array( 'mediawiki.action.view.metadata' ) ); } // Add remote Filepage.css if( !$this->repo->isLocal() ) { $css = $this->repo->getDescriptionStylesheetUrl(); if ( $css ) { - $wgOut->addStyle( $css ); + $out->addStyle( $css ); } } // always show the local local Filepage.css, bug 29277 - $wgOut->addModuleStyles( 'filepage' ); + $out->addModuleStyles( 'filepage' ); } /** @@ -202,12 +230,12 @@ class ImagePage extends Article { */ protected function showTOC( $metadata ) { $r = array( - '<li><a href="#file">' . wfMsgHtml( 'file-anchor-link' ) . '</a></li>', - '<li><a href="#filehistory">' . wfMsgHtml( 'filehist' ) . '</a></li>', - '<li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>', + '<li><a href="#file">' . wfMessage( 'file-anchor-link' )->escaped() . '</a></li>', + '<li><a href="#filehistory">' . wfMessage( 'filehist' )->escaped() . '</a></li>', + '<li><a href="#filelinks">' . wfMessage( 'imagelinks' )->escaped() . '</a></li>', ); if ( $metadata ) { - $r[] = '<li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>'; + $r[] = '<li><a href="#metadata">' . wfMessage( 'metadata' )->escaped() . '</a></li>'; } wfRunHooks( 'ImagePageShowTOC', array( $this, &$r ) ); @@ -225,7 +253,7 @@ class ImagePage extends Article { */ protected function makeMetadataTable( $metadata ) { $r = "<div class=\"mw-imagepage-section-metadata\">"; - $r .= wfMsgNoTrans( 'metadata-help' ); + $r .= wfMessage( 'metadata-help' )->plain(); $r .= "<table id=\"mw_metadata\" class=\"mw_metadata\">\n"; foreach ( $metadata as $type => $stuff ) { foreach ( $stuff as $v ) { @@ -259,12 +287,16 @@ class ImagePage extends Article { } protected function openShowImage() { - global $wgOut, $wgUser, $wgImageLimits, $wgRequest, - $wgLang, $wgEnableUploads, $wgSend404Code; + global $wgImageLimits, $wgEnableUploads, $wgSend404Code; $this->loadFile(); + $out = $this->getContext()->getOutput(); + $user = $this->getContext()->getUser(); + $lang = $this->getContext()->getLanguage(); + $dirmark = $lang->getDirMarkEntity(); + $request = $this->getContext()->getRequest(); - $sizeSel = intval( $wgUser->getOption( 'imagesize' ) ); + $sizeSel = intval( $user->getOption( 'imagesize' ) ); if ( !isset( $wgImageLimits[$sizeSel] ) ) { $sizeSel = User::getDefaultOption( 'imagesize' ); @@ -278,11 +310,10 @@ class ImagePage extends Article { $max = $wgImageLimits[$sizeSel]; $maxWidth = $max[0]; $maxHeight = $max[1]; - $dirmark = $wgLang->getDirMark(); if ( $this->displayImg->exists() ) { # image - $page = $wgRequest->getIntOrNull( 'page' ); + $page = $request->getIntOrNull( 'page' ); if ( is_null( $page ) ) { $params = array(); $page = 1; @@ -294,15 +325,15 @@ class ImagePage extends Article { $height_orig = $this->displayImg->getHeight( $page ); $height = $height_orig; - $longDesc = wfMsg( 'parentheses', $this->displayImg->getLongDesc() ); + $longDesc = wfMessage( 'parentheses', $this->displayImg->getLongDesc() )->text(); - wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$wgOut ) ); + wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$out ) ); if ( $this->displayImg->allowInlineDisplay() ) { # image # "Download high res version" link below the image - # $msgsize = wfMsgHtml( 'file-info-size', $width_orig, $height_orig, Linker::formatSize( $this->displayImg->getSize() ), $mime ); + # $msgsize = wfMessage( 'file-info-size', $width_orig, $height_orig, Linker::formatSize( $this->displayImg->getSize() ), $mime )->escaped(); # We'll show a thumbnail of this image if ( $width > $maxWidth || $height > $maxHeight ) { # Calculate the thumbnail size. @@ -318,21 +349,33 @@ class ImagePage extends Article { # Note that $height <= $maxHeight now, but might not be identical # because of rounding. } - $msgbig = wfMsgHtml( 'show-big-image' ); + $msgbig = wfMessage( 'show-big-image' )->escaped(); + if ( $this->displayImg->getRepo()->canTransformVia404() ) { + $thumbSizes = $wgImageLimits; + } else { + # Creating thumb links triggers thumbnail generation. + # Just generate the thumb for the current users prefs. + $thumbOption = $user->getOption( 'thumbsize' ); + $thumbSizes = array( isset( $wgImageLimits[$thumbOption] ) + ? $wgImageLimits[$thumbOption] + : $wgImageLimits[User::getDefaultOption( 'thumbsize' )] ); + } + # Generate thumbnails or thumbnail links as needed... $otherSizes = array(); - foreach ( $wgImageLimits as $size ) { - if ( $size[0] < $width_orig && $size[1] < $height_orig && - $size[0] != $width && $size[1] != $height ) { + foreach ( $thumbSizes as $size ) { + if ( $size[0] < $width_orig && $size[1] < $height_orig + && $size[0] != $width && $size[1] != $height ) + { $otherSizes[] = $this->makeSizeLink( $params, $size[0], $size[1] ); } } $msgsmall = wfMessage( 'show-big-image-preview' )-> rawParams( $this->makeSizeLink( $params, $width, $height ) )-> parse(); - if ( count( $otherSizes ) && $this->displayImg->getRepo()->canTransformVia404() ) { + if ( count( $otherSizes ) ) { $msgsmall .= ' ' . Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ), - wfMessage( 'show-big-image-other' )->rawParams( $wgLang->pipeList( $otherSizes ) )-> + wfMessage( 'show-big-image-other' )->rawParams( $lang->pipeList( $otherSizes ) )-> params( count( $otherSizes ) )->parse() ); } @@ -340,6 +383,9 @@ class ImagePage extends Article { # Some sort of audio file that doesn't have dimensions # Don't output a no hi res message for such a file $msgsmall = ''; + } elseif ( $this->displayImg->isVectorized() ) { + # For vectorized images, full size is just the frame size + $msgsmall = ''; } else { # Image is small enough to show full size on image page $msgsmall = wfMessage( 'file-nohires' )->parse(); @@ -354,7 +400,7 @@ class ImagePage extends Article { $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1; if ( $isMulti ) { - $wgOut->addHTML( '<table class="multipageimage"><tr><td>' ); + $out->addHTML( '<table class="multipageimage"><tr><td>' ); } if ( $thumbnail ) { @@ -362,7 +408,7 @@ class ImagePage extends Article { 'alt' => $this->displayImg->getTitle()->getPrefixedText(), 'file-link' => true, ); - $wgOut->addHTML( '<div class="fullImageLink" id="file">' . + $out->addHTML( '<div class="fullImageLink" id="file">' . $thumbnail->toHtml( $options ) . $anchorclose . "</div>\n" ); } @@ -371,13 +417,12 @@ class ImagePage extends Article { $count = $this->displayImg->pageCount(); if ( $page > 1 ) { - $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false ); - $link = Linker::link( + $label = $out->parse( wfMessage( 'imgmultipageprev' )->text(), false ); + $link = Linker::linkKnown( $this->getTitle(), $label, array(), - array( 'page' => $page - 1 ), - array( 'known', 'noclasses' ) + array( 'page' => $page - 1 ) ); $thumb1 = Linker::makeThumbLinkObj( $this->getTitle(), $this->displayImg, $link, $label, 'none', array( 'page' => $page - 1 ) ); @@ -386,13 +431,12 @@ class ImagePage extends Article { } if ( $page < $count ) { - $label = wfMsg( 'imgmultipagenext' ); - $link = Linker::link( + $label = wfMessage( 'imgmultipagenext' )->text(); + $link = Linker::linkKnown( $this->getTitle(), $label, array(), - array( 'page' => $page + 1 ), - array( 'known', 'noclasses' ) + array( 'page' => $page + 1 ) ); $thumb2 = Linker::makeThumbLinkObj( $this->getTitle(), $this->displayImg, $link, $label, 'none', array( 'page' => $page + 1 ) ); @@ -409,18 +453,18 @@ class ImagePage extends Article { ); $options = array(); for ( $i = 1; $i <= $count; $i++ ) { - $options[] = Xml::option( $wgLang->formatNum( $i ), $i, $i == $page ); + $options[] = Xml::option( $lang->formatNum( $i ), $i, $i == $page ); } $select = Xml::tags( 'select', array( 'id' => 'pageselector', 'name' => 'page' ), implode( "\n", $options ) ); - $wgOut->addHTML( + $out->addHTML( '</td><td><div class="multipageimagenavbox">' . Xml::openElement( 'form', $formParams ) . Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . - wfMsgExt( 'imgmultigoto', array( 'parseinline', 'replaceafter' ), $select ) . - Xml::submitButton( wfMsg( 'imgmultigo' ) ) . + wfMessage( 'imgmultigoto' )->rawParams( $select )->parse() . + Xml::submitButton( wfMessage( 'imgmultigo' )->text() ) . Xml::closeElement( 'form' ) . "<hr />$thumb1\n$thumb2<br style=\"clear: both\" /></div></td></tr></table>" ); @@ -430,7 +474,7 @@ class ImagePage extends Article { if ( $this->displayImg->isSafeFile() ) { $icon = $this->displayImg->iconThumb(); - $wgOut->addHTML( '<div class="fullImageLink" id="file">' . + $out->addHTML( '<div class="fullImageLink" id="file">' . $icon->toHtml( array( 'file-link' => true ) ) . "</div>\n" ); } @@ -447,27 +491,69 @@ class ImagePage extends Article { $medialink = "[[Media:$filename|$linktext]]"; if ( !$this->displayImg->isSafeFile() ) { - $warning = wfMsgNoTrans( 'mediawarning' ); - $wgOut->addWikiText( <<<EOT -<div class="fullMedia"><span class="dangerousLink">{$medialink}</span>$dirmark <span class="fileInfo">$longDesc</span></div> + $warning = wfMessage( 'mediawarning' )->plain(); + // dirmark is needed here to separate the file name, which + // most likely ends in Latin characters, from the description, + // which may begin with the file type. In RTL environment + // this will get messy. + // The dirmark, however, must not be immediately adjacent + // to the filename, because it can get copied with it. + // See bug 25277. + $out->addWikiText( <<<EOT +<div class="fullMedia"><span class="dangerousLink">{$medialink}</span> $dirmark<span class="fileInfo">$longDesc</span></div> <div class="mediaWarning">$warning</div> EOT ); } else { - $wgOut->addWikiText( <<<EOT -<div class="fullMedia">{$medialink}{$dirmark} <span class="fileInfo">$longDesc</span> + $out->addWikiText( <<<EOT +<div class="fullMedia">{$medialink} {$dirmark}<span class="fileInfo">$longDesc</span> </div> EOT ); } } + // Add cannot animate thumbnail warning + if ( !$this->displayImg->canAnimateThumbIfAppropriate() ) { + // Include the extension so wiki admins can + // customize it on a per file-type basis + // (aka say things like use format X instead). + // additionally have a specific message for + // file-no-thumb-animation-gif + $ext = $this->displayImg->getExtension(); + $noAnimMesg = wfMessageFallback( + 'file-no-thumb-animation-' . $ext, + 'file-no-thumb-animation' + )->plain(); + + $out->addWikiText( <<<EOT +<div class="mw-noanimatethumb">{$noAnimMesg}</div> +EOT + ); + } + if ( !$this->displayImg->isLocal() ) { $this->printSharedImageText(); } } else { # Image does not exist - if ( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) { + if ( !$this->getID() ) { + # No article exists either + # Show deletion log to be consistent with normal articles + LogEventsList::showLogExtract( + $out, + array( 'delete', 'move' ), + $this->getTitle()->getPrefixedText(), + '', + array( 'lim' => 10, + 'conds' => array( "log_action != 'revision'" ), + 'showIfEmpty' => false, + 'msgKey' => array( 'moveddeleted-notice' ) + ) + ); + } + + if ( $wgEnableUploads && $user->isAllowed( 'upload' ) ) { // Only show an upload link if the user can upload $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); $nofile = array( @@ -480,15 +566,15 @@ EOT // Note, if there is an image description page, but // no image, then this setRobotPolicy is overriden // by Article::View(). - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile ); + $out->setRobotPolicy( 'noindex,nofollow' ); + $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile ); if ( !$this->getID() && $wgSend404Code ) { // If there is no image, no shared image, and no description page, // output a 404, to be consistent with articles. - $wgRequest->response()->header( 'HTTP/1.1 404 Not Found' ); + $request->response()->header( 'HTTP/1.1 404 Not Found' ); } } - $wgOut->setFileVersion( $this->displayImg ); + $out->setFileVersion( $this->displayImg ); } /** @@ -518,8 +604,7 @@ EOT * Show a notice that the file is from a shared repository */ protected function printSharedImageText() { - global $wgOut; - + $out = $this->getContext()->getOutput(); $this->loadFile(); $descUrl = $this->mPage->getFile()->getDescriptionUrl(); @@ -527,18 +612,18 @@ EOT /* Add canonical to head if there is no local page for this shared file */ if( $descUrl && $this->mPage->getID() == 0 ) { - $wgOut->addLink( array( 'rel' => 'canonical', 'href' => $descUrl ) ); + $out->addLink( array( 'rel' => 'canonical', 'href' => $descUrl ) ); } $wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n"; $repo = $this->mPage->getFile()->getRepo()->getDisplayName(); - if ( $descUrl && $descText && wfMsgNoTrans( 'sharedupload-desc-here' ) !== '-' ) { - $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) ); - } elseif ( $descUrl && wfMsgNoTrans( 'sharedupload-desc-there' ) !== '-' ) { - $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) ); + if ( $descUrl && $descText && wfMessage( 'sharedupload-desc-here' )->plain() !== '-' ) { + $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) ); + } elseif ( $descUrl && wfMessage( 'sharedupload-desc-there' )->plain() !== '-' ) { + $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) ); } else { - $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ ); + $out->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ ); } if ( $descText ) { @@ -560,7 +645,7 @@ EOT * external editing (and instructions link) etc. */ protected function uploadLinksBox() { - global $wgUser, $wgOut, $wgEnableUploads, $wgUseExternalEditor; + global $wgEnableUploads, $wgUseExternalEditor; if ( !$wgEnableUploads ) { return; @@ -571,35 +656,38 @@ EOT return; } - $wgOut->addHTML( "<br /><ul>\n" ); + $out = $this->getContext()->getOutput(); + $out->addHTML( "<ul>\n" ); # "Upload a new version of this file" link - if ( UploadBase::userCanReUpload( $wgUser, $this->mPage->getFile()->name ) ) { - $ulink = Linker::makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) ); - $wgOut->addHTML( "<li id=\"mw-imagepage-reupload-link\"><div class=\"plainlinks\">{$ulink}</div></li>\n" ); + $canUpload = $this->getTitle()->userCan( 'upload', $this->getContext()->getUser() ); + if ( $canUpload && UploadBase::userCanReUpload( $this->getContext()->getUser(), $this->mPage->getFile()->name ) ) { + $ulink = Linker::makeExternalLink( $this->getUploadUrl(), wfMessage( 'uploadnewversion-linktext' )->text() ); + $out->addHTML( "<li id=\"mw-imagepage-reupload-link\"><div class=\"plainlinks\">{$ulink}</div></li>\n" ); + } else { + $out->addHTML( "<li id=\"mw-imagepage-upload-disallowed\">" . $this->getContext()->msg( 'upload-disallowed-here' )->escaped() . "</li>\n" ); } # External editing link if ( $wgUseExternalEditor ) { - $elink = Linker::link( + $elink = Linker::linkKnown( $this->getTitle(), - wfMsgHtml( 'edit-externally' ), + wfMessage( 'edit-externally' )->escaped(), array(), array( 'action' => 'edit', 'externaledit' => 'true', 'mode' => 'file' - ), - array( 'known', 'noclasses' ) + ) ); - $wgOut->addHTML( + $out->addHTML( '<li id="mw-imagepage-edit-external">' . $elink . ' <small>' . - wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . - "</small></li>\n" + wfMessage( 'edit-externally-help' )->parse() . + "</small></li>\n" ); } - $wgOut->addHTML( "</ul>\n" ); + $out->addHTML( "</ul>\n" ); } protected function closeShowImage() { } # For overloading @@ -609,12 +697,11 @@ EOT * we follow it with an upload history of the image and its usage. */ protected function imageHistory() { - global $wgOut; - $this->loadFile(); + $out = $this->getContext()->getOutput(); $pager = new ImageHistoryPseudoPager( $this ); - $wgOut->addHTML( $pager->getBody() ); - $wgOut->preventClickjacking( $pager->getPreventClickjacking() ); + $out->addHTML( $pager->getBody() ); + $out->preventClickjacking( $pager->getPreventClickjacking() ); $this->mPage->getFile()->resetHistory(); // free db resources @@ -643,10 +730,9 @@ EOT } protected function imageLinks() { - global $wgOut, $wgLang; - $limit = 100; + $out = $this->getContext()->getOutput(); $res = $this->queryImageLinks( $this->getTitle()->getDbKey(), $limit + 1); $rows = array(); $redirects = array(); @@ -670,7 +756,7 @@ EOT } if ( $count == 0 ) { - $wgOut->wrapWikiMsg( + $out->wrapWikiMsg( Html::rawElement( 'div', array( 'id' => 'mw-imagepage-nolinkstoimage' ), "\n$1\n" ), 'nolinkstoimage' @@ -678,18 +764,18 @@ EOT return; } - $wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" ); + $out->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" ); if ( !$hasMore ) { - $wgOut->addWikiMsg( 'linkstoimage', $count ); + $out->addWikiMsg( 'linkstoimage', $count ); } else { // More links than the limit. Add a link to [[Special:Whatlinkshere]] - $wgOut->addWikiMsg( 'linkstoimage-more', - $wgLang->formatNum( $limit ), + $out->addWikiMsg( 'linkstoimage-more', + $this->getContext()->getLanguage()->formatNum( $limit ), $this->getTitle()->getPrefixedDBkey() ); } - $wgOut->addHTML( + $out->addHTML( Html::openElement( 'ul', array( 'class' => 'mw-imagepage-linkstoimage' ) ) . "\n" ); @@ -720,7 +806,7 @@ EOT $link2 = Linker::linkKnown( Title::makeTitle( $row->page_namespace, $row->page_title ) ); $ul .= Html::rawElement( 'li', - array( 'id' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), + array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), $link2 ) . "\n"; } @@ -728,39 +814,38 @@ EOT $liContents = wfMessage( 'linkstoimage-redirect' )->rawParams( $link, $ul )->parse(); } - $wgOut->addHTML( Html::rawElement( + $out->addHTML( Html::rawElement( 'li', - array( 'id' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), + array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ), $liContents ) . "\n" ); }; - $wgOut->addHTML( Html::closeElement( 'ul' ) . "\n" ); + $out->addHTML( Html::closeElement( 'ul' ) . "\n" ); $res->free(); // Add a links to [[Special:Whatlinkshere]] if ( $count > $limit ) { - $wgOut->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() ); + $out->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() ); } - $wgOut->addHTML( Html::closeElement( 'div' ) . "\n" ); + $out->addHTML( Html::closeElement( 'div' ) . "\n" ); } protected function imageDupes() { - global $wgOut, $wgLang; - $this->loadFile(); + $out = $this->getContext()->getOutput(); $dupes = $this->mPage->getDuplicates(); if ( count( $dupes ) == 0 ) { return; } - $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" ); - $wgOut->addWikiMsg( 'duplicatesoffile', - $wgLang->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey() + $out->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" ); + $out->addWikiMsg( 'duplicatesoffile', + $this->getContext()->getLanguage()->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey() ); - $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); + $out->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); /** * @var $file File @@ -768,21 +853,15 @@ EOT foreach ( $dupes as $file ) { $fromSrc = ''; if ( $file->isLocal() ) { - $link = Linker::link( - $file->getTitle(), - null, - array(), - array(), - array( 'known', 'noclasses' ) - ); + $link = Linker::linkKnown( $file->getTitle() ); } else { $link = Linker::makeExternalLink( $file->getDescriptionUrl(), $file->getTitle()->getPrefixedText() ); - $fromSrc = wfMsg( 'shared-repo-from', $file->getRepo()->getDisplayName() ); + $fromSrc = wfMessage( 'shared-repo-from', $file->getRepo()->getDisplayName() )->text(); } - $wgOut->addHTML( "<li>{$link} {$fromSrc}</li>\n" ); + $out->addHTML( "<li>{$link} {$fromSrc}</li>\n" ); } - $wgOut->addHTML( "</ul></div>\n" ); + $out->addHTML( "</ul></div>\n" ); } /** @@ -806,12 +885,12 @@ EOT * @param $description String */ function showError( $description ) { - global $wgOut; - $wgOut->setPageTitle( wfMessage( 'internalerror' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setArticleRelated( false ); - $wgOut->enableClientCache( false ); - $wgOut->addWikiText( $description ); + $out = $this->getContext()->getOutput(); + $out->setPageTitle( wfMessage( 'internalerror' ) ); + $out->setRobotPolicy( 'noindex,nofollow' ); + $out->setArticleRelated( false ); + $out->enableClientCache( false ); + $out->addWikiText( $description ); } /** @@ -836,7 +915,7 @@ EOT * * @ingroup Media */ -class ImageHistoryList { +class ImageHistoryList extends ContextSource { /** * @var Title @@ -871,6 +950,7 @@ class ImageHistoryList { $this->title = $imagePage->getTitle(); $this->imagePage = $imagePage; $this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender(); + $this->setContext( $imagePage->getContext() ); } /** @@ -892,19 +972,18 @@ class ImageHistoryList { * @return string */ public function beginImageHistoryList( $navLinks = '' ) { - global $wgOut, $wgUser; - return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) . "\n" + return Xml::element( 'h2', array( 'id' => 'filehistory' ), $this->msg( 'filehist' )->text() ) . "\n" . "<div id=\"mw-imagepage-section-filehistory\">\n" - . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) ) + . $this->msg( 'filehist-help' )->parseAsBlock() . $navLinks . "\n" . Xml::openElement( 'table', array( 'class' => 'wikitable filehistory' ) ) . "\n" . '<tr><td></td>' - . ( $this->current->isLocal() && ( $wgUser->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' ) - . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>' - . ( $this->showThumb ? '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>' : '' ) - . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>' - . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>' - . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>' + . ( $this->current->isLocal() && ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' ) + . '<th>' . $this->msg( 'filehist-datetime' )->escaped() . '</th>' + . ( $this->showThumb ? '<th>' . $this->msg( 'filehist-thumb' )->escaped() . '</th>' : '' ) + . '<th>' . $this->msg( 'filehist-dimensions' )->escaped() . '</th>' + . '<th>' . $this->msg( 'filehist-user' )->escaped() . '</th>' + . '<th>' . $this->msg( 'filehist-comment' )->escaped() . '</th>' . "</tr>\n"; } @@ -922,43 +1001,45 @@ class ImageHistoryList { * @return string */ public function imageHistoryLine( $iscur, $file ) { - global $wgUser, $wgLang, $wgContLang; + global $wgContLang; + $user = $this->getUser(); + $lang = $this->getLanguage(); $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() ); $img = $iscur ? $file->getName() : $file->getArchiveName(); - $user = $file->getUser( 'id' ); - $usertext = $file->getUser( 'text' ); - $description = $file->getDescription(); + $userId = $file->getUser( 'id' ); + $userText = $file->getUser( 'text' ); + $description = $file->getDescription( File::FOR_THIS_USER, $user ); $local = $this->current->isLocal(); $row = $selected = ''; // Deletion link - if ( $local && ( $wgUser->isAllowedAny( 'delete', 'deletedhistory' ) ) ) { + if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) { $row .= '<td>'; # Link to remove from history - if ( $wgUser->isAllowed( 'delete' ) ) { + if ( $user->isAllowed( 'delete' ) ) { $q = array( 'action' => 'delete' ); if ( !$iscur ) { $q['oldimage'] = $img; } - $row .= Linker::link( + $row .= Linker::linkKnown( $this->title, - wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ), - array(), $q, array( 'known' ) + $this->msg( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' )->escaped(), + array(), $q ); } # Link to hide content. Don't show useless link to people who cannot hide revisions. - $canHide = $wgUser->isAllowed( 'deleterevision' ); - if ( $canHide || ( $wgUser->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) { - if ( $wgUser->isAllowed( 'delete' ) ) { + $canHide = $user->isAllowed( 'deleterevision' ); + if ( $canHide || ( $user->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) { + if ( $user->isAllowed( 'delete' ) ) { $row .= '<br />'; } // If file is top revision or locked from this user, don't link - if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED ) ) { + if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED, $user ) ) { $del = Linker::revDeleteLinkDisabled( $canHide ); } else { - list( $ts, $name ) = explode( '!', $img, 2 ); + list( $ts, ) = explode( '!', $img, 2 ); $query = array( 'type' => 'oldimage', 'target' => $this->title->getPrefixedText(), @@ -975,21 +1056,22 @@ class ImageHistoryList { // Reversion link/current indicator $row .= '<td>'; if ( $iscur ) { - $row .= wfMsgHtml( 'filehist-current' ); - } elseif ( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) { + $row .= $this->msg( 'filehist-current' )->escaped(); + } elseif ( $local && $this->title->quickUserCan( 'edit', $user ) + && $this->title->quickUserCan( 'upload', $user ) + ) { if ( $file->isDeleted( File::DELETED_FILE ) ) { - $row .= wfMsgHtml( 'filehist-revert' ); + $row .= $this->msg( 'filehist-revert' )->escaped(); } else { - $row .= Linker::link( + $row .= Linker::linkKnown( $this->title, - wfMsgHtml( 'filehist-revert' ), + $this->msg( 'filehist-revert' )->escaped(), array(), array( 'action' => 'revert', 'oldimage' => $img, - 'wpEditToken' => $wgUser->getEditToken( $img ) - ), - array( 'known', 'noclasses' ) + 'wpEditToken' => $user->getEditToken( $img ) + ) ); } } @@ -1000,32 +1082,31 @@ class ImageHistoryList { $selected = "class='filehistory-selected'"; } $row .= "<td $selected style='white-space: nowrap;'>"; - if ( !$file->userCan( File::DELETED_FILE ) ) { + if ( !$file->userCan( File::DELETED_FILE, $user ) ) { # Don't link to unviewable files - $row .= '<span class="history-deleted">' . $wgLang->timeanddate( $timestamp, true ) . '</span>'; + $row .= '<span class="history-deleted">' . $lang->userTimeAndDate( $timestamp, $user ) . '</span>'; } elseif ( $file->isDeleted( File::DELETED_FILE ) ) { if ( $local ) { $this->preventClickjacking(); $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); # Make a link to review the image - $url = Linker::link( + $url = Linker::linkKnown( $revdel, - $wgLang->timeanddate( $timestamp, true ), + $lang->userTimeAndDate( $timestamp, $user ), array(), array( 'target' => $this->title->getPrefixedText(), 'file' => $img, - 'token' => $wgUser->getEditToken( $img ) - ), - array( 'known', 'noclasses' ) + 'token' => $user->getEditToken( $img ) + ) ); } else { - $url = $wgLang->timeanddate( $timestamp, true ); + $url = $lang->userTimeAndDate( $timestamp, $user ); } $row .= '<span class="history-deleted">' . $url . '</span>'; } else { $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img ); - $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeanddate( $timestamp, true ) ); + $row .= Xml::element( 'a', array( 'href' => $url ), $lang->userTimeAndDate( $timestamp, $user ) ); } $row .= "</td>"; @@ -1037,27 +1118,33 @@ class ImageHistoryList { // Image dimensions + size $row .= '<td>'; $row .= htmlspecialchars( $file->getDimensionsString() ); - $row .= ' <span style="white-space: nowrap;">(' . Linker::formatSize( $file->getSize() ) . ')</span>'; + $row .= $this->msg( 'word-separator' )->plain(); + $row .= '<span style="white-space: nowrap;">'; + $row .= $this->msg( 'parentheses' )->rawParams( Linker::formatSize( $file->getSize() ) )->plain(); + $row .= '</span>'; $row .= '</td>'; // Uploading user $row .= '<td>'; // Hide deleted usernames if ( $file->isDeleted( File::DELETED_USER ) ) { - $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; + $row .= '<span class="history-deleted">' . $this->msg( 'rev-deleted-user' )->escaped() . '</span>'; } else { if ( $local ) { - $row .= Linker::userLink( $user, $usertext ) . ' <span style="white-space: nowrap;">' . - Linker::userToolLinks( $user, $usertext ) . '</span>'; + $row .= Linker::userLink( $userId, $userText ); + $row .= $this->msg( 'word-separator' )->plain(); + $row .= '<span style="white-space: nowrap;">'; + $row .= Linker::userToolLinks( $userId, $userText ); + $row .= '</span>'; } else { - $row .= htmlspecialchars( $usertext ); + $row .= htmlspecialchars( $userText ); } } $row .= '</td>'; // Don't show deleted descriptions if ( $file->isDeleted( File::DELETED_COMMENT ) ) { - $row .= '<td><span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></td>'; + $row .= '<td><span class="history-deleted">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span></td>'; } else { $row .= '<td dir="' . $wgContLang->getDir() . '">' . Linker::formatComment( $description, $this->title ) . '</td>'; } @@ -1074,9 +1161,11 @@ class ImageHistoryList { * @return string */ protected function getThumbForLine( $file ) { - global $wgLang; - - if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) { + $lang = $this->getLanguage(); + $user = $this->getUser(); + if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE,$user ) + && !$file->isDeleted( File::DELETED_FILE ) ) + { $params = array( 'width' => '120', 'height' => '120', @@ -1085,20 +1174,20 @@ class ImageHistoryList { $thumbnail = $file->transform( $params ); $options = array( - 'alt' => wfMsg( 'filehist-thumbtext', - $wgLang->timeanddate( $timestamp, true ), - $wgLang->date( $timestamp, true ), - $wgLang->time( $timestamp, true ) ), + 'alt' => $this->msg( 'filehist-thumbtext', + $lang->userTimeAndDate( $timestamp, $user ), + $lang->userDate( $timestamp, $user ), + $lang->userTime( $timestamp, $user ) )->text(), 'file-link' => true, ); if ( !$thumbnail ) { - return wfMsgHtml( 'filehist-nothumb' ); + return $this->msg( 'filehist-nothumb' )->escaped(); } return $thumbnail->toHtml( $options ); } else { - return wfMsgHtml( 'filehist-nothumb' ); + return $this->msg( 'filehist-nothumb' )->escaped(); } } @@ -1162,6 +1251,7 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { } /** + * @param $row object * @return string */ function formatRow( $row ) { diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php index f46974b2..f9f6ceed 100644 --- a/includes/ImageQueryPage.php +++ b/includes/ImageQueryPage.php @@ -1,4 +1,25 @@ <?php +/** + * Variant of QueryPage which uses a gallery to output results. + * + * 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 SpecialPage + */ /** * Variant of QueryPage which uses a gallery to output results, thus diff --git a/includes/Import.php b/includes/Import.php index e906c7f0..11f37952 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -1,6 +1,6 @@ <?php /** - * MediaWiki page data importer + * MediaWiki page data importer. * * Copyright © 2003,2005 Brion Vibber <brion@pobox.com> * http://www.mediawiki.org/ @@ -33,7 +33,7 @@ class WikiImporter { private $reader = null; private $mLogItemCallback, $mUploadCallback, $mRevisionCallback, $mPageCallback; - private $mSiteInfoCallback, $mTargetNamespace, $mPageOutCallback; + private $mSiteInfoCallback, $mTargetNamespace, $mTargetRootPage, $mPageOutCallback; private $mNoticeCallback, $mDebug; private $mImportUploads, $mImageBasePath; private $mNoUpdates = false; @@ -200,6 +200,39 @@ class WikiImporter { } /** + * Set a target root page under which all pages are imported + * @param $rootpage + * @return status object + */ + public function setTargetRootPage( $rootpage ) { + $status = Status::newGood(); + if( is_null( $rootpage ) ) { + // No rootpage + $this->mTargetRootPage = null; + } elseif( $rootpage !== '' ) { + $rootpage = rtrim( $rootpage, '/' ); //avoid double slashes + $title = Title::newFromText( $rootpage, !is_null( $this->mTargetNamespace ) ? $this->mTargetNamespace : NS_MAIN ); + if( !$title || $title->isExternal() ) { + $status->fatal( 'import-rootpage-invalid' ); + } else { + if( !MWNamespace::hasSubpages( $title->getNamespace() ) ) { + global $wgContLang; + + $displayNSText = $title->getNamespace() == NS_MAIN + ? wfMessage( 'blanknamespace' )->text() + : $wgContLang->getNsText( $title->getNamespace() ); + $status->fatal( 'import-rootpage-nosubpage', $displayNSText ); + } else { + // set namespace to 'all', so the namespace check in processTitle() can passed + $this->setTargetNamespace( null ); + $this->mTargetRootPage = $title->getPrefixedDBKey(); + } + } + } + return $status; + } + + /** * @param $dir */ public function setImageBasePath( $dir ) { @@ -275,7 +308,7 @@ class WikiImporter { } /** - * Notify the callback function when a new <page> is reached. + * Notify the callback function when a new "<page>" is reached. * @param $title Title */ function pageCallback( $title ) { @@ -285,7 +318,7 @@ class WikiImporter { } /** - * Notify the callback function when a </page> is closed. + * Notify the callback function when a "</page>" is closed. * @param $title Title * @param $origTitle Title * @param $revCount Integer @@ -301,7 +334,8 @@ class WikiImporter { /** * Notify the callback function of a revision - * @param $revision A WikiRevision object + * @param $revision WikiRevision object + * @return bool|mixed */ private function revisionCallback( $revision ) { if ( isset( $this->mRevisionCallback ) ) { @@ -314,7 +348,8 @@ class WikiImporter { /** * Notify the callback function of a new log item - * @param $revision A WikiRevision object + * @param $revision WikiRevision object + * @return bool|mixed */ private function logItemCallback( $revision ) { if ( isset( $this->mLogItemCallback ) ) { @@ -394,6 +429,7 @@ class WikiImporter { /** * Primary entry point + * @return bool */ public function doImport() { $this->reader->read(); @@ -783,9 +819,14 @@ class WikiImporter { $origTitle = Title::newFromText( $workTitle ); if( !is_null( $this->mTargetNamespace ) && !is_null( $origTitle ) ) { - $title = Title::makeTitle( $this->mTargetNamespace, + # makeTitleSafe, because $origTitle can have a interwiki (different setting of interwiki map) + # and than dbKey can begin with a lowercase char + $title = Title::makeTitleSafe( $this->mTargetNamespace, $origTitle->getDBkey() ); } else { + if( !is_null( $this->mTargetRootPage ) ) { + $workTitle = $this->mTargetRootPage . '/' . $workTitle; + } $title = Title::newFromText( $workTitle ); } @@ -826,7 +867,7 @@ class UploadSourceAdapter { * @return string */ static function registerSource( $source ) { - $id = wfGenerateToken(); + $id = wfRandomString(); self::$sourceRegistrations[$id] = $source; diff --git a/includes/Init.php b/includes/Init.php index 72c10543..a8540f2c 100644 --- a/includes/Init.php +++ b/includes/Init.php @@ -1,4 +1,24 @@ <?php +/** + * Some functions that are useful during startup. + * + * 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 + */ /** * Some functions that are useful during startup. @@ -197,6 +217,7 @@ class MWInit { * @param $methodName string * @param $args array * + * @return mixed */ static function callStaticMethod( $className, $methodName, $args ) { $r = new ReflectionMethod( $className, $methodName ); diff --git a/includes/Licenses.php b/includes/Licenses.php index 8a06c6fc..ba504a99 100644 --- a/includes/Licenses.php +++ b/includes/Licenses.php @@ -1,14 +1,32 @@ <?php /** - * A License class for use on Special:Upload + * License selector for use on Special:Upload. * - * @ingroup SpecialPage + * 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 SpecialPage * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ +/** + * A License class for use on Special:Upload + */ class Licenses extends HTMLFormField { /** * @var string @@ -34,7 +52,7 @@ class Licenses extends HTMLFormField { public function __construct( $params ) { parent::__construct( $params ); - $this->msg = empty( $params['licenses'] ) ? wfMsgForContent( 'licenses' ) : $params['licenses']; + $this->msg = empty( $params['licenses'] ) ? wfMessage( 'licenses' )->inContentLanguage()->plain() : $params['licenses']; $this->selected = null; $this->makeLicenses(); @@ -102,7 +120,7 @@ class Licenses extends HTMLFormField { foreach ( $tagset as $key => $val ) if ( is_array( $val ) ) { $this->html .= $this->outputOption( - $this->msg( $key ), '', + $key, '', array( 'disabled' => 'disabled', 'style' => 'color: GrayText', // for MSIE @@ -112,7 +130,7 @@ class Licenses extends HTMLFormField { $this->makeHtml( $val, $depth + 1 ); } else { $this->html .= $this->outputOption( - $this->msg( $val->text ), $val->template, + $val->text, $val->template, array( 'title' => '{{' . $val->template . '}}' ), $depth ); @@ -120,13 +138,15 @@ class Licenses extends HTMLFormField { } /** - * @param $text + * @param $message * @param $value * @param $attribs null * @param $depth int * @return string */ - protected function outputOption( $text, $value, $attribs = null, $depth = 0 ) { + protected function outputOption( $message, $value, $attribs = null, $depth = 0 ) { + $msgObj = $this->msg( $message ); + $text = $msgObj->exists() ? $msgObj->text() : $message; $attribs['value'] = $value; if ( $value === $this->selected ) $attribs['selected'] = 'selected'; @@ -134,15 +154,6 @@ class Licenses extends HTMLFormField { return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n"; } - /** - * @param $str string - * @return String - */ - protected function msg( $str ) { - $msg = wfMessage( $str ); - return $msg->exists() ? $msg->text() : $str; - } - /**#@-*/ /** @@ -164,7 +175,7 @@ class Licenses extends HTMLFormField { public function getInputHTML( $value ) { $this->selected = $value; - $this->html = $this->outputOption( wfMsg( 'nolicense' ), '', + $this->html = $this->outputOption( wfMessage( 'nolicense' )->text(), '', (bool)$this->selected ? null : array( 'selected' => 'selected' ) ); $this->makeHtml( $this->getLicenses() ); diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index af7680fb..214f4959 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -1,4 +1,25 @@ <?php +/** + * Functions to help implement an external link filter for spam control. + * + * 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 + */ + /** * Some functions to help implement an external link filter for spam control. @@ -120,7 +141,7 @@ class LinkFilter { * Filters an array returned by makeLikeArray(), removing everything past first pattern placeholder. * * @param $arr array: array to filter - * @return filtered array + * @return array filtered array */ public static function keepOneWildcard( $arr ) { if( !is_array( $arr ) ) { diff --git a/includes/Linker.php b/includes/Linker.php index 575f2841..56626bd7 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1,5 +1,26 @@ <?php /** + * Methods to make links and related items. + * + * 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 + */ + +/** * Some internal bits split of from Skin.php. These functions are used * for primarily page content: links, embedded images, table of contents. Links * are also used in the skin. @@ -20,6 +41,7 @@ class Linker { * * @param $class String: the contents of the class attribute; if an empty * string is passed, which is the default value, defaults to 'external'. + * @return string * @deprecated since 1.18 Just pass the external class directly to something using Html::expandAttributes */ static function getExternalLinkAttributes( $class = 'external' ) { @@ -36,6 +58,7 @@ class Linker { * @param $unused String: unused * @param $class String: the contents of the class attribute; if an empty * string is passed, which is the default value, defaults to 'external'. + * @return string */ static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) { global $wgContLang; @@ -57,6 +80,7 @@ class Linker { * not HTML-escaped * @param $unused String: unused * @param $class String: the contents of the class attribute, default none + * @return string */ static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) { $title = urldecode( $title ); @@ -73,6 +97,7 @@ class Linker { * @param $class String: the contents of the class attribute, default none * @param $title Mixed: optional (unescaped) string to use in the title * attribute; if false, default to the name of the page we're linking to + * @return string */ static function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) { if ( $title === false ) { @@ -114,9 +139,9 @@ class Linker { if ( $t->isRedirect() ) { # Page is a redirect $colour = 'mw-redirect'; - } elseif ( $threshold > 0 && - $t->exists() && $t->getLength() < $threshold && - $t->isContentPage() ) { + } elseif ( $threshold > 0 && $t->isContentPage() && + $t->exists() && $t->getLength() < $threshold + ) { # Page is a stub $colour = 'stub'; } @@ -173,6 +198,12 @@ class Linker { wfProfileOut( __METHOD__ ); return "<!-- ERROR -->$html"; } + + if( is_string( $query ) ) { + // some functions withing core using this still hand over query strings + wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' ); + $query = wfCgiToArray( $query ); + } $options = (array)$options; $dummy = new DummyLinker; // dummy linker instance for bc on the hooks @@ -229,6 +260,7 @@ class Linker { /** * Identical to link(), except $options defaults to 'known'. + * @return string */ public static function linkKnown( $target, $html = null, $customAttribs = array(), @@ -243,6 +275,7 @@ class Linker { * @param $target Title * @param $query Array: query parameters * @param $options Array + * @return String */ private static function linkUrl( $target, $query, $options ) { wfProfileIn( __METHOD__ ); @@ -312,7 +345,7 @@ class Linker { } elseif ( in_array( 'known', $options ) ) { $defaults['title'] = $target->getPrefixedText(); } else { - $defaults['title'] = wfMsg( 'red-link-title', $target->getPrefixedText() ); + $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )->text(); } # Finally, merge the custom attribs with the default ones, and iterate @@ -380,6 +413,11 @@ class Linker { * despite $query not being used. * * @param $nt Title + * @param $html String [optional] + * @param $query String [optional] + * @param $trail String [optional] + * @param $prefix String [optional] + * * * @return string */ @@ -392,6 +430,31 @@ class Linker { } /** + * Get a message saying that an invalid title was encountered. + * This should be called after a method like Title::makeTitleSafe() returned + * a value indicating that the title object is invalid. + * + * @param $context IContextSource context to use to get the messages + * @param $namespace int Namespace number + * @param $title string Text of the title, without the namespace part + */ + public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) { + global $wgContLang; + + // First we check whether the namespace exists or not. + if ( MWNamespace::exists( $namespace ) ) { + if ( $namespace == NS_MAIN ) { + $name = $context->msg( 'blanknamespace' )->text(); + } else { + $name = $wgContLang->getFormattedNsText( $namespace ); + } + return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text(); + } else { + return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text(); + } + } + + /** * @param $title Title * @return Title */ @@ -456,7 +519,8 @@ class Linker { * Given parameters derived from [[Image:Foo|options...]], generate the * HTML that that syntax inserts in the page. * - * @param $title Title object + * @param $parser Parser object + * @param $title Title object of the file (not the currently viewed page) * @param $file File object, or false if it doesn't exist * @param $frameParams Array: associative array of parameters external to the media handler. * Boolean parameters are indicated by presence or absence, the value is arbitrary and @@ -472,6 +536,7 @@ class Linker { * valign Vertical alignment (baseline, sub, super, top, text-top, middle, * bottom, text-bottom) * alt Alternate text for image (i.e. alt attribute). Plain text. + * class HTML for image classes. Plain text. * caption HTML for image caption. * link-url URL to link to * link-title Title object to link to @@ -483,9 +548,10 @@ class Linker { * @param $time String: timestamp of the file, set as false for current * @param $query String: query params for desc url * @param $widthOption: Used by the parser to remember the user preference thumbnailsize + * @since 1.20 * @return String: HTML for an image, with links, wrappers, etc. */ - public static function makeImageLink2( Title $title, $file, $frameParams = array(), + public static function makeImageLink( /*Parser*/ $parser, Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false, $query = "", $widthOption = null ) { $res = null; @@ -515,6 +581,9 @@ class Linker { if ( !isset( $fp['title'] ) ) { $fp['title'] = ''; } + if ( !isset( $fp['class'] ) ) { + $fp['class'] = ''; + } $prefix = $postfix = ''; @@ -558,16 +627,20 @@ class Linker { } if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) { - global $wgContLang; - # Create a thumbnail. Alignment depends on language - # writing direction, # right aligned for left-to-right- - # languages ("Western languages"), left-aligned - # for right-to-left-languages ("Semitic languages") + # Create a thumbnail. Alignment depends on the writing direction of + # the page content language (right-aligned for LTR languages, + # left-aligned for RTL languages) # - # If thumbnail width has not been provided, it is set + # If a thumbnail width has not been provided, it is set # to the default user option as specified in Language*.php if ( $fp['align'] == '' ) { - $fp['align'] = $wgContLang->alignEnd(); + if( $parser instanceof Parser ) { + $fp['align'] = $parser->getTargetLanguage()->alignEnd(); + } else { + # backwards compatibility, remove with makeImageLink2() + global $wgContLang; + $fp['align'] = $wgContLang->alignEnd(); + } } return $prefix . self::makeThumbLink2( $title, $file, $fp, $hp, $time, $query ) . $postfix; } @@ -594,9 +667,12 @@ class Linker { $params = array( 'alt' => $fp['alt'], 'title' => $fp['title'], - 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false , - 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ); - $params = self::getImageLinkMTOParams( $fp, $query ) + $params; + 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false, + 'img-class' => $fp['class'] ); + if ( isset( $fp['border'] ) ) { + $params['img-class'] .= ( $params['img-class'] !== '' ) ? ' thumbborder' : 'thumbborder'; + } + $params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params; $s = $thumb->toHtml( $params ); } @@ -607,18 +683,37 @@ class Linker { } /** + * See makeImageLink() + * When this function is removed, remove if( $parser instanceof Parser ) check there too + * @deprecated since 1.20 + */ + public static function makeImageLink2( Title $title, $file, $frameParams = array(), + $handlerParams = array(), $time = false, $query = "", $widthOption = null ) { + return self::makeImageLink( null, $title, $file, $frameParams, + $handlerParams, $time, $query, $widthOption ); + } + + /** * Get the link parameters for MediaTransformOutput::toHtml() from given * frame parameters supplied by the Parser. - * @param $frameParams The frame parameters - * @param $query An optional query string to add to description page links + * @param $frameParams array The frame parameters + * @param $query string An optional query string to add to description page links + * @return array */ - private static function getImageLinkMTOParams( $frameParams, $query = '' ) { + private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) { $mtoParams = array(); if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) { $mtoParams['custom-url-link'] = $frameParams['link-url']; if ( isset( $frameParams['link-target'] ) ) { $mtoParams['custom-target-link'] = $frameParams['link-target']; } + if ( $parser ) { + $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] ); + foreach ( $extLinkAttrs as $name => $val ) { + // Currently could include 'rel' and 'target' + $mtoParams['parser-extlink-'.$name] = $val; + } + } } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) { $mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] ); } elseif ( !empty( $frameParams['no-link'] ) ) { @@ -640,6 +735,7 @@ class Linker { * @param $params Array * @param $framed Boolean * @param $manualthumb String + * @return mixed */ public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt, $align = 'right', $params = array(), $framed = false , $manualthumb = "" ) @@ -736,13 +832,14 @@ class Linker { $s .= self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true ); $zoomIcon = ''; } elseif ( !$thumb ) { - $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); + $s .= wfMessage( 'thumbnail_error', '' )->escaped(); $zoomIcon = ''; } else { $params = array( 'alt' => $fp['alt'], 'title' => $fp['title'], - 'img-class' => 'thumbimage' ); + 'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== '' ) ? $fp['class'] . ' thumbimage' : 'thumbimage' + ); $params = self::getImageLinkMTOParams( $fp, $query ) + $params; $s .= $thumb->toHtml( $params ); if ( isset( $fp['framed'] ) ) { @@ -752,7 +849,7 @@ class Linker { Html::rawElement( 'a', array( 'href' => $url, 'class' => 'internal', - 'title' => wfMsg( 'thumbnail-more' ) ), + 'title' => wfMessage( 'thumbnail-more' )->text() ), Html::element( 'img', array( 'src' => $wgStylePath . '/common/images/magnify-clip' . ( $wgContLang->isRTL() ? '-rtl' : '' ) . '.png', 'width' => 15, @@ -848,7 +945,7 @@ class Linker { * This will make a broken link if $file is false. * * @param $title Title object. - * @param $file File|false mixed File object or false + * @param $file File|bool mixed File object or false * @param $html String: pre-sanitized HTML * @return String: HTML * @@ -882,7 +979,7 @@ class Linker { $key = strtolower( $name ); } - return self::linkKnown( SpecialPage::getTitleFor( $name ) , wfMsg( $key ) ); + return self::linkKnown( SpecialPage::getTitleFor( $name ) , wfMessage( $key )->text() ); } /** @@ -892,6 +989,7 @@ class Linker { * @param $escape Boolean: do we escape the link text? * @param $linktype String: type of external link. Gets added to the classes * @param $attribs Array of extra attributes to <a> + * @return string */ public static function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) { $class = "external"; @@ -923,7 +1021,7 @@ class Linker { * @param $userName String: user name in database. * @param $altUserName String: text to display instead of the user name (optional) * @return String: HTML fragment - * @since 1.19 Method exists for a long time. $displayText was added in 1.19. + * @since 1.19 Method exists for a long time. $altUserName was added in 1.19. */ public static function userLink( $userId, $userName, $altUserName = false ) { if ( $userId == 0 ) { @@ -973,7 +1071,7 @@ class Linker { } $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); - $items[] = self::link( $contribsPage, wfMsgHtml( 'contribslink' ), $attribs ); + $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs ); } if ( $blockable && $wgUser->isAllowed( 'block' ) ) { $items[] = self::blockLink( $userId, $userText ); @@ -986,7 +1084,10 @@ class Linker { wfRunHooks( 'UserToolLinksEdit', array( $userId, $userText, &$items ) ); if ( $items ) { - return ' <span class="mw-usertoollinks">(' . $wgLang->pipeList( $items ) . ')</span>'; + return wfMessage( 'word-separator' )->plain() + . '<span class="mw-usertoollinks">' + . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped() + . '</span>'; } else { return ''; } @@ -997,6 +1098,7 @@ class Linker { * @param $userId Integer: user identifier * @param $userText String: user name or IP address * @param $edits Integer: user edit count (optional, for performance) + * @return String */ public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) { return self::userToolLinks( $userId, $userText, true, 0, $edits ); @@ -1010,7 +1112,7 @@ class Linker { */ public static function userTalkLink( $userId, $userText ) { $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText ); - $userTalkLink = self::link( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) ); + $userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() ); return $userTalkLink; } @@ -1021,7 +1123,7 @@ class Linker { */ public static function blockLink( $userId, $userText ) { $blockPage = SpecialPage::getTitleFor( 'Block', $userText ); - $blockLink = self::link( $blockPage, wfMsgHtml( 'blocklink' ) ); + $blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() ); return $blockLink; } @@ -1032,7 +1134,7 @@ class Linker { */ public static function emailLink( $userId, $userText ) { $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText ); - $emailLink = self::link( $emailPage, wfMsgHtml( 'emaillink' ) ); + $emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() ); return $emailLink; } @@ -1044,12 +1146,12 @@ class Linker { */ public static function revUserLink( $rev, $isPublic = false ) { if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) { - $link = wfMsgHtml( 'rev-deleted-user' ); + $link = wfMessage( 'rev-deleted-user' )->escaped(); } elseif ( $rev->userCan( Revision::DELETED_USER ) ) { $link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ), $rev->getUserText( Revision::FOR_THIS_USER ) ); } else { - $link = wfMsgHtml( 'rev-deleted-user' ); + $link = wfMessage( 'rev-deleted-user' )->escaped(); } if ( $rev->isDeleted( Revision::DELETED_USER ) ) { return '<span class="history-deleted">' . $link . '</span>'; @@ -1065,14 +1167,15 @@ class Linker { */ public static function revUserTools( $rev, $isPublic = false ) { if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) { - $link = wfMsgHtml( 'rev-deleted-user' ); + $link = wfMessage( 'rev-deleted-user' )->escaped(); } elseif ( $rev->userCan( Revision::DELETED_USER ) ) { $userId = $rev->getUser( Revision::FOR_THIS_USER ); $userText = $rev->getUserText( Revision::FOR_THIS_USER ); - $link = self::userLink( $userId, $userText ) . - ' ' . self::userToolLinks( $userId, $userText ); + $link = self::userLink( $userId, $userText ) + . wfMessage( 'word-separator' )->plain() + . self::userToolLinks( $userId, $userText ); } else { - $link = wfMsgHtml( 'rev-deleted-user' ); + $link = wfMessage( 'rev-deleted-user' )->escaped(); } if ( $rev->isDeleted( Revision::DELETED_USER ) ) { return ' <span class="history-deleted">' . $link . '</span>'; @@ -1095,6 +1198,7 @@ class Linker { * @param $comment String * @param $title Mixed: Title object (to generate link to the section in autocomment) or null * @param $local Boolean: whether section links should refer to local page + * @return mixed|String */ public static function formatComment( $comment, $title = null, $local = false ) { wfProfileIn( __METHOD__ ); @@ -1126,7 +1230,7 @@ class Linker { * Called by Linker::formatComment. * * @param $comment String: comment text - * @param $title An optional title object used to links to sections + * @param $title Title|null An optional title object used to links to sections * @param $local Boolean: whether section links should refer to local page * @return String: formatted comment */ @@ -1155,41 +1259,45 @@ class Linker { $pre = $match[1]; $auto = $match[2]; $post = $match[3]; - $link = ''; - if ( $title ) { - $section = $auto; - - # Remove links that a user may have manually put in the autosummary - # This could be improved by copying as much of Parser::stripSectionName as desired. - $section = str_replace( '[[:', '', $section ); - $section = str_replace( '[[', '', $section ); - $section = str_replace( ']]', '', $section ); - - $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784 - if ( $local ) { - $sectionTitle = Title::newFromText( '#' . $section ); - } else { - $sectionTitle = Title::makeTitleSafe( $title->getNamespace(), - $title->getDBkey(), $section ); + $comment = null; + wfRunHooks( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) ); + if ( $comment === null ) { + $link = ''; + if ( $title ) { + $section = $auto; + + # Remove links that a user may have manually put in the autosummary + # This could be improved by copying as much of Parser::stripSectionName as desired. + $section = str_replace( '[[:', '', $section ); + $section = str_replace( '[[', '', $section ); + $section = str_replace( ']]', '', $section ); + + $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784 + if ( $local ) { + $sectionTitle = Title::newFromText( '#' . $section ); + } else { + $sectionTitle = Title::makeTitleSafe( $title->getNamespace(), + $title->getDBkey(), $section ); + } + if ( $sectionTitle ) { + $link = self::link( $sectionTitle, + $wgLang->getArrow(), array(), array(), + 'noclasses' ); + } else { + $link = ''; + } } - if ( $sectionTitle ) { - $link = self::link( $sectionTitle, - $wgLang->getArrow(), array(), array(), - 'noclasses' ); - } else { - $link = ''; + if ( $pre ) { + # written summary $presep autocomment (summary /* section */) + $pre .= wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped(); } + if ( $post ) { + # autocomment $postsep written summary (/* section */ summary) + $auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped(); + } + $auto = '<span class="autocomment">' . $auto . '</span>'; + $comment = $pre . $link . $wgLang->getDirMark() . '<span dir="auto">' . $auto . $post . '</span>'; } - if ( $pre ) { - # written summary $presep autocomment (summary /* section */) - $pre .= wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ); - } - if ( $post ) { - # autocomment $postsep written summary (/* section */ summary) - $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) ); - } - $auto = '<span class="autocomment">' . $auto . '</span>'; - $comment = $pre . $link . $wgLang->getDirMark() . '<span dir="auto">' . $auto . $post . '</span>'; return $comment; } @@ -1205,7 +1313,7 @@ class Linker { * * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser * @param $comment String: text to format links in - * @param $title An optional title object used to links to sections + * @param $title Title|null An optional title object used to links to sections * @param $local Boolean: whether section links should refer to local page * @return String */ @@ -1399,7 +1507,8 @@ class Linker { return ''; } else { $formatted = self::formatComment( $comment, $title, $local ); - return " <span class=\"comment\" dir=\"auto\">($formatted)</span>"; + $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped(); + return " <span class=\"comment\">$formatted</span>"; } } @@ -1417,12 +1526,12 @@ class Linker { return ""; } if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) { - $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>"; + $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>"; } elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) { $block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ), $rev->getTitle(), $local ); } else { - $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>"; + $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>"; } if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { return " <span class=\"history-deleted\">$block</span>"; @@ -1436,13 +1545,11 @@ class Linker { */ public static function formatRevisionSize( $size ) { if ( $size == 0 ) { - $stxt = wfMsgExt( 'historyempty', 'parsemag' ); + $stxt = wfMessage( 'historyempty' )->escaped(); } else { - global $wgLang; - $stxt = wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $size ) ); - $stxt = "($stxt)"; + $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped(); + $stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped(); } - $stxt = htmlspecialchars( $stxt ); return "<span class=\"history-size\">$stxt</span>"; } @@ -1484,6 +1591,7 @@ class Linker { * End a Table Of Contents line. * tocUnindent() will be used instead if we're ending a line below * the new level. + * @return string */ public static function tocLineEnd() { return "</li>\n"; @@ -1493,11 +1601,13 @@ class Linker { * Wraps the TOC in a table and provides the hide/collapse javascript. * * @param $toc String: html of the Table Of Contents - * @param $lang mixed: Language code for the toc title + * @param $lang String|Language|false: Language for the toc title, defaults to user language * @return String: full html of the TOC */ public static function tocList( $toc, $lang = false ) { - $title = wfMsgExt( 'toc', array( 'language' => $lang, 'escape' ) ); + $lang = wfGetLangObj( $lang ); + $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); + return '<table id="toc" class="toc"><tr><td>' . '<div id="toctitle"><h2>' . $title . "</h2></div>\n" @@ -1509,7 +1619,7 @@ class Linker { * Generate a table of contents from a section tree * Currently unused. * - * @param $tree Return value of ParserOutput::getSections() + * @param $tree array Return value of ParserOutput::getSections() * @return String: HTML fragment */ public static function generateTOC( $tree ) { @@ -1562,6 +1672,7 @@ class Linker { /** * Split a link trail, return the "inside" portion and the remainder of the trail * as a two-element array + * @return array */ static function splitTrail( $trail ) { global $wgContLang; @@ -1589,38 +1700,102 @@ class Linker { * other users. * * @param $rev Revision object + * @param $context IContextSource context to use or null for the main context. + * @return string */ - public static function generateRollback( $rev ) { - return '<span class="mw-rollback-link">[' - . self::buildRollbackLink( $rev ) - . ']</span>'; + public static function generateRollback( $rev, IContextSource $context = null ) { + if ( $context === null ) { + $context = RequestContext::getMain(); + } + + return '<span class="mw-rollback-link">' + . $context->msg( 'brackets' )->rawParams( + self::buildRollbackLink( $rev, $context ) )->plain() + . '</span>'; } /** * Build a raw rollback link, useful for collections of "tool" links * * @param $rev Revision object + * @param $context IContextSource context to use or null for the main context. * @return String: HTML fragment */ - public static function buildRollbackLink( $rev ) { - global $wgRequest, $wgUser; + public static function buildRollbackLink( $rev, IContextSource $context = null ) { + global $wgShowRollbackEditCount, $wgMiserMode; + + // To config which pages are effected by miser mode + $disableRollbackEditCountSpecialPage = array( 'Recentchanges', 'Watchlist' ); + + if ( $context === null ) { + $context = RequestContext::getMain(); + } + $title = $rev->getTitle(); $query = array( 'action' => 'rollback', 'from' => $rev->getUserText(), - 'token' => $wgUser->getEditToken( array( $title->getPrefixedText(), $rev->getUserText() ) ), + 'token' => $context->getUser()->getEditToken( array( $title->getPrefixedText(), $rev->getUserText() ) ), ); - if ( $wgRequest->getBool( 'bot' ) ) { + if ( $context->getRequest()->getBool( 'bot' ) ) { $query['bot'] = '1'; $query['hidediff'] = '1'; // bug 15999 } - return self::link( - $title, - wfMsgHtml( 'rollbacklink' ), - array( 'title' => wfMsg( 'tooltip-rollback' ) ), - $query, - array( 'known', 'noclasses' ) - ); + + $disableRollbackEditCount = false; + if( $wgMiserMode ) { + foreach( $disableRollbackEditCountSpecialPage as $specialPage ) { + if( $context->getTitle()->isSpecial( $specialPage ) ) { + $disableRollbackEditCount = true; + break; + } + } + } + + if( !$disableRollbackEditCount && is_int( $wgShowRollbackEditCount ) && $wgShowRollbackEditCount > 0 ) { + $dbr = wfGetDB( DB_SLAVE ); + + // Up to the value of $wgShowRollbackEditCount revisions are counted + $res = $dbr->select( 'revision', + array( 'rev_id', 'rev_user_text' ), + // $rev->getPage() returns null sometimes + array( 'rev_page' => $rev->getTitle()->getArticleID() ), + __METHOD__, + array( 'USE INDEX' => 'page_timestamp', + 'ORDER BY' => 'rev_timestamp DESC', + 'LIMIT' => $wgShowRollbackEditCount + 1 ) + ); + + $editCount = 0; + while( $row = $dbr->fetchObject( $res ) ) { + if( $rev->getUserText() != $row->rev_user_text ) { + break; + } + $editCount++; + } + + if( $editCount > $wgShowRollbackEditCount ) { + $editCount_output = $context->msg( 'rollbacklinkcount-morethan' )->numParams( $wgShowRollbackEditCount )->parse(); + } else { + $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse(); + } + + return self::link( + $title, + $editCount_output, + array( 'title' => $context->msg( 'tooltip-rollback' )->text() ), + $query, + array( 'known', 'noclasses' ) + ); + } else { + return self::link( + $title, + $context->msg( 'rollbacklink' )->escaped(), + array( 'title' => $context->msg( 'tooltip-rollback' )->text() ), + $query, + array( 'known', 'noclasses' ) + ); + } } /** @@ -1647,35 +1822,38 @@ class Linker { # Construct the HTML $outText = '<div class="mw-templatesUsedExplanation">'; if ( $preview ) { - $outText .= wfMsgExt( 'templatesusedpreview', array( 'parse' ), count( $templates ) ); + $outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) ) + ->parseAsBlock(); } elseif ( $section ) { - $outText .= wfMsgExt( 'templatesusedsection', array( 'parse' ), count( $templates ) ); + $outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) ) + ->parseAsBlock(); } else { - $outText .= wfMsgExt( 'templatesused', array( 'parse' ), count( $templates ) ); + $outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) ) + ->parseAsBlock(); } $outText .= "</div><ul>\n"; - usort( $templates, array( 'Title', 'compare' ) ); + usort( $templates, 'Title::compare' ); foreach ( $templates as $titleObj ) { $r = $titleObj->getRestrictions( 'edit' ); if ( in_array( 'sysop', $r ) ) { - $protected = wfMsgExt( 'template-protected', array( 'parseinline' ) ); + $protected = wfMessage( 'template-protected' )->parse(); } elseif ( in_array( 'autoconfirmed', $r ) ) { - $protected = wfMsgExt( 'template-semiprotected', array( 'parseinline' ) ); + $protected = wfMessage( 'template-semiprotected' )->parse(); } else { $protected = ''; } if ( $titleObj->quickUserCan( 'edit' ) ) { $editLink = self::link( $titleObj, - wfMsg( 'editlink' ), + wfMessage( 'editlink' )->text(), array(), array( 'action' => 'edit' ) ); } else { $editLink = self::link( $titleObj, - wfMsg( 'viewsourcelink' ), + wfMessage( 'viewsourcelink' )->text(), array(), array( 'action' => 'edit' ) ); @@ -1696,14 +1874,13 @@ class Linker { * @return String: HTML output */ public static function formatHiddenCategories( $hiddencats ) { - global $wgLang; wfProfileIn( __METHOD__ ); $outText = ''; if ( count( $hiddencats ) > 0 ) { # Construct the HTML $outText = '<div class="mw-hiddenCategoriesExplanation">'; - $outText .= wfMsgExt( 'hiddencategories', array( 'parse' ), $wgLang->formatnum( count( $hiddencats ) ) ); + $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock(); $outText .= "</div><ul>\n"; foreach ( $hiddencats as $titleObj ) { @@ -1719,7 +1896,7 @@ class Linker { * Format a size in bytes for output, using an appropriate * unit (B, KB, MB or GB) according to the magnitude in question * - * @param $size Size to format + * @param $size int Size to format * @return String */ public static function formatSize( $size ) { @@ -1855,18 +2032,19 @@ class Linker { * Creates a (show/hide) link for deleting revisions/log entries * * @param $query Array: query parameters to be passed to link() - * @param $restricted Boolean: set to true to use a <strong> instead of a <span> + * @param $restricted Boolean: set to true to use a "<strong>" instead of a "<span>" * @param $delete Boolean: set to true to use (show/hide) rather than (show) * - * @return String: HTML <a> link to Special:Revisiondelete, wrapped in a + * @return String: HTML "<a>" link to Special:Revisiondelete, wrapped in a * span to allow for customization of appearance with CSS */ public static function revDeleteLink( $query = array(), $restricted = false, $delete = true ) { $sp = SpecialPage::getTitleFor( 'Revisiondelete' ); - $html = $delete ? wfMsgHtml( 'rev-delundel' ) : wfMsgHtml( 'rev-showdeleted' ); + $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted'; + $html = wfMessage( $msgKey )->escaped(); $tag = $restricted ? 'strong' : 'span'; $link = self::link( $sp, $html, array(), $query, array( 'known', 'noclasses' ) ); - return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), "($link)" ); + return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), wfMessage( 'parentheses' )->rawParams( $link )->escaped() ); } /** @@ -1878,8 +2056,10 @@ class Linker { * of appearance with CSS */ public static function revDeleteLinkDisabled( $delete = true ) { - $html = $delete ? wfMsgHtml( 'rev-delundel' ) : wfMsgHtml( 'rev-showdeleted' ); - return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), "($html)" ); + $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted'; + $html = wfMessage( $msgKey )->escaped(); + $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped(); + return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), $htmlParentheses ); } /* Deprecated methods */ @@ -1896,6 +2076,7 @@ class Linker { * @param $trail String: Optional trail. Alphabetic characters at the start of this string will * be included in the link text. Other characters will be appended after * the end of the link. + * @return string */ static function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) { wfDeprecated( __METHOD__, '1.16' ); @@ -1924,6 +2105,7 @@ class Linker { * be included in the link text. Other characters will be appended after * the end of the link. * @param $prefix String: optional prefix. As trail, only before instead of after. + * @return string */ static function makeLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { # wfDeprecated( __METHOD__, '1.16' ); // See r105985 and it's revert. Somewhere still used. @@ -1955,7 +2137,7 @@ class Linker { * @param $prefix String: text before link text * @param $aprops String: extra attributes to the a-element * @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead - * @return the a-element + * @return string the a-element */ static function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' @@ -1993,6 +2175,7 @@ class Linker { * be included in the link text. Other characters will be appended after * the end of the link. * @param $prefix String: Optional prefix + * @return string */ static function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) { wfDeprecated( __METHOD__, '1.16' ); @@ -2024,6 +2207,7 @@ class Linker { * be included in the link text. Other characters will be appended after * the end of the link. * @param $prefix String: Optional prefix + * @return string */ static function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) { wfDeprecated( __METHOD__, '1.16' ); @@ -2038,6 +2222,7 @@ class Linker { /** * Returns the attributes for the tooltip and access key. + * @return array */ public static function tooltipAndAccesskeyAttribs( $name ) { # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output @@ -2058,6 +2243,7 @@ class Linker { /** * Returns raw bits of HTML, use titleAttrib() + * @return null|string */ public static function tooltip( $name, $options = null ) { # @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output @@ -2084,6 +2270,7 @@ class DummyLinker { * * @param $fname String Name of called method * @param $args Array Arguments to the method + * @return mixed */ public function __call( $fname, $args ) { return call_user_func_array( array( 'Linker', $fname ), $args ); diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 27d1dfd2..87db4d60 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -1,6 +1,6 @@ <?php /** - * See docs/deferred.txt + * Updater for link tracking tables after a page edit. * * 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 @@ -17,14 +17,19 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @file + */ + +/** + * See docs/deferred.txt + * * @todo document (e.g. one-sentence top-level class description). */ -class LinksUpdate { +class LinksUpdate extends SqlDataUpdate { - /**@{{ - * @private - */ - var $mId, //!< Page ID of the article linked from + // @todo: make members protected, but make sure extensions don't break + + public $mId, //!< Page ID of the article linked from $mTitle, //!< Title object of the article linked from $mParserOutput, //!< Parser output $mLinks, //!< Map of title strings to IDs for the links in the document @@ -37,7 +42,6 @@ class LinksUpdate { $mDb, //!< Database connection reference $mOptions, //!< SELECT options to be used (array) $mRecursive; //!< Whether to queue jobs for recursive updates - /**@}}*/ /** * Constructor @@ -47,22 +51,25 @@ class LinksUpdate { * @param $recursive Boolean: queue jobs for recursive updates? */ function __construct( $title, $parserOutput, $recursive = true ) { - global $wgAntiLockFlags; - - if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) { - $this->mOptions = array(); - } else { - $this->mOptions = array( 'FOR UPDATE' ); - } - $this->mDb = wfGetDB( DB_MASTER ); + parent::__construct( false ); // no implicit transaction - if ( !is_object( $title ) ) { + if ( !( $title instanceof Title ) ) { throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " . "Please see Article::editUpdates() for an invocation example.\n" ); } + + if ( !( $parserOutput instanceof ParserOutput ) ) { + throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " . + "Please see WikiPage::doEditUpdates() for an invocation example.\n" ); + } + $this->mTitle = $title; $this->mId = $title->getArticleID(); + if ( !$this->mId ) { + throw new MWException( "The Title object did not provide an article ID. Perhaps the page doesn't exist?" ); + } + $this->mParserOutput = $parserOutput; $this->mLinks = $parserOutput->getLinks(); $this->mImages = $parserOutput->getImages(); @@ -254,51 +261,6 @@ class LinksUpdate { } /** - * Invalidate the cache of a list of pages from a single namespace - * - * @param $namespace Integer - * @param $dbkeys Array - */ - function invalidatePages( $namespace, $dbkeys ) { - if ( !count( $dbkeys ) ) { - return; - } - - /** - * Determine which pages need to be updated - * This is necessary to prevent the job queue from smashing the DB with - * large numbers of concurrent invalidations of the same page - */ - $now = $this->mDb->timestamp(); - $ids = array(); - $res = $this->mDb->select( 'page', array( 'page_id' ), - array( - 'page_namespace' => $namespace, - 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')', - 'page_touched < ' . $this->mDb->addQuotes( $now ) - ), __METHOD__ - ); - foreach ( $res as $row ) { - $ids[] = $row->page_id; - } - if ( !count( $ids ) ) { - return; - } - - /** - * Do the update - * We still need the page_touched condition, in case the row has changed since - * the non-locking select above. - */ - $this->mDb->update( 'page', array( 'page_touched' => $now ), - array( - 'page_id IN (' . $this->mDb->makeList( $ids ) . ')', - 'page_touched < ' . $this->mDb->addQuotes( $now ) - ), __METHOD__ - ); - } - - /** * @param $cats */ function invalidateCategories( $cats ) { @@ -849,3 +811,72 @@ class LinksUpdate { } } } + +/** + * Update object handling the cleanup of links tables after a page was deleted. + **/ +class LinksDeletionUpdate extends SqlDataUpdate { + + protected $mPage; //!< WikiPage the wikipage that was deleted + + /** + * Constructor + * + * @param $page WikiPage Page we are updating + */ + function __construct( WikiPage $page ) { + parent::__construct( false ); // no implicit transaction + + $this->mPage = $page; + } + + /** + * Do some database updates after deletion + */ + public function doUpdate() { + $title = $this->mPage->getTitle(); + $id = $this->mPage->getId(); + + # Delete restrictions for it + $this->mDb->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); + + # Fix category table counts + $cats = array(); + $res = $this->mDb->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ ); + + foreach ( $res as $row ) { + $cats [] = $row->cl_to; + } + + $this->mPage->updateCategoryCounts( array(), $cats ); + + # If using cascading deletes, we can skip some explicit deletes + if ( !$this->mDb->cascadingDeletes() ) { + $this->mDb->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); + + # Delete outgoing links + $this->mDb->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ ); + $this->mDb->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ ); + } + + # If using cleanup triggers, we can skip some manual deletes + if ( !$this->mDb->cleanupTriggers() ) { + # Clean up recentchanges entries... + $this->mDb->delete( 'recentchanges', + array( 'rc_type != ' . RC_LOG, + 'rc_namespace' => $title->getNamespace(), + 'rc_title' => $title->getDBkey() ), + __METHOD__ ); + $this->mDb->delete( 'recentchanges', + array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ), + __METHOD__ ); + } + } +} diff --git a/includes/LocalisationCache.php b/includes/LocalisationCache.php index 3b1f45cc..d8e5d3a3 100644 --- a/includes/LocalisationCache.php +++ b/includes/LocalisationCache.php @@ -1,4 +1,24 @@ <?php +/** + * Cache of the contents of localisation files. + * + * 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 + */ define( 'MW_LC_VERSION', 2 ); @@ -90,7 +110,7 @@ class LocalisationCache { 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap', 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases', 'imageFiles', 'preloadedMessages', 'namespaceGenderAliases', - 'digitGroupingPattern' + 'digitGroupingPattern', 'pluralRules', 'compiledPluralRules', ); /** @@ -98,7 +118,7 @@ class LocalisationCache { * by a fallback sequence. */ static public $mergeableMapKeys = array( 'messages', 'namespaceNames', - 'dateFormats', 'imageFiles', 'preloadedMessages', + 'dateFormats', 'imageFiles', 'preloadedMessages' ); /** @@ -134,6 +154,12 @@ class LocalisationCache { */ static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' ); + /** + * Associative array of cached plural rules. The key is the language code, + * the value is an array of plural rules for that language. + */ + var $pluralRules = null; + var $mergeableKeys = null; /** @@ -214,9 +240,9 @@ class LocalisationCache { */ public function getItem( $code, $key ) { if ( !isset( $this->loadedItems[$code][$key] ) ) { - wfProfileIn( __METHOD__.'-load' ); + wfProfileIn( __METHOD__ . '-load' ); $this->loadItem( $code, $key ); - wfProfileOut( __METHOD__.'-load' ); + wfProfileOut( __METHOD__ . '-load' ); } if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) { @@ -236,9 +262,9 @@ class LocalisationCache { public function getSubitem( $code, $key, $subkey ) { if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) && !isset( $this->loadedItems[$code][$key] ) ) { - wfProfileIn( __METHOD__.'-load' ); + wfProfileIn( __METHOD__ . '-load' ); $this->loadSubitem( $code, $key, $subkey ); - wfProfileOut( __METHOD__.'-load' ); + wfProfileOut( __METHOD__ . '-load' ); } if ( isset( $this->data[$code][$key][$subkey] ) ) { @@ -343,10 +369,11 @@ class LocalisationCache { /** * Returns true if the cache identified by $code is missing or expired. + * @return bool */ public function isExpired( $code ) { if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) { - wfDebug( __METHOD__."($code): forced reload\n" ); + wfDebug( __METHOD__ . "($code): forced reload\n" ); return true; } @@ -355,7 +382,7 @@ class LocalisationCache { $preload = $this->store->get( $code, 'preload' ); // Different keys may expire separately, at least in LCStore_Accel if ( $deps === null || $keys === null || $preload === null ) { - wfDebug( __METHOD__."($code): cache missing, need to make one\n" ); + wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" ); return true; } @@ -365,7 +392,7 @@ class LocalisationCache { // anymore (e.g. uninstalled extensions) // When this happens, always expire the cache if ( !$dep instanceof CacheDependency || $dep->isExpired() ) { - wfDebug( __METHOD__."($code): cache for $code expired due to " . + wfDebug( __METHOD__ . "($code): cache for $code expired due to " . get_class( $dep ) . "\n" ); return true; } @@ -460,9 +487,95 @@ class LocalisationCache { } elseif ( $_fileType == 'aliases' ) { $data = compact( 'aliases' ); } else { - throw new MWException( __METHOD__.": Invalid file type: $_fileType" ); + throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" ); } + return $data; + } + + /** + * Get the compiled plural rules for a given language from the XML files. + * @since 1.20 + */ + public function getCompiledPluralRules( $code ) { + $rules = $this->getPluralRules( $code ); + if ( $rules === null ) { + return null; + } + try { + $compiledRules = CLDRPluralRuleEvaluator::compile( $rules ); + } catch( CLDRPluralRuleError $e ) { + wfDebugLog( 'l10n', $e->getMessage() . "\n" ); + return array(); + } + return $compiledRules; + } + + /** + * Get the plural rules for a given language from the XML files. + * Cached. + * @since 1.20 + */ + public function getPluralRules( $code ) { + if ( $this->pluralRules === null ) { + $cldrPlural = __DIR__ . "/../languages/data/plurals.xml"; + $mwPlural = __DIR__ . "/../languages/data/plurals-mediawiki.xml"; + // Load CLDR plural rules + $this->loadPluralFile( $cldrPlural ); + if ( file_exists( $mwPlural ) ) { + // Override or extend + $this->loadPluralFile( $mwPlural ); + } + } + if ( !isset( $this->pluralRules[$code] ) ) { + return null; + } else { + return $this->pluralRules[$code]; + } + } + + /** + * Load a plural XML file with the given filename, compile the relevant + * rules, and save the compiled rules in a process-local cache. + */ + protected function loadPluralFile( $fileName ) { + $doc = new DOMDocument; + $doc->load( $fileName ); + $rulesets = $doc->getElementsByTagName( "pluralRules" ); + foreach ( $rulesets as $ruleset ) { + $codes = $ruleset->getAttribute( 'locales' ); + $rules = array(); + $ruleElements = $ruleset->getElementsByTagName( "pluralRule" ); + foreach ( $ruleElements as $elt ) { + $rules[] = $elt->nodeValue; + } + foreach ( explode( ' ', $codes ) as $code ) { + $this->pluralRules[$code] = $rules; + } + } + } + + /** + * Read the data from the source files for a given language, and register + * the relevant dependencies in the $deps array. If the localisation + * exists, the data array is returned, otherwise false is returned. + */ + protected function readSourceFilesAndRegisterDeps( $code, &$deps ) { + $fileName = Language::getMessagesFileName( $code ); + if ( !file_exists( $fileName ) ) { + return false; + } + + $deps[] = new FileDependency( $fileName ); + $data = $this->readPHPFile( $fileName, 'core' ); + + # Load CLDR plural rules for JavaScript + $data['pluralRules'] = $this->getPluralRules( $code ); + # And for PHP + $data['compiledPluralRules'] = $this->getCompiledPluralRules( $code ); + + $deps['plurals'] = new FileDependency( __DIR__ . "/../languages/data/plurals.xml" ); + $deps['plurals-mw'] = new FileDependency( __DIR__ . "/../languages/data/plurals-mediawiki.xml" ); return $data; } @@ -564,14 +677,12 @@ class LocalisationCache { $deps = array(); # Load the primary localisation from the source file - $fileName = Language::getMessagesFileName( $code ); - if ( !file_exists( $fileName ) ) { - wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" ); + $data = $this->readSourceFilesAndRegisterDeps( $code, $deps ); + if ( $data === false ) { + wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" ); $coreData['fallback'] = 'en'; } else { - $deps[] = new FileDependency( $fileName ); - $data = $this->readPHPFile( $fileName, 'core' ); - wfDebug( __METHOD__.": got localisation for $code from source\n" ); + wfDebug( __METHOD__ . ": got localisation for $code from source\n" ); # Merge primary localisation foreach ( $data as $key => $value ) { @@ -584,7 +695,6 @@ class LocalisationCache { if ( is_null( $coreData['fallback'] ) ) { $coreData['fallback'] = $code === 'en' ? false : 'en'; } - if ( $coreData['fallback'] === false ) { $coreData['fallbackSequence'] = array(); } else { @@ -600,15 +710,11 @@ class LocalisationCache { foreach ( $coreData['fallbackSequence'] as $fbCode ) { # Load the secondary localisation from the source file to # avoid infinite cycles on cyclic fallbacks - $fbFilename = Language::getMessagesFileName( $fbCode ); - - if ( !file_exists( $fbFilename ) ) { + $fbData = $this->readSourceFilesAndRegisterDeps( $fbCode, $deps ); + if ( $fbData === false ) { continue; } - $deps[] = new FileDependency( $fbFilename ); - $fbData = $this->readPHPFile( $fbFilename, 'core' ); - foreach ( self::$allKeys as $key ) { if ( !isset( $fbData[$key] ) ) { continue; @@ -633,7 +739,7 @@ class LocalisationCache { $used = false; foreach ( $data as $key => $item ) { - if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) { + if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) { $used = true; } } @@ -663,19 +769,26 @@ class LocalisationCache { $page = str_replace( ' ', '_', $page ); } # Decouple the reference to prevent accidental damage - unset($page); + unset( $page ); + + # If there were no plural rules, return an empty array + if ( $allData['pluralRules'] === null ) { + $allData['pluralRules'] = array(); + } + if ( $allData['compiledPluralRules'] === null ) { + $allData['compiledPluralRules'] = array(); + } # Set the list keys $allData['list'] = array(); foreach ( self::$splitKeys as $key ) { $allData['list'][$key] = array_keys( $allData[$key] ); } - # Run hooks wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) ); if ( is_null( $allData['namespaceNames'] ) ) { - throw new MWException( __METHOD__.': Localisation data failed sanity check! ' . + throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' . 'Check that your languages/messages/MessagesEn.php file is intact.' ); } @@ -793,8 +906,8 @@ class LocalisationCache { interface LCStore { /** * Get a value. - * @param $code Language code - * @param $key Cache key + * @param $code string Language code + * @param $key string Cache key */ function get( $code, $key ); @@ -903,17 +1016,17 @@ class LCStore_DB implements LCStore { } if ( !$code ) { - throw new MWException( __METHOD__.": Invalid language \"$code\"" ); + throw new MWException( __METHOD__ . ": Invalid language \"$code\"" ); } $this->dbw = wfGetDB( DB_MASTER ); try { - $this->dbw->begin(); + $this->dbw->begin( __METHOD__ ); $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ ); } catch ( DBQueryError $e ) { if ( $this->dbw->wasReadOnlyError() ) { $this->readOnly = true; - $this->dbw->rollback(); + $this->dbw->rollback( __METHOD__ ); $this->dbw->ignoreErrors( false ); return; } else { @@ -934,7 +1047,7 @@ class LCStore_DB implements LCStore { $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ ); } - $this->dbw->commit(); + $this->dbw->commit( __METHOD__ ); $this->currentLang = null; $this->dbw = null; $this->batch = array(); @@ -947,7 +1060,7 @@ class LCStore_DB implements LCStore { } if ( is_null( $this->currentLang ) ) { - throw new MWException( __CLASS__.': must call startWrite() before calling set()' ); + throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' ); } $this->batch[] = array( @@ -1019,7 +1132,7 @@ class LCStore_CDB implements LCStore { } // Close reader to stop permission errors on write - if( !empty($this->readers[$code]) ) { + if ( !empty( $this->readers[$code] ) ) { $this->readers[$code]->close(); } @@ -1037,14 +1150,14 @@ class LCStore_CDB implements LCStore { public function set( $key, $value ) { if ( is_null( $this->writer ) ) { - throw new MWException( __CLASS__.': must call startWrite() before calling set()' ); + throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' ); } $this->writer->set( $key, serialize( $value ) ); } protected function getFileName( $code ) { if ( !$code || strpos( $code, '/' ) !== false ) { - throw new MWException( __METHOD__.": Invalid language \"$code\"" ); + throw new MWException( __METHOD__ . ": Invalid language \"$code\"" ); } return "{$this->directory}/l10n_cache-$code.cdb"; } @@ -1160,8 +1273,9 @@ class LocalisationCache_BulkLoad extends LocalisationCache { while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) { reset( $this->mruLangs ); $code = key( $this->mruLangs ); - wfDebug( __METHOD__.": unloading $code\n" ); + wfDebug( __METHOD__ . ": unloading $code\n" ); $this->unload( $code ); } } -}
\ No newline at end of file + +} diff --git a/includes/MWFunction.php b/includes/MWFunction.php index 0113f917..36fcc30b 100644 --- a/includes/MWFunction.php +++ b/includes/MWFunction.php @@ -1,6 +1,7 @@ <?php - /** + * Helper methods to call functions and instance objects. + * * 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 @@ -16,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @file */ class MWFunction { diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 1ba46701..42791f57 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -1,15 +1,30 @@ <?php /** - * File for magic words + * File for magic words. * - * See docs/magicword.txt + * See docs/magicword.txt. + * + * 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 Parser */ /** - * This class encapsulates "magic words" such as #redirect, __NOTOC__, etc. + * This class encapsulates "magic words" such as "#redirect", __NOTOC__, etc. * * @par Usage: * @code @@ -27,7 +42,7 @@ * * To add magic words in an extension, use $magicWords in a file listed in * $wgExtensionMessagesFiles[]. - * + * * @par Example: * @code * $magicWords = array(); @@ -84,6 +99,7 @@ class MagicWord { 'numberoffiles', 'numberofedits', 'articlepath', + 'pageid', 'sitename', 'server', 'servername', @@ -95,6 +111,7 @@ class MagicWord { 'fullpagenamee', 'namespace', 'namespacee', + 'namespacenumber', 'currentweek', 'currentdow', 'localweek', @@ -282,6 +299,7 @@ class MagicWord { * Initialises this object with an ID * * @param $id + * @throws MWException */ function load( $id ) { global $wgContLang; @@ -290,8 +308,8 @@ class MagicWord { $wgContLang->getMagic( $this ); if ( !$this->mSynonyms ) { $this->mSynonyms = array( 'dkjsagfjsgashfajsh' ); - #throw new MWException( "Error: invalid magic word '$id'" ); - wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" ); + throw new MWException( "Error: invalid magic word '$id'" ); + #wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" ); } wfProfileOut( __METHOD__ ); } @@ -628,6 +646,9 @@ class MagicWordArray { var $baseRegex, $regex; var $matches; + /** + * @param $names array + */ function __construct( $names = array() ) { $this->names = $names; } @@ -756,12 +777,21 @@ class MagicWordArray { } /** + * @since 1.20 + * @return array + */ + public function getNames() { + return $this->names; + } + + /** * Parse a match array from preg_match * Returns array(magic word ID, parameter value) * If there is no parameter value, that element will be false. * * @param $m array * + * @throws MWException * @return array */ function parseMatch( $m ) { @@ -798,7 +828,7 @@ class MagicWordArray { $regexes = $this->getVariableStartToEndRegex(); foreach ( $regexes as $regex ) { if ( $regex !== '' ) { - $m = false; + $m = array(); if ( preg_match( $regex, $text, $m ) ) { return $this->parseMatch( $m ); } @@ -813,7 +843,7 @@ class MagicWordArray { * * @param $text string * - * @return string|false + * @return string|bool False on failure */ public function matchStartToEnd( $text ) { $hash = $this->getHash(); @@ -861,7 +891,7 @@ class MagicWordArray { * * @param $text string * - * @return int|false + * @return int|bool False on failure */ public function matchStartAndRemove( &$text ) { $regexes = $this->getRegexStart(); diff --git a/includes/Message.php b/includes/Message.php index 10f9d3e1..b0147b9a 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -1,12 +1,34 @@ <?php /** + * Fetching and processing of interface messages. + * + * 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 + * @author Niklas Laxström + */ + +/** * The Message class provides methods which fullfil two basic services: * - fetching interface messages * - processing messages into a variety of formats * * First implemented with MediaWiki 1.17, the Message class is intented to * replace the old wfMsg* functions that over time grew unusable. - * @see https://www.mediawiki.org/wiki/New_messages_API for equivalences + * @see https://www.mediawiki.org/wiki/Manual:Messages_API for equivalences * between old and new functions. * * You should use the wfMessage() global function which acts as a wrapper for @@ -90,8 +112,7 @@ * ->plain(); * @endcode * - * @note You cannot parse the text except in the content or interface - * @note languages + * @note You can parse the text only in the content or interface languages * * @section message_compare_old Comparison with old wfMsg* functions: * @@ -134,7 +155,6 @@ * @see https://www.mediawiki.org/wiki/Localisation * * @since 1.17 - * @author Niklas Laxström */ class Message { /** @@ -189,6 +209,7 @@ class Message { /** * Constructor. + * @since 1.17 * @param $key: message key, or array of message keys to try and use the first non-empty message for * @param $params Array message parameters * @return Message: $this @@ -204,6 +225,7 @@ class Message { * Factory function that is just wrapper for the real constructor. It is * intented to be used instead of the real constructor, because it allows * chaining method calls, while new objects don't. + * @since 1.17 * @param $key String: message key * @param Varargs: parameters as Strings * @return Message: $this @@ -218,6 +240,7 @@ class Message { * Factory function accepting multiple message keys and returning a message instance * for the first message which is non-empty. If all messages are empty then an * instance of the first message key is returned. + * @since 1.18 * @param Varargs: message keys (or first arg as an array of all the message keys) * @return Message: $this */ @@ -237,6 +260,7 @@ class Message { /** * Adds parameters to the parameter list of this message. + * @since 1.17 * @param Varargs: parameters as Strings, or a single argument that is an array of Strings * @return Message: $this */ @@ -255,6 +279,7 @@ class Message { * In other words the parsing process cannot access the contents * of this type of parameter, and you need to make sure it is * sanitized beforehand. The parser will see "$n", instead. + * @since 1.17 * @param Varargs: raw parameters as Strings (or single argument that is an array of raw parameters) * @return Message: $this */ @@ -272,6 +297,7 @@ class Message { /** * Add parameters that are numeric and will be passed through * Language::formatNum before substitution + * @since 1.18 * @param Varargs: numeric parameters (or single argument that is array of numeric parameters) * @return Message: $this */ @@ -288,13 +314,14 @@ class Message { /** * Set the language and the title from a context object - * + * @since 1.19 * @param $context IContextSource * @return Message: $this */ public function setContext( IContextSource $context ) { $this->inLanguage( $context->getLanguage() ); $this->title( $context->getTitle() ); + $this->interface = true; return $this; } @@ -303,6 +330,7 @@ class Message { * Request the message in any language that is supported. * As a side effect interface message status is unconditionally * turned off. + * @since 1.17 * @param $lang Mixed: language code or Language object. * @return Message: $this */ @@ -326,6 +354,7 @@ class Message { /** * Request the message in the wiki's content language, * unless it is disabled for this message. + * @since 1.17 * @see $wgForceUIMsgAsContentMsg * @return Message: $this */ @@ -342,7 +371,20 @@ class Message { } /** + * Allows manipulating the interface message flag directly. + * Can be used to restore the flag after setting a language. + * @param $value bool + * @return Message: $this + * @since 1.20 + */ + public function setInterfaceMessageFlag( $value ) { + $this->interface = (bool) $value; + return $this; + } + + /** * Enable or disable database use. + * @since 1.17 * @param $value Boolean * @return Message: $this */ @@ -353,7 +395,7 @@ class Message { /** * Set the Title object to use as context when transforming the message - * + * @since 1.18 * @param $title Title object * @return Message: $this */ @@ -364,10 +406,19 @@ class Message { /** * Returns the message parsed from wikitext to HTML. + * @since 1.17 * @return String: HTML */ public function toString() { - $string = $this->getMessageText(); + $string = $this->fetchMessage(); + + if ( $string === false ) { + $key = htmlspecialchars( is_array( $this->key ) ? $this->key[0] : $this->key ); + if ( $this->format === 'plain' ) { + return '<' . $key . '>'; + } + return '<' . $key . '>'; + } # Replace parameters before text parsing $string = $this->replaceParameters( $string, 'before' ); @@ -398,6 +449,7 @@ class Message { * Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg: * $foo = Message::get($key); * $string = "<abbr>$foo</abbr>"; + * @since 1.18 * @return String */ public function __toString() { @@ -406,6 +458,7 @@ class Message { /** * Fully parse the text from wikitext to HTML + * @since 1.17 * @return String parsed HTML */ public function parse() { @@ -415,6 +468,7 @@ class Message { /** * Returns the message text. {{-transformation is done. + * @since 1.17 * @return String: Unescaped message text. */ public function text() { @@ -424,6 +478,7 @@ class Message { /** * Returns the message text as-is, only parameters are subsituted. + * @since 1.17 * @return String: Unescaped untransformed message text. */ public function plain() { @@ -433,6 +488,7 @@ class Message { /** * Returns the parsed message text which is always surrounded by a block element. + * @since 1.17 * @return String: HTML */ public function parseAsBlock() { @@ -443,6 +499,7 @@ class Message { /** * Returns the message text. {{-transformation is done and the result * is escaped excluding any raw parameters. + * @since 1.17 * @return String: Escaped message text. */ public function escaped() { @@ -452,6 +509,7 @@ class Message { /** * Check whether a message key has been defined currently. + * @since 1.17 * @return Bool: true if it is and false if not. */ public function exists() { @@ -460,6 +518,7 @@ class Message { /** * Check whether a message does not exist, or is an empty string + * @since 1.18 * @return Bool: true if is is and false if not * @todo FIXME: Merge with isDisabled()? */ @@ -470,6 +529,7 @@ class Message { /** * Check whether a message does not exist, is an empty string, or is "-" + * @since 1.18 * @return Bool: true if is is and false if not */ public function isDisabled() { @@ -478,6 +538,7 @@ class Message { } /** + * @since 1.17 * @param $value * @return array */ @@ -486,6 +547,7 @@ class Message { } /** + * @since 1.18 * @param $value * @return array */ @@ -495,6 +557,7 @@ class Message { /** * Substitutes any paramaters into the message text. + * @since 1.17 * @param $message String: the message text * @param $type String: either before or after * @return String @@ -513,6 +576,7 @@ class Message { /** * Extracts the parameter type and preprocessed the value if needed. + * @since 1.18 * @param $param String|Array: Parameter as defined in this class. * @return Tuple(type, value) */ @@ -536,6 +600,7 @@ class Message { /** * Wrapper for what ever method we use to parse wikitext. + * @since 1.17 * @param $string String: Wikitext message contents * @return string Wikitext parsed into HTML */ @@ -545,6 +610,7 @@ class Message { /** * Wrapper for what ever method we use to {{-transform wikitext. + * @since 1.17 * @param $string String: Wikitext message contents * @return string Wikitext with {{-constructs replaced with their values. */ @@ -553,21 +619,8 @@ class Message { } /** - * Returns the textual value for the message. - * @return Message contents or placeholder - */ - protected function getMessageText() { - $message = $this->fetchMessage(); - if ( $message === false ) { - return '<' . htmlspecialchars( is_array($this->key) ? $this->key[0] : $this->key ) . '>'; - } else { - return $message; - } - } - - /** * Wrapper for what ever method we use to get message contents - * + * @since 1.17 * @return string */ protected function fetchMessage() { diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php index be6b27c9..34014e1b 100644 --- a/includes/MessageBlobStore.php +++ b/includes/MessageBlobStore.php @@ -1,5 +1,7 @@ <?php /** + * Resource message blobs storage used by the resource loader. + * * 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 @@ -15,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @file * @author Roan Kattouw * @author Trevor Parscal */ @@ -296,7 +299,7 @@ class MessageBlobStore { */ private static function reencodeBlob( $blob, $key, $lang ) { $decoded = FormatJson::decode( $blob, true ); - $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); + $decoded[$key] = wfMessage( $key )->inLanguage( $lang )->plain(); return FormatJson::encode( (object)$decoded ); } @@ -350,7 +353,7 @@ class MessageBlobStore { $messages = array(); foreach ( $module->getMessages() as $key ) { - $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); + $messages[$key] = wfMessage( $key )->inLanguage( $lang )->plain(); } return FormatJson::encode( (object)$messages ); diff --git a/includes/Metadata.php b/includes/Metadata.php index e5e3296b..0ca15393 100644 --- a/includes/Metadata.php +++ b/includes/Metadata.php @@ -1,21 +1,23 @@ <?php /** + * Base code to format metadata. * * Copyright 2004, Evan Prodromou <evan@wikitravel.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 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. + * 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 + * 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 * * @author Evan Prodromou <evan@wikitravel.org> * @file @@ -58,7 +60,7 @@ abstract class RdfMetaData { global $wgLanguageCode, $wgSitename; $this->element( 'title', $this->mArticle->getTitle()->getText() ); - $this->pageOrString( 'publisher', wfMsg( 'aboutpage' ), $wgSitename ); + $this->pageOrString( 'publisher', wfMessage( 'aboutpage' )->text(), $wgSitename ); $this->element( 'language', $wgLanguageCode ); $this->element( 'type', 'Text' ); $this->element( 'format', 'text/html' ); @@ -115,14 +117,18 @@ abstract class RdfMetaData { protected function person( $name, User $user ) { if( $user->isAnon() ){ - $this->element( $name, wfMsgExt( 'anonymous', array( 'parsemag' ), 1 ) ); + $this->element( $name, wfMessage( 'anonymous' )->numParams( 1 )->text() ); } else { $real = $user->getRealName(); if( $real ) { $this->element( $name, $real ); } else { $userName = $user->getName(); - $this->pageOrString( $name, $user->getUserPage(), wfMsgExt( 'siteuser', 'parsemag', $userName, $userName ) ); + $this->pageOrString( + $name, + $user->getUserPage(), + wfMessage( 'siteuser', $userName, $userName )->text() + ); } } } diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index f86b6051..1873e7bf 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -2,6 +2,21 @@ /** * Module defining helper functions for detecting and dealing with mime types. * + * 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 */ @@ -123,7 +138,7 @@ END_STRING * Implements functions related to mime types such as detection and mapping to * file extension. * - * Instances of this class are stateles, there only needs to be one global instance + * Instances of this class are stateless, there only needs to be one global instance * of MimeMagic. Please use MimeMagic::singleton() to get that instance. */ class MimeMagic { @@ -215,8 +230,6 @@ class MimeMagic { continue; } - #print "processing MIME line $s<br>"; - $mime = substr( $s, 0, $i ); $ext = trim( substr($s, $i+1 ) ); @@ -566,6 +579,7 @@ class MimeMagic { * * @param string $file * @param mixed $ext + * @return bool|string */ private function doGuessMimeType( $file, $ext ) { // TODO: remove $ext param // Read a chunk of the file @@ -1030,6 +1044,7 @@ class MimeMagic { * * This funktion relies on the mapping defined by $this->mMediaTypes * @access private + * @return int|string */ function findMediaType( $extMime ) { if ( strpos( $extMime, '.' ) === 0 ) { @@ -1067,6 +1082,7 @@ class MimeMagic { * @param $fileName String: the file name (unused at present) * @param $chunk String: the first 256 bytes of the file * @param $proposed String: the MIME type proposed by the server + * @return Array */ public function getIEMimeTypes( $fileName, $chunk, $proposed ) { $ca = $this->getIEContentAnalyzer(); diff --git a/includes/Namespace.php b/includes/Namespace.php index 292559d0..2e2b8d61 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -1,6 +1,22 @@ <?php /** - * Provide things related to namespaces + * Provide things related to namespaces. + * + * 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 */ @@ -14,7 +30,6 @@ * Users and translators should not change them * */ - class MWNamespace { /** @@ -33,7 +48,7 @@ class MWNamespace { * @param $index * @param $method * - * @return true + * @return bool */ private static function isMethodValidFor( $index, $method ) { if ( $index < NS_MAIN ) { @@ -50,7 +65,15 @@ class MWNamespace { */ public static function isMovable( $index ) { global $wgAllowImageMoving; - return !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) || $index == NS_CATEGORY ); + + $result = !( $index < NS_MAIN || ( $index == NS_FILE && !$wgAllowImageMoving ) || $index == NS_CATEGORY ); + + /** + * @since 1.20 + */ + wfRunHooks( 'NamespaceIsMovable', array( $index, &$result ) ); + + return $result; } /** @@ -67,6 +90,7 @@ class MWNamespace { /** * @see self::isSubject * @deprecated Please use the more consistently named isSubject (since 1.19) + * @return bool */ public static function isMain( $index ) { wfDeprecated( __METHOD__, '1.19' ); @@ -185,7 +209,7 @@ class MWNamespace { * Returns array of all defined namespaces with their canonical * (English) names. * - * @return \array + * @return array * @since 1.17 */ public static function getCanonicalNamespaces() { @@ -315,6 +339,33 @@ class MWNamespace { return $wgContentNamespaces; } } + + /** + * List all namespace indices which are considered subject, aka not a talk + * or special namespace. See also MWNamespace::isSubject + * + * @return array of namespace indices + */ + public static function getSubjectNamespaces() { + return array_filter( + MWNamespace::getValidNamespaces(), + 'MWNamespace::isSubject' + ); + } + + /** + * List all namespace indices which are considered talks, aka not a subject + * or special namespace. See also MWNamespace::isTalk + * + * @return array of namespace indices + */ + public static function getTalkNamespaces() { + return array_filter( + MWNamespace::getValidNamespaces(), + 'MWNamespace::isTalk' + ); + } + /** * Is the namespace first-letter capitalized? * @@ -353,4 +404,16 @@ class MWNamespace { return $index == NS_USER || $index == NS_USER_TALK; } + /** + * It is not possible to use pages from this namespace as template? + * + * @since 1.20 + * @param $index int Index to check + * @return bool + */ + public static function isNonincludable( $index ) { + global $wgNonincludableNamespaces; + return $wgNonincludableNamespaces && in_array( $index, $wgNonincludableNamespaces ); + } + } diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index 4112f8a2..46a43f63 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -1,6 +1,21 @@ <?php /** - * Functions to be used with PHP's output buffer + * Functions to be used with PHP's output buffer. + * + * 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 */ @@ -76,7 +91,12 @@ function wfRequestExtension() { * @return string */ function wfGzipHandler( $s ) { - if( !function_exists( 'gzencode' ) || headers_sent() ) { + if( !function_exists( 'gzencode' ) ) { + wfDebug( __FUNCTION__ . "() skipping compression (gzencode unavaible)\n" ); + return $s; + } + if( headers_sent() ) { + wfDebug( __FUNCTION__ . "() skipping compression (headers already sent)\n" ); return $s; } @@ -90,6 +110,7 @@ function wfGzipHandler( $s ) { } if( wfClientAcceptsGzip() ) { + wfDebug( __FUNCTION__ . "() is compressing output\n" ); header( 'Content-Encoding: gzip' ); $s = gzencode( $s, 6 ); } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index a91d5465..b4a81bb1 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -1,7 +1,24 @@ <?php -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} +/** + * Preparation for the final page rendering. + * + * 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 + */ /** * This class should be covered by a general architecture document which does @@ -19,10 +36,10 @@ if ( !defined( 'MEDIAWIKI' ) ) { * @todo document */ class OutputPage extends ContextSource { - /// Should be private. Used with addMeta() which adds <meta> + /// Should be private. Used with addMeta() which adds "<meta>" var $mMetatags = array(); - /// <meta keyworkds="stuff"> most of the time the first 10 links to an article + /// "<meta keywords='stuff'>" most of the time the first 10 links to an article var $mKeywords = array(); var $mLinktags = array(); @@ -33,17 +50,17 @@ class OutputPage extends ContextSource { /// Should be private - has getter and setter. Contains the HTML title var $mPagetitle = ''; - /// Contains all of the <body> content. Should be private we got set/get accessors and the append() method. + /// Contains all of the "<body>" content. Should be private we got set/get accessors and the append() method. var $mBodytext = ''; /** * Holds the debug lines that will be output as comments in page source if * $wgDebugComments is enabled. See also $wgShowDebug. - * TODO: make a getter method for this + * @deprecated since 1.20; use MWDebug class instead. */ - public $mDebugtext = ''; // TODO: we might want to replace it by wfDebug() wfDebugLog() + public $mDebugtext = ''; - /// Should be private. Stores contents of <title> tag + /// Should be private. Stores contents of "<title>" tag var $mHTMLtitle = ''; /// Should be private. Is the displayed content related to the source of the corresponding wiki article. @@ -99,8 +116,8 @@ class OutputPage extends ContextSource { /** * Should be private. Used for JavaScript (pre resource loader) * We should split js / css. - * mScripts content is inserted as is in <head> by Skin. This might contains - * either a link to a stylesheet or inline css. + * mScripts content is inserted as is in "<head>" by Skin. This might + * contains either a link to a stylesheet or inline css. */ var $mScripts = ''; @@ -118,7 +135,7 @@ class OutputPage extends ContextSource { */ var $mPageLinkTitle = ''; - /// Array of elements in <head>. Parser might add its own headers! + /// Array of elements in "<head>". Parser might add its own headers! var $mHeadItems = array(); // @todo FIXME: Next variables probably comes from the resource loader @@ -180,7 +197,7 @@ class OutputPage extends ContextSource { /** * Comes from the parser. This was probably made to load CSS/JS only - * if we had <gallery>. Used directly in CategoryPage.php + * if we had "<gallery>". Used directly in CategoryPage.php * Looks like resource loader can replace this. */ var $mNoGallery = false; @@ -220,7 +237,6 @@ class OutputPage extends ContextSource { private $mFollowPolicy = 'follow'; private $mVaryHeader = array( 'Accept-Encoding' => array( 'list-contains=gzip' ), - 'Cookie' => null ); /** @@ -276,7 +292,7 @@ class OutputPage extends ContextSource { } /** - * Add a new <meta> tag + * Add a new "<meta>" tag * To add an http-equiv meta tag, precede the name with "http:" * * @param $name String tag name @@ -389,7 +405,7 @@ class OutputPage extends ContextSource { /** * Add a self-contained script tag with the given contents * - * @param $script String: JavaScript text, no <script> tags + * @param $script String: JavaScript text, no "<script>" tags */ public function addInlineScript( $script ) { $this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n"; @@ -631,24 +647,16 @@ class OutputPage extends ContextSource { $maxModified = max( $modifiedTimes ); $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified ); - if( empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' ); + if ( $clientHeader === false ) { wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false ); return false; } - # Make debug info - $info = ''; - foreach ( $modifiedTimes as $name => $value ) { - if ( $info !== '' ) { - $info .= ', '; - } - $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value ); - } - # IE sends sizes after the date like this: # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 # this breaks strtotime(). - $clientHeader = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); + $clientHeader = preg_replace( '/;.*$/', '', $clientHeader ); wfSuppressWarnings(); // E_STRICT system time bitching $clientHeaderTime = strtotime( $clientHeader ); @@ -659,6 +667,15 @@ class OutputPage extends ContextSource { } $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime ); + # Make debug info + $info = ''; + foreach ( $modifiedTimes as $name => $value ) { + if ( $info !== '' ) { + $info .= ', '; + } + $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value ); + } + wfDebug( __METHOD__ . ": client sent If-Modified-Since: " . wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", false ); wfDebug( __METHOD__ . ": effective Last-Modified: " . @@ -763,7 +780,7 @@ class OutputPage extends ContextSource { } /** - * "HTML title" means the contents of <title>. + * "HTML title" means the contents of "<title>". * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file. * * @param $name string @@ -777,7 +794,7 @@ class OutputPage extends ContextSource { } /** - * Return the "HTML title", i.e. the content of the <title> tag. + * Return the "HTML title", i.e. the content of the "<title>" tag. * * @return String */ @@ -788,7 +805,7 @@ class OutputPage extends ContextSource { /** * Set $mRedirectedFrom, the Title of the page which redirected us to the current page. * - * param @t Title + * @param $t Title */ public function setRedirectedFrom( $t ) { $this->mRedirectedFrom = $t; @@ -1075,7 +1092,7 @@ class OutputPage extends ContextSource { /** * Add new language links * - * @param $newLinkArray Associative array mapping language code to the page + * @param $newLinkArray array Associative array mapping language code to the page * name */ public function addLanguageLinks( $newLinkArray ) { @@ -1085,7 +1102,7 @@ class OutputPage extends ContextSource { /** * Reset the language links and add new language links * - * @param $newLinkArray Associative array mapping language code to the page + * @param $newLinkArray array Associative array mapping language code to the page * name */ public function setLanguageLinks( $newLinkArray ) { @@ -1298,18 +1315,9 @@ class OutputPage extends ContextSource { } /** - * Add $text to the debug output - * - * @param $text String: debug text - */ - public function debug( $text ) { - $this->mDebugtext .= $text; - } - - /** * Get/set the ParserOptions object to use for wikitext parsing * - * @param $options either the ParserOption to use or null to only get the + * @param $options ParserOptions|null either the ParserOption to use or null to only get the * current ParserOption object * @return ParserOptions object */ @@ -1346,11 +1354,11 @@ class OutputPage extends ContextSource { * Set the timestamp of the revision which will be displayed. This is used * to avoid a extra DB call in Skin::lastModified(). * - * @param $revid Mixed: string, or null + * @param $timestamp Mixed: string, or null * @return Mixed: previous value */ - public function setRevisionTimestamp( $timestmap ) { - return wfSetVar( $this->mRevisionTimestamp, $timestmap ); + public function setRevisionTimestamp( $timestamp) { + return wfSetVar( $this->mRevisionTimestamp, $timestamp ); } /** @@ -1366,7 +1374,7 @@ class OutputPage extends ContextSource { /** * Set the displayed file version * - * @param $file File|false + * @param $file File|bool * @return Mixed: previous value */ public function setFileVersion( $file ) { @@ -1662,18 +1670,6 @@ class OutputPage extends ContextSource { } /** - * Return whether this page is not cacheable because "useskin" or "uselang" - * URL parameters were passed. - * - * @return Boolean - */ - function uncacheableBecauseRequestVars() { - $request = $this->getRequest(); - return $request->getText( 'useskin', false ) === false - && $request->getText( 'uselang', false ) === false; - } - - /** * Check if the request has a cache-varying cookie header * If it does, it's very important that we don't allow public caching * @@ -1718,6 +1714,16 @@ class OutputPage extends ContextSource { } /** + * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader, + * such as Accept-Encoding or Cookie + * + * @return String + */ + public function getVaryHeader() { + return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ); + } + + /** * Get a complete X-Vary-Options header * * @return String @@ -1734,7 +1740,7 @@ class OutputPage extends ContextSource { $headers = array(); foreach( $this->mVaryHeader as $header => $option ) { $newheader = $header; - if( is_array( $option ) ) { + if ( is_array( $option ) && count( $option ) > 0 ) { $newheader .= ';' . implode( ';', $option ); } $headers[] = $newheader; @@ -1829,18 +1835,19 @@ class OutputPage extends ContextSource { $response->header( "ETag: $this->mETag" ); } + $this->addVaryHeader( 'Cookie' ); $this->addAcceptLanguage(); # don't serve compressed data to clients who can't handle it # maintain different caches for logged-in users and non-logged in ones - $response->header( 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ) ); + $response->header( $this->getVaryHeader() ); if ( $wgUseXVO ) { # Add an X-Vary-Options header for Squid with Wikimedia patches $response->header( $this->getXVO() ); } - if( !$this->uncacheableBecauseRequestVars() && $this->mEnableClientCache ) { + if( $this->mEnableClientCache ) { if( $wgUseSquid && session_id() == '' && !$this->isPrintable() && $this->mSquidMaxage != 0 && !$this->haveCacheVaryCookies() @@ -1983,6 +1990,9 @@ class OutputPage extends ContextSource { wfProfileOut( 'Output-skin' ); } + // This hook allows last minute changes to final overall output by modifying output buffer + wfRunHooks( 'AfterFinalPageOutput', array( $this ) ); + $this->sendCacheControl(); ob_end_flush(); wfProfileOut( __METHOD__ ); @@ -2008,18 +2018,14 @@ class OutputPage extends ContextSource { /** * Prepare this object to display an error page; disable caching and * indexing, clear the current text and redirect, set the page's title - * and optionally an custom HTML title (content of the <title> tag). + * and optionally an custom HTML title (content of the "<title>" tag). * * @param $pageTitle String|Message will be passed directly to setPageTitle() * @param $htmlTitle String|Message will be passed directly to setHTMLTitle(); - * optional, if not passed the <title> attribute will be + * optional, if not passed the "<title>" attribute will be * based on $pageTitle */ public function prepareErrorPage( $pageTitle, $htmlTitle = false ) { - if ( $this->getTitle() ) { - $this->mDebugtext .= 'Original title: ' . $this->getTitle()->getPrefixedText() . "\n"; - } - $this->setPageTitle( $pageTitle ); if ( $htmlTitle !== false ) { $this->setHTMLTitle( $htmlTitle ); @@ -2037,13 +2043,18 @@ class OutputPage extends ContextSource { * * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) ); * showErrorPage( 'titlemsg', $messageObject ); + * showErrorPage( $titleMessageObj, $messageObject ); * - * @param $title String: message key for page title + * @param $title Mixed: message key (string) for page title, or a Message object * @param $msg Mixed: message key (string) for page text, or a Message object * @param $params Array: message parameters; ignored if $msg is a Message object */ public function showErrorPage( $title, $msg, $params = array() ) { - $this->prepareErrorPage( $this->msg( $title ), $this->msg( 'errorpagetitle' ) ); + if( !$title instanceof Message ) { + $title = $this->msg( $title ); + } + + $this->prepareErrorPage( $title ); if ( $msg instanceof Message ){ $this->addHTML( $msg->parse() ); @@ -2330,7 +2341,7 @@ $templates * Add a "return to" link pointing to a specified title * * @param $title Title to link - * @param $query String query string + * @param $query Array query string parameters * @param $text String text of the link (input is not escaped) */ public function addReturnTo( $title, $query = array(), $text = null ) { @@ -2344,7 +2355,7 @@ $templates * Add a "return to" link pointing to a specified title, * or the title indicated in the request, or else the main page * - * @param $unused No longer used + * @param $unused * @param $returnto Title or String to return to * @param $returntoquery String: query string for the return to link */ @@ -2370,13 +2381,13 @@ $templates $titleObj = Title::newMainPage(); } - $this->addReturnTo( $titleObj, $returntoquery ); + $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) ); } /** * @param $sk Skin The given Skin * @param $includeStyle Boolean: unused - * @return String: The doctype, opening <html>, and head element. + * @return String: The doctype, opening "<html>", and head element. */ public function headElement( Skin $sk, $includeStyle = true ) { global $wgContLang; @@ -2440,7 +2451,7 @@ $templates */ private function addDefaultModules() { global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax, - $wgAjaxWatch, $wgEnableMWSuggest; + $wgAjaxWatch; // Add base resources $this->addModules( array( @@ -2465,11 +2476,11 @@ $templates wfRunHooks( 'AjaxAddScript', array( &$this ) ); if( $wgAjaxWatch && $this->getUser()->isLoggedIn() ) { - $this->addModules( 'mediawiki.action.watch.ajax' ); + $this->addModules( 'mediawiki.page.watch.ajax' ); } - if ( $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) { - $this->addModules( 'mediawiki.legacy.mwsuggest' ); + if ( !$this->getUser()->getOption( 'disablesuggest', false ) ) { + $this->addModules( 'mediawiki.searchSuggest' ); } } @@ -2501,19 +2512,21 @@ $templates * @param $only String ResourceLoaderModule TYPE_ class constant * @param $useESI boolean * @param $extraQuery Array with extra query parameters to add to each request. array( param => value ) - * @param $loadCall boolean If true, output an (asynchronous) mw.loader.load() call rather than a <script src="..."> tag - * @return string html <script> and <style> tags + * @param $loadCall boolean If true, output an (asynchronous) mw.loader.load() call rather than a "<script src='...'>" tag + * @return string html "<script>" and "<style>" tags */ protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) { global $wgResourceLoaderUseESI; + $modules = (array) $modules; + if ( !count( $modules ) ) { return ''; } if ( count( $modules ) > 1 ) { // Remove duplicate module requests - $modules = array_unique( (array) $modules ); + $modules = array_unique( $modules ); // Sort module names so requests are more uniform sort( $modules ); @@ -2530,7 +2543,7 @@ $templates // Create keyed-by-group list of module objects from modules list $groups = array(); $resourceLoader = $this->getResourceLoader(); - foreach ( (array) $modules as $name ) { + foreach ( $modules as $name ) { $module = $resourceLoader->getModule( $name ); # Check that we're allowed to include this module on this page if ( !$module @@ -2551,7 +2564,7 @@ $templates } $links = ''; - foreach ( $groups as $group => $modules ) { + foreach ( $groups as $group => $grpModules ) { // Special handling for user-specific groups $user = null; if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) { @@ -2573,14 +2586,30 @@ $templates $extraQuery ); $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) ); - // Drop modules that know they're empty - foreach ( $modules as $key => $module ) { + // Extract modules that know they're empty + $emptyModules = array (); + foreach ( $grpModules as $key => $module ) { if ( $module->isKnownEmpty( $context ) ) { - unset( $modules[$key] ); + $emptyModules[$key] = 'ready'; + unset( $grpModules[$key] ); } } + // Inline empty modules: since they're empty, just mark them as 'ready' + if ( count( $emptyModules ) > 0 && $only !== ResourceLoaderModule::TYPE_STYLES ) { + // If we're only getting the styles, we don't need to do anything for empty modules. + $links .= Html::inlineScript( + + ResourceLoader::makeLoaderConditionalScript( + + ResourceLoader::makeLoaderStateScript( $emptyModules ) + + ) + + ) . "\n"; + } + // If there are no modules left, skip this group - if ( $modules === array() ) { + if ( count( $grpModules ) === 0 ) { continue; } @@ -2591,12 +2620,12 @@ $templates if ( $group === 'private' ) { if ( $only == ResourceLoaderModule::TYPE_STYLES ) { $links .= Html::inlineStyle( - $resourceLoader->makeModuleResponse( $context, $modules ) + $resourceLoader->makeModuleResponse( $context, $grpModules ) ); } else { $links .= Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( - $resourceLoader->makeModuleResponse( $context, $modules ) + $resourceLoader->makeModuleResponse( $context, $grpModules ) ) ); } @@ -2612,7 +2641,7 @@ $templates if ( $group === 'user' ) { // Get the maximum timestamp $timestamp = 1; - foreach ( $modules as $module ) { + foreach ( $grpModules as $module ) { $timestamp = max( $timestamp, $module->getModifiedTime( $context ) ); } // Add a version parameter so cache will break when things change @@ -2620,7 +2649,7 @@ $templates } $url = ResourceLoader::makeLoaderURL( - array_keys( $modules ), + array_keys( $grpModules ), $this->getLanguage()->getCode(), $this->getSkin()->getSkinName(), $user, @@ -2642,7 +2671,7 @@ $templates // Automatically select style/script elements if ( $only === ResourceLoaderModule::TYPE_STYLES ) { $link = Html::linkedStyle( $url ); - } else if ( $loadCall ) { + } else if ( $loadCall ) { $link = Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) ) @@ -2663,14 +2692,14 @@ $templates } /** - * JS stuff to put in the <head>. This is the startup module, config + * JS stuff to put in the "<head>". This is the startup module, config * vars and modules marked with position 'top' * * @return String: HTML fragment */ function getHeadScripts() { global $wgResourceLoaderExperimentalAsyncLoading; - + // Startup - this will immediately load jquery and mediawiki modules $scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true ); @@ -2702,7 +2731,7 @@ $templates ) ); } - + if ( $wgResourceLoaderExperimentalAsyncLoading ) { $scripts .= $this->getScriptsForBottomQueue( true ); } @@ -2711,12 +2740,12 @@ $templates } /** - * JS stuff to put at the 'bottom', which can either be the bottom of the <body> - * or the bottom of the <head> depending on $wgResourceLoaderExperimentalAsyncLoading: + * JS stuff to put at the 'bottom', which can either be the bottom of the "<body>" + * or the bottom of the "<head>" depending on $wgResourceLoaderExperimentalAsyncLoading: * modules marked with position 'bottom', legacy scripts ($this->mScripts), * user preferences, site JS and user JS * - * @param $inHead boolean If true, this HTML goes into the <head>, if false it goes into the <body> + * @param $inHead boolean If true, this HTML goes into the "<head>", if false it goes into the "<body>" * @return string */ function getScriptsForBottomQueue( $inHead ) { @@ -2748,47 +2777,92 @@ $templates // Legacy Scripts $scripts .= "\n" . $this->mScripts; - $userScripts = array(); + $defaultModules = array(); // Add site JS if enabled if ( $wgUseSiteJs ) { $scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS, /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead ); - if( $this->getUser()->isLoggedIn() ){ - $userScripts[] = 'user.groups'; - } + $defaultModules['site'] = 'loading'; + } else { + // The wiki is configured to not allow a site module. + $defaultModules['site'] = 'missing'; } // Add user JS if enabled - if ( $wgAllowUserJs && $this->getUser()->isLoggedIn() ) { - if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) { - # XXX: additional security check/prompt? - // We're on a preview of a JS subpage - // Exclude this page from the user module in case it's in there (bug 26283) - $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false, - array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead - ); - // Load the previewed JS - $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n"; + if ( $wgAllowUserJs ) { + if ( $this->getUser()->isLoggedIn() ) { + if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) { + # XXX: additional security check/prompt? + // We're on a preview of a JS subpage + // Exclude this page from the user module in case it's in there (bug 26283) + $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false, + array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead + ); + // Load the previewed JS + $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n"; + // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded + // asynchronously and may arrive *after* the inline script here. So the previewed code + // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js... + } else { + // Include the user module normally, i.e., raw to avoid it being wrapped in a closure. + $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, + /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead + ); + } + $defaultModules['user'] = 'loading'; } else { - // Include the user module normally - // We can't do $userScripts[] = 'user'; because the user module would end up - // being wrapped in a closure, so load it raw like 'site' - $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, + // Non-logged-in users have no user module. Treat it as empty and 'ready' to avoid + // blocking default gadgets that might depend on it. Although arguably default-enabled + // gadgets should not depend on the user module, it's harmless and less error-prone to + // handle this case. + $defaultModules['user'] = 'ready'; + } + } else { + // User JS disabled + $defaultModules['user'] = 'missing'; + } + + // Group JS is only enabled if site JS is enabled. + if ( $wgUseSiteJs ) { + if ( $this->getUser()->isLoggedIn() ) { + $scripts .= $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED, /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead ); + $defaultModules['user.groups'] = 'loading'; + } else { + // Non-logged-in users have no user.groups module. Treat it as empty and 'ready' to + // avoid blocking gadgets that might depend upon the module. + $defaultModules['user.groups'] = 'ready'; } + } else { + // Site (and group JS) disabled + $defaultModules['user.groups'] = 'missing'; } - $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED, - /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead - ); - return $scripts; + $loaderInit = ''; + if ( $inHead ) { + // We generate loader calls anyway, so no need to fix the client-side loader's state to 'loading'. + foreach ( $defaultModules as $m => $state ) { + if ( $state == 'loading' ) { + unset( $defaultModules[$m] ); + } + } + } + if ( count( $defaultModules ) > 0 ) { + $loaderInit = Html::inlineScript( + ResourceLoader::makeLoaderConditionalScript( + ResourceLoader::makeLoaderStateScript( $defaultModules ) + ) + ) . "\n"; + } + return $loaderInit . $scripts; } /** - * JS stuff to put at the bottom of the <body> + * JS stuff to put at the bottom of the "<body>" + * @return string */ function getBottomScripts() { global $wgResourceLoaderExperimentalAsyncLoading; @@ -2802,7 +2876,7 @@ $templates /** * Add one or more variables to be set in mw.config in JavaScript. * - * @param $key {String|Array} Key or array of key/value pars. + * @param $keys {String|Array} Key or array of key/value pairs. * @param $value {Mixed} [optional] Value of the configuration variable. */ public function addJsConfigVars( $keys, $value = null ) { @@ -2830,7 +2904,7 @@ $templates * @return array */ public function getJSVars() { - global $wgUseAjax, $wgEnableMWSuggest; + global $wgUseAjax, $wgContLang; $latestRevID = 0; $pageID = 0; @@ -2885,17 +2959,17 @@ $templates 'wgPageContentLanguage' => $lang->getCode(), 'wgSeparatorTransformTable' => $compactSeparatorTransTable, 'wgDigitTransformTable' => $compactDigitTransTable, + 'wgDefaultDateFormat' => $lang->getDefaultDateFormat(), + 'wgMonthNames' => $lang->getMonthNamesArray(), + 'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(), 'wgRelevantPageName' => $relevantTitle->getPrefixedDBKey(), ); - if ( $lang->hasVariants() ) { - $vars['wgUserVariant'] = $lang->getPreferredVariant(); + if ( $wgContLang->hasVariants() ) { + $vars['wgUserVariant'] = $wgContLang->getPreferredVariant(); } foreach ( $title->getRestrictionTypes() as $type ) { $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type ); } - if ( $wgUseAjax && $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) { - $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $this->getUser() ); - } if ( $title->isMainPage() ) { $vars['wgIsMainPage'] = true; } @@ -2938,12 +3012,11 @@ $templates } /** - * @param $unused Unused - * @param $addContentType bool + * @param $addContentType bool: Whether "<meta>" specifying content type should be returned * - * @return string HTML tag links to be put in the header. + * @return array in format "link name or number => 'link html'". */ - public function getHeadLinks( $unused = null, $addContentType = false ) { + public function getHeadLinksArray( $addContentType = false ) { global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI, $wgSitename, $wgVersion, $wgHtml5, $wgMimeType, $wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes, @@ -2956,20 +3029,20 @@ $templates if ( $wgHtml5 ) { # More succinct than <meta http-equiv=Content-Type>, has the # same effect - $tags[] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) ); + $tags['meta-charset'] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) ); } else { - $tags[] = Html::element( 'meta', array( + $tags['meta-content-type'] = Html::element( 'meta', array( 'http-equiv' => 'Content-Type', 'content' => "$wgMimeType; charset=UTF-8" ) ); - $tags[] = Html::element( 'meta', array( // bug 15835 + $tags['meta-content-style-type'] = Html::element( 'meta', array( // bug 15835 'http-equiv' => 'Content-Style-Type', 'content' => 'text/css' ) ); } } - $tags[] = Html::element( 'meta', array( + $tags['meta-generator'] = Html::element( 'meta', array( 'name' => 'generator', 'content' => "MediaWiki $wgVersion", ) ); @@ -2978,7 +3051,7 @@ $templates if( $p !== 'index,follow' ) { // http://www.robotstxt.org/wc/meta-user.html // Only show if it's different from the default robots policy - $tags[] = Html::element( 'meta', array( + $tags['meta-robots'] = Html::element( 'meta', array( 'name' => 'robots', 'content' => $p, ) ); @@ -2989,7 +3062,7 @@ $templates "/<.*?" . ">/" => '', "/_/" => ' ' ); - $tags[] = Html::element( 'meta', array( + $tags['meta-keywords'] = Html::element( 'meta', array( 'name' => 'keywords', 'content' => preg_replace( array_keys( $strip ), @@ -3006,7 +3079,11 @@ $templates } else { $a = 'name'; } - $tags[] = Html::element( 'meta', + $tagName = "meta-{$tag[0]}"; + if ( isset( $tags[$tagName] ) ) { + $tagName .= $tag[1]; + } + $tags[$tagName] = Html::element( 'meta', array( $a => $tag[0], 'content' => $tag[1] @@ -3025,14 +3102,14 @@ $templates && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) { // Original UniversalEditButton $msg = $this->msg( 'edit' )->text(); - $tags[] = Html::element( 'link', array( + $tags['universal-edit-button'] = Html::element( 'link', array( 'rel' => 'alternate', 'type' => 'application/x-wiki', 'title' => $msg, 'href' => $this->getTitle()->getLocalURL( 'action=edit' ) ) ); // Alternate edit link - $tags[] = Html::element( 'link', array( + $tags['alternative-edit'] = Html::element( 'link', array( 'rel' => 'edit', 'title' => $msg, 'href' => $this->getTitle()->getLocalURL( 'action=edit' ) @@ -3045,15 +3122,15 @@ $templates # uses whichever one appears later in the HTML source. Make sure # apple-touch-icon is specified first to avoid this. if ( $wgAppleTouchIcon !== false ) { - $tags[] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) ); + $tags['apple-touch-icon'] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) ); } if ( $wgFavicon !== false ) { - $tags[] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); + $tags['favicon'] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); } # OpenSearch description link - $tags[] = Html::element( 'link', array( + $tags['opensearch'] = Html::element( 'link', array( 'rel' => 'search', 'type' => 'application/opensearchdescription+xml', 'href' => wfScript( 'opensearch_desc' ), @@ -3065,7 +3142,7 @@ $templates # for the MediaWiki API (and potentially additional custom API # support such as WordPress or Twitter-compatible APIs for a # blogging extension, etc) - $tags[] = Html::element( 'link', array( + $tags['rsd'] = Html::element( 'link', array( 'rel' => 'EditURI', 'type' => 'application/rsd+xml', // Output a protocol-relative URL here if $wgServer is protocol-relative @@ -3085,14 +3162,14 @@ $templates if ( !$urlvar ) { $variants = $lang->getVariants(); foreach ( $variants as $_v ) { - $tags[] = Html::element( 'link', array( + $tags["variant-$_v"] = Html::element( 'link', array( 'rel' => 'alternate', 'hreflang' => $_v, 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) ) ); } } else { - $tags[] = Html::element( 'link', array( + $tags['canonical'] = Html::element( 'link', array( 'rel' => 'canonical', 'href' => $this->getTitle()->getCanonicalUrl() ) ); @@ -3115,7 +3192,7 @@ $templates } if ( $copyright ) { - $tags[] = Html::element( 'link', array( + $tags['copyright'] = Html::element( 'link', array( 'rel' => 'copyright', 'href' => $copyright ) ); @@ -3164,11 +3241,21 @@ $templates } } } - return implode( "\n", $tags ); + return $tags; + } + + /** + * @param $unused + * @param $addContentType bool: Whether "<meta>" specifying content type should be returned + * + * @return string HTML tag links to be put in the header. + */ + public function getHeadLinks( $unused = null, $addContentType = false ) { + return implode( "\n", $this->getHeadLinksArray( $addContentType ) ); } /** - * Generate a <link rel/> for a feed. + * Generate a "<link rel/>" for a feed. * * @param $type String: feed type * @param $url String: URL to the feed @@ -3223,7 +3310,7 @@ $templates } /** - * Build a set of <link>s for the stylesheets specified in the $this->styles array. + * Build a set of "<link>" elements for the stylesheets specified in the $this->styles array. * These will be applied to various media & IE conditionals. * * @return string @@ -3259,7 +3346,7 @@ $templates $otherTags .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false, array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ) ); - + // Load the previewed CSS // If needed, Janus it first. This is user-supplied CSS, so it's // assumed to be right for the content language directionality. @@ -3421,7 +3508,7 @@ $templates * Add a wikitext-formatted message to the output. * This is equivalent to: * - * $wgOut->addWikiText( wfMsgNoTrans( ... ) ) + * $wgOut->addWikiText( wfMessage( ... )->plain() ) */ public function addWikiMsg( /*...*/ ) { $args = func_get_args(); @@ -3450,9 +3537,6 @@ $templates * message names, or arrays, in which case the first element is the message name, * and subsequent elements are the parameters to that message. * - * The special named parameter 'options' in a message specification array is passed - * through to the $options parameter of wfMsgExt(). - * * Don't use this for messages that are not in users interface language. * * For example: @@ -3461,7 +3545,7 @@ $templates * * Is equivalent to: * - * $wgOut->addWikiText( "<div class='error'>\n" . wfMsgNoTrans( 'some-error' ) . "\n</div>" ); + * $wgOut->addWikiText( "<div class='error'>\n" . wfMessage( 'some-error' )->plain() . "\n</div>" ); * * The newline after opening div is needed in some wikitext. See bug 19226. * @@ -3478,14 +3562,17 @@ $templates $args = $spec; $name = array_shift( $args ); if ( isset( $args['options'] ) ) { - $options = $args['options']; unset( $args['options'] ); + wfDeprecated( + 'Adding "options" to ' . __METHOD__ . ' is no longer supported', + '1.20' + ); } } else { $args = array(); $name = $spec; } - $s = str_replace( '$' . ( $n + 1 ), wfMsgExt( $name, $options, $args ), $s ); + $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s ); } $this->addWikiText( $s ); } diff --git a/includes/PHPVersionError.php b/includes/PHPVersionError.php index ec6490a8..dad71f82 100644 --- a/includes/PHPVersionError.php +++ b/includes/PHPVersionError.php @@ -1,6 +1,27 @@ <?php /** * Display something vaguely comprehensible in the event of a totally unrecoverable error. + * + * 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 + */ + +/** + * Display something vaguely comprehensible in the event of a totally unrecoverable error. * Does not assume access to *anything*; no globals, no autloader, no database, no localisation. * Safe for PHP4 (and putting this here means that WebStart.php and GlobalSettings.php * no longer need to be). @@ -17,9 +38,9 @@ * version are hardcoded here */ function wfPHPVersionError( $type ){ - $mwVersion = '1.19'; + $mwVersion = '1.20'; $phpVersion = PHP_VERSION; - $message = "MediaWiki $mwVersion requires at least PHP version 5.2.3, you are using PHP $phpVersion."; + $message = "MediaWiki $mwVersion requires at least PHP version 5.3.2, you are using PHP $phpVersion."; if( $type == 'index.php' ) { $encLogo = htmlspecialchars( str_replace( '//', '/', pathinfo( $_SERVER['SCRIPT_NAME'], PATHINFO_DIRNAME ) . '/' diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php index dc5e971d..01a2439f 100644 --- a/includes/PageQueryPage.php +++ b/includes/PageQueryPage.php @@ -1,4 +1,25 @@ <?php +/** + * Variant of QueryPage which formats the result as a simple link to the page. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup SpecialPage + */ /** * Variant of QueryPage which formats the result as a simple link to the page @@ -16,11 +37,15 @@ abstract class PageQueryPage extends QueryPage { */ public function formatResult( $skin, $row ) { global $wgContLang; + $title = Title::makeTitleSafe( $row->namespace, $row->title ); - $text = $row->title; + if ( $title instanceof Title ) { $text = $wgContLang->convert( $title->getPrefixedText() ); + return Linker::linkKnown( $title, htmlspecialchars( $text ) ); + } else { + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $row->namespace, $row->title ) ); } - return Linker::linkKnown( $title, htmlspecialchars( $text ) ); } } diff --git a/includes/Pager.php b/includes/Pager.php index faae3d2d..96ba446e 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -1,12 +1,31 @@ <?php /** - * @defgroup Pager Pager + * Efficient paging for SQL queries. + * + * 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 Pager */ /** + * @defgroup Pager Pager + */ + +/** * Basic pager interface. * @ingroup Pager */ @@ -100,6 +119,11 @@ abstract class IndexPager extends ContextSource implements Pager { protected $mLastShown, $mFirstShown, $mPastTheEndIndex, $mDefaultQuery, $mNavigationBar; /** + * Whether to include the offset in the query + */ + protected $mIncludeOffset = false; + + /** * Result object for the query. Warning: seek before use. * * @var ResultWrapper @@ -120,7 +144,10 @@ abstract class IndexPager extends ContextSource implements Pager { # Use consistent behavior for the limit options $this->mDefaultLimit = intval( $this->getUser()->getOption( 'rclimit' ) ); - list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset(); + if ( !$this->mLimit ) { + // Don't override if a subclass calls $this->setLimit() in its constructor. + list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset(); + } $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' ); $this->mDb = wfGetDB( DB_SLAVE ); @@ -157,6 +184,15 @@ abstract class IndexPager extends ContextSource implements Pager { } /** + * Get the Database object in use + * + * @return DatabaseBase + */ + public function getDatabase() { + return $this->mDb; + } + + /** * Do the query, using information from the object context. This function * has been kept minimal to make it overridable if necessary, to allow for * result sets formed from multiple DB queries. @@ -175,6 +211,7 @@ abstract class IndexPager extends ContextSource implements Pager { $queryLimit, $descending ); + $this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult ); $this->mQueryDone = true; @@ -202,10 +239,30 @@ abstract class IndexPager extends ContextSource implements Pager { /** * Set the limit from an other source than the request * + * Verifies limit is between 1 and 5000 + * * @param $limit Int|String */ function setLimit( $limit ) { - $this->mLimit = $limit; + $limit = (int) $limit; + // WebRequest::getLimitOffset() puts a cap of 5000, so do same here. + if ( $limit > 5000 ) { + $limit = 5000; + } + if ( $limit > 0 ) { + $this->mLimit = $limit; + } + } + + /** + * Set whether a row matching exactly the offset should be also included + * in the result or not. By default this is not the case, but when the + * offset is user-supplied this might be wanted. + * + * @param $include bool + */ + public function setIncludeOffset( $include ) { + $this->mIncludeOffset = $include; } /** @@ -284,7 +341,20 @@ abstract class IndexPager extends ContextSource implements Pager { * @param $descending Boolean: query direction, false for ascending, true for descending * @return ResultWrapper */ - function reallyDoQuery( $offset, $limit, $descending ) { + public function reallyDoQuery( $offset, $limit, $descending ) { + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $descending ); + return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); + } + + /** + * Build variables to use by the database wrapper. + * + * @param $offset String: index offset, inclusive + * @param $limit Integer: exact query limit + * @param $descending Boolean: query direction, false for ascending, true for descending + * @return array + */ + protected function buildQueryInfo( $offset, $limit, $descending ) { $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')'; $info = $this->getQueryInfo(); $tables = $info['tables']; @@ -294,22 +364,21 @@ abstract class IndexPager extends ContextSource implements Pager { $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array(); $sortColumns = array_merge( array( $this->mIndexField ), $this->mExtraSortFields ); if ( $descending ) { - $options['ORDER BY'] = implode( ',', $sortColumns ); - $operator = '>'; + $options['ORDER BY'] = $sortColumns; + $operator = $this->mIncludeOffset ? '>=' : '>'; } else { $orderBy = array(); foreach ( $sortColumns as $col ) { $orderBy[] = $col . ' DESC'; } - $options['ORDER BY'] = implode( ',', $orderBy ); - $operator = '<'; + $options['ORDER BY'] = $orderBy; + $operator = $this->mIncludeOffset ? '<=' : '<'; } if ( $offset != '' ) { $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ); } $options['LIMIT'] = intval( $limit ); - $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); - return new ResultWrapper( $this->mDb, $res ); + return array( $tables, $fields, $conds, $fname, $options, $join_conds ); } /** @@ -366,9 +435,10 @@ abstract class IndexPager extends ContextSource implements Pager { * @param $text String: text displayed on the link * @param $query Array: associative array of paramter to be in the query string * @param $type String: value of the "rel" attribute + * * @return String: HTML fragment */ - function makeLink($text, $query = null, $type=null) { + function makeLink( $text, array $query = null, $type = null ) { if ( $query === null ) { return $text; } @@ -382,6 +452,7 @@ abstract class IndexPager extends ContextSource implements Pager { if( $type ) { $attrs['class'] = "mw-{$type}link"; } + return Linker::linkKnown( $this->getTitle(), $text, @@ -433,7 +504,7 @@ abstract class IndexPager extends ContextSource implements Pager { * By default, all parameters passed in the URL are used, except for a * short blacklist. * - * @return Associative array + * @return array Associative array */ function getDefaultQuery() { if ( !isset( $this->mDefaultQuery ) ) { @@ -526,6 +597,7 @@ abstract class IndexPager extends ContextSource implements Pager { function getPagingLinks( $linkTexts, $disabledTexts = array() ) { $queries = $this->getPagingQueries(); $links = array(); + foreach ( $queries as $type => $query ) { if ( $query !== false ) { $links[$type] = $this->makeLink( @@ -539,6 +611,7 @@ abstract class IndexPager extends ContextSource implements Pager { $links[$type] = $linkTexts[$type]; } } + return $links; } @@ -651,41 +724,32 @@ abstract class AlphabeticPager extends IndexPager { * @return String HTML */ function getNavigationBar() { - if ( !$this->isNavigationBarShown() ) return ''; + if ( !$this->isNavigationBarShown() ) { + return ''; + } if( isset( $this->mNavigationBar ) ) { return $this->mNavigationBar; } - $lang = $this->getLanguage(); - - $opts = array( 'parsemag', 'escapenoentities' ); $linkTexts = array( - 'prev' => wfMsgExt( - 'prevn', - $opts, - $lang->formatNum( $this->mLimit ) - ), - 'next' => wfMsgExt( - 'nextn', - $opts, - $lang->formatNum($this->mLimit ) - ), - 'first' => wfMsgExt( 'page_first', $opts ), - 'last' => wfMsgExt( 'page_last', $opts ) + 'prev' => $this->msg( 'prevn' )->numParams( $this->mLimit )->escaped(), + 'next' => $this->msg( 'nextn' )->numParams( $this->mLimit )->escaped(), + 'first' => $this->msg( 'page_first' )->escaped(), + 'last' => $this->msg( 'page_last' )->escaped() ); + $lang = $this->getLanguage(); + $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = $lang->pipeList( $limitLinks ); - $this->mNavigationBar = - "(" . $lang->pipeList( - array( $pagingLinks['first'], - $pagingLinks['last'] ) - ) . ") " . - wfMsgHtml( 'viewprevnext', $pagingLinks['prev'], - $pagingLinks['next'], $limits ); + $this->mNavigationBar = $this->msg( 'parentheses' )->rawParams( + $lang->pipeList( array( $pagingLinks['first'], + $pagingLinks['last'] ) ) )->escaped() . " " . + $this->msg( 'viewprevnext' )->rawParams( $pagingLinks['prev'], + $pagingLinks['next'], $limits )->escaped(); if( !is_array( $this->getIndexField() ) ) { # Early return to avoid undue nesting @@ -699,21 +763,22 @@ abstract class AlphabeticPager extends IndexPager { if( $first ) { $first = false; } else { - $extra .= wfMsgExt( 'pipe-separator' , 'escapenoentities' ); + $extra .= $this->msg( 'pipe-separator' )->escaped(); } if( $order == $this->mOrderType ) { - $extra .= wfMsgHTML( $msgs[$order] ); + $extra .= $this->msg( $msgs[$order] )->escaped(); } else { $extra .= $this->makeLink( - wfMsgHTML( $msgs[$order] ), + $this->msg( $msgs[$order] )->escaped(), array( 'order' => $order ) ); } } if( $extra !== '' ) { - $this->mNavigationBar .= " ($extra)"; + $extra = ' ' . $this->msg( 'parentheses' )->rawParams( $extra )->escaped(); + $this->mNavigationBar .= $extra; } return $this->mNavigationBar; @@ -750,43 +815,35 @@ abstract class ReverseChronologicalPager extends IndexPager { return $this->mNavigationBar; } - $nicenumber = $this->getLanguage()->formatNum( $this->mLimit ); $linkTexts = array( - 'prev' => wfMsgExt( - 'pager-newer-n', - array( 'parsemag', 'escape' ), - $nicenumber - ), - 'next' => wfMsgExt( - 'pager-older-n', - array( 'parsemag', 'escape' ), - $nicenumber - ), - 'first' => wfMsgHtml( 'histlast' ), - 'last' => wfMsgHtml( 'histfirst' ) + 'prev' => $this->msg( 'pager-newer-n' )->numParams( $this->mLimit )->escaped(), + 'next' => $this->msg( 'pager-older-n' )->numParams( $this->mLimit )->escaped(), + 'first' => $this->msg( 'histlast' )->escaped(), + 'last' => $this->msg( 'histfirst' )->escaped() ); $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = $this->getLanguage()->pipeList( $limitLinks ); + $firstLastLinks = $this->msg( 'parentheses' )->rawParams( "{$pagingLinks['first']}" . + $this->msg( 'pipe-separator' )->escaped() . + "{$pagingLinks['last']}" )->escaped(); + + $this->mNavigationBar = $firstLastLinks . ' ' . + $this->msg( 'viewprevnext' )->rawParams( + $pagingLinks['prev'], $pagingLinks['next'], $limits )->escaped(); - $this->mNavigationBar = "({$pagingLinks['first']}" . - wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . - "{$pagingLinks['last']}) " . - wfMsgHTML( - 'viewprevnext', - $pagingLinks['prev'], $pagingLinks['next'], - $limits - ); return $this->mNavigationBar; } function getDateCond( $year, $month ) { $year = intval( $year ); $month = intval( $month ); + // Basic validity checks $this->mYear = $year > 0 ? $year : false; $this->mMonth = ( $month > 0 && $month < 13 ) ? $month : false; + // Given an optional year and month, we need to generate a timestamp // to use as "WHERE rev_timestamp <= result" // Examples: year = 2006 equals < 20070101 (+000000) @@ -795,6 +852,7 @@ abstract class ReverseChronologicalPager extends IndexPager { if ( !$this->mYear && !$this->mMonth ) { return; } + if ( $this->mYear ) { $year = $this->mYear; } else { @@ -805,6 +863,7 @@ abstract class ReverseChronologicalPager extends IndexPager { $year--; } } + if ( $this->mMonth ) { $month = $this->mMonth + 1; // For December, we want January 1 of the next year @@ -817,14 +876,18 @@ abstract class ReverseChronologicalPager extends IndexPager { $month = 1; $year++; } + // Y2K38 bug if ( $year > 2032 ) { $year = 2032; } + $ymd = (int)sprintf( "%04d%02d01", $year, $month ); + if ( $ymd > 20320101 ) { $ymd = 20320101; } + $this->mOffset = $this->mDb->timestamp( "${ymd}000000" ); } } @@ -837,7 +900,7 @@ abstract class TablePager extends IndexPager { var $mSort; var $mCurrentRow; - function __construct( IContextSource $context = null ) { + public function __construct( IContextSource $context = null ) { if ( $context ) { $this->setContext( $context ); } @@ -855,12 +918,16 @@ abstract class TablePager extends IndexPager { parent::__construct(); } + /** + * @protected + * @return string + */ function getStartBody() { global $wgStylePath; $tableClass = htmlspecialchars( $this->getTableClass() ); $sortClass = htmlspecialchars( $this->getSortHeaderClass() ); - $s = "<table style='border:1;' class=\"mw-datatable $tableClass\"><thead><tr>\n"; + $s = "<table style='border:1px;' class=\"mw-datatable $tableClass\"><thead><tr>\n"; $fields = $this->getFieldNames(); # Make table header @@ -877,18 +944,18 @@ abstract class TablePager extends IndexPager { $image = 'Arr_d.png'; $query['asc'] = '1'; $query['desc'] = ''; - $alt = htmlspecialchars( wfMsg( 'descending_abbrev' ) ); + $alt = $this->msg( 'descending_abbrev' )->escaped(); } else { # Ascending $image = 'Arr_u.png'; $query['asc'] = ''; $query['desc'] = '1'; - $alt = htmlspecialchars( wfMsg( 'ascending_abbrev' ) ); + $alt = $this->msg( 'ascending_abbrev' )->escaped(); } $image = htmlspecialchars( "$wgStylePath/common/images/$image" ); $link = $this->makeLink( "<img width=\"12\" height=\"12\" alt=\"$alt\" src=\"$image\" />" . - htmlspecialchars( $name ), $query ); + htmlspecialchars( $name ), $query ); $s .= "<th class=\"$sortClass\">$link</th>\n"; } else { $s .= '<th>' . $this->makeLink( htmlspecialchars( $name ), $query ) . "</th>\n"; @@ -901,39 +968,55 @@ abstract class TablePager extends IndexPager { return $s; } + /** + * @protected + * @return string + */ function getEndBody() { return "</tbody></table>\n"; } + /** + * @protected + * @return string + */ function getEmptyBody() { $colspan = count( $this->getFieldNames() ); - $msgEmpty = wfMsgHtml( 'table_pager_empty' ); + $msgEmpty = $this->msg( 'table_pager_empty' )->escaped(); return "<tr><td colspan=\"$colspan\">$msgEmpty</td></tr>\n"; } /** - * @param $row Array + * @protected + * @param stdClass $row * @return String HTML */ function formatRow( $row ) { - $this->mCurrentRow = $row; # In case formatValue etc need to know + $this->mCurrentRow = $row; // In case formatValue etc need to know $s = Xml::openElement( 'tr', $this->getRowAttrs( $row ) ); $fieldNames = $this->getFieldNames(); + foreach ( $fieldNames as $field => $name ) { $value = isset( $row->$field ) ? $row->$field : null; $formatted = strval( $this->formatValue( $field, $value ) ); + if ( $formatted == '' ) { $formatted = ' '; } + $s .= Xml::tags( 'td', $this->getCellAttrs( $field, $value ), $formatted ); } + $s .= "</tr>\n"; + return $s; } /** * Get a class name to be applied to the given row. * + * @protected + * * @param $row Object: the database result row * @return String */ @@ -944,8 +1027,10 @@ abstract class TablePager extends IndexPager { /** * Get attributes to be applied to the given row. * + * @protected + * * @param $row Object: the database result row - * @return Array of <attr> => <value> + * @return Array of attribute => value */ function getRowAttrs( $row ) { $class = $this->getRowClass( $row ); @@ -962,6 +1047,8 @@ abstract class TablePager extends IndexPager { * take this as an excuse to hardcode styles; use classes and * CSS instead. Row context is available in $this->mCurrentRow * + * @protected + * * @param $field String The column * @param $value String The cell contents * @return Array of attr => value @@ -970,18 +1057,34 @@ abstract class TablePager extends IndexPager { return array( 'class' => 'TablePager_col_' . $field ); } + /** + * @protected + * @return string + */ function getIndexField() { return $this->mSort; } + /** + * @protected + * @return string + */ function getTableClass() { return 'TablePager'; } + /** + * @protected + * @return string + */ function getNavClass() { return 'TablePager_nav'; } + /** + * @protected + * @return string + */ function getSortHeaderClass() { return 'TablePager_sort'; } @@ -990,7 +1093,7 @@ abstract class TablePager extends IndexPager { * A navigation bar with images * @return String HTML */ - function getNavigationBar() { + public function getNavigationBar() { global $wgStylePath; if ( !$this->isNavigationBarShown() ) { @@ -1025,7 +1128,7 @@ abstract class TablePager extends IndexPager { $linkTexts = array(); $disabledTexts = array(); foreach ( $labels as $type => $label ) { - $msgLabel = wfMsgHtml( $label ); + $msgLabel = $this->msg( $label )->escaped(); $linkTexts[$type] = "<img src=\"$path/{$images[$type]}\" alt=\"$msgLabel\"/><br />$msgLabel"; $disabledTexts[$type] = "<img src=\"$path/{$disabledImages[$type]}\" alt=\"$msgLabel\"/><br />$msgLabel"; } @@ -1042,11 +1145,11 @@ abstract class TablePager extends IndexPager { } /** - * Get a <select> element which has options for each of the allowed limits + * Get a "<select>" element which has options for each of the allowed limits * * @return String: HTML fragment */ - function getLimitSelect() { + public function getLimitSelect() { # Add the current limit from the query string # to avoid that the limit is lost after clicking Go next time if ( !in_array( $this->mLimit, $this->mLimitsShown ) ) { @@ -1072,7 +1175,7 @@ abstract class TablePager extends IndexPager { } /** - * Get <input type="hidden"> elements for use in a method="get" form. + * Get \<input type="hidden"\> elements for use in a method="get" form. * Resubmits all defined elements of the query string, except for a * blacklist, passed in the $blacklist parameter. * @@ -1117,9 +1220,10 @@ abstract class TablePager extends IndexPager { */ function getLimitDropdown() { # Make the select with some explanatory text - $msgSubmit = wfMsgHtml( 'table_pager_limit_submit' ); + $msgSubmit = $this->msg( 'table_pager_limit_submit' )->escaped(); - return wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) . + return $this->msg( 'table_pager_limit' ) + ->rawParams( $this->getLimitSelect() )->escaped() . "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" . $this->getHiddenFields( array( 'limit' ) ); } @@ -1139,13 +1243,19 @@ abstract class TablePager extends IndexPager { * The current result row is available as $this->mCurrentRow, in case you * need more context. * + * @protected + * * @param $name String: the database field name * @param $value String: the value retrieved from the database */ abstract function formatValue( $name, $value ); /** - * The database field name used as a default sort order + * The database field name used as a default sort order. + * + * @protected + * + * @return string */ abstract function getDefaultSort(); diff --git a/includes/PathRouter.php b/includes/PathRouter.php index 3e298a58..2dbc7ec0 100644 --- a/includes/PathRouter.php +++ b/includes/PathRouter.php @@ -1,5 +1,26 @@ <?php /** + * Parser to extract query parameters out of REQUEST_URI paths. + * + * 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 + */ + +/** * PathRouter class. * This class can take patterns such as /wiki/$1 and use them to * parse query parameters out of REQUEST_URI paths. @@ -34,7 +55,7 @@ * the relevant contents * - The default behavior is equivalent to `array( 'title' => '$1' )`, * if you don't want the title parameter you can explicitly use `array( 'title' => false )` - * - You can specify a value that won't have replacements in it + * - You can specify a value that won't have replacements in it * using `'foo' => array( 'value' => 'bar' );` * * Options: @@ -52,10 +73,19 @@ class PathRouter { /** + * @var array + */ + private $patterns = array(); + + /** * Protected helper to do the actual bulk work of adding a single pattern. * This is in a separate method so that add() can handle the difference between * a single string $path and an array() $path that contains multiple path * patterns each with an associated $key to pass on. + * @param $path string + * @param $params array + * @param $options array + * @param $key null|string */ protected function doAdd( $path, $params, $options, $key = null ) { // Make sure all paths start with a / @@ -123,9 +153,9 @@ class PathRouter { /** * Add a new path pattern to the path router * - * @param $path The path pattern to add - * @param $params The params for this path pattern - * @param $options The options for this path pattern + * @param $path string|array The path pattern to add + * @param $params array The params for this path pattern + * @param $options array The options for this path pattern */ public function add( $path, $params = array(), $options = array() ) { if ( is_array( $path ) ) { @@ -140,6 +170,9 @@ class PathRouter { /** * Add a new path pattern to the path router with the strict option on * @see self::add + * @param $path string|array + * @param $params array + * @param $options array */ public function addStrict( $path, $params = array(), $options = array() ) { $options['strict'] = true; @@ -158,6 +191,10 @@ class PathRouter { array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns ); } + /** + * @param $pattern object + * @return float|int + */ protected static function makeWeight( $pattern ) { # Start with a weight of 0 $weight = 0; @@ -195,14 +232,14 @@ class PathRouter { /** * Parse a path and return the query matches for the path * - * @param $path The path to parse + * @param $path string The path to parse * @return Array The array of matches for the path */ public function parse( $path ) { // Make sure our patterns are sorted by weight so the most specific // matches are tested first $this->sortByWeight(); - + $matches = null; foreach ( $this->patterns as $pattern ) { @@ -219,6 +256,11 @@ class PathRouter { return is_null( $matches ) ? array() : $matches; } + /** + * @param $path string + * @param $pattern string + * @return array|null + */ protected static function extractTitle( $path, $pattern ) { // Convert the path pattern into a regexp we can match with $regexp = preg_quote( $pattern->path, '#' ); @@ -321,6 +363,8 @@ class PathRouterPatternReplacer { * We do this inside of a replacement callback because after replacement we can't tell the * difference between a $1 that was not replaced and a $1 that was part of * the content a $1 was replaced with. + * @param $value string + * @return string */ public function replace( $value ) { $this->error = false; @@ -331,6 +375,10 @@ class PathRouterPatternReplacer { return $value; } + /** + * @param $m array + * @return string + */ protected function callback( $m ) { if ( $m[1] == "key" ) { if ( is_null( $this->key ) ) { @@ -348,4 +396,4 @@ class PathRouterPatternReplacer { } } -}
\ No newline at end of file +} diff --git a/includes/PoolCounter.php b/includes/PoolCounter.php index 83ae0abe..452dbc54 100644 --- a/includes/PoolCounter.php +++ b/includes/PoolCounter.php @@ -1,4 +1,25 @@ <?php +/** + * Provides of semaphore semantics for restricting the number + * of workers that may be concurrently performing the same task. + * + * 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 + */ /** * When you have many workers (threads/servers) giving service, and a @@ -150,6 +171,7 @@ abstract class PoolCounterWork { /** * Do something with the error, like showing it to the user. + * @return bool */ function error( $status ) { return false; diff --git a/includes/Preferences.php b/includes/Preferences.php index ea1efa18..216ba48c 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -1,5 +1,26 @@ <?php /** + * Form to edit user perferences. + * + * 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 + */ + +/** * We're now using the HTMLForm object with some customisation to generate the * Preferences form. This object handles generic submission, CSRF protection, * layout and other logic in a reusable manner. We subclass it as a PreferencesForm @@ -24,7 +45,6 @@ * Once fields have been retrieved and validated, submission logic is handed * over to the tryUISubmit static method of this class. */ - class Preferences { static $defaultPreferences = null; static $saveFilters = array( @@ -84,9 +104,9 @@ class Preferences { // Already set, no problem continue; } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing - $field->validate( $prefFromUser, $user->mOptions ) === true ) { + $field->validate( $prefFromUser, $user->getOptions() ) === true ) { $info['default'] = $prefFromUser; - } elseif ( $field->validate( $globalDefault, $user->mOptions ) === true ) { + } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) { $info['default'] = $globalDefault; } else { throw new MWException( "Global default '$globalDefault' is invalid for field $name" ); @@ -252,7 +272,7 @@ class Preferences { } // Language - $languages = Language::getLanguageNames( false ); + $languages = Language::fetchLanguageNames( null, 'mw' ); if ( !array_key_exists( $wgLanguageCode, $languages ) ) { $languages[$wgLanguageCode] = $wgLanguageCode; } @@ -351,9 +371,13 @@ class Preferences { $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : ''; if ( $wgAuth->allowPropChange( 'emailaddress' ) ) { - $emailAddress .= $emailAddress == '' ? $link : " ($link)"; + $emailAddress .= $emailAddress == '' ? $link : ( + $context->msg( 'word-separator' )->plain() + . $context->msg( 'parentheses' )->rawParams( $link )->plain() + ); } + $defaultPreferences['emailaddress'] = array( 'type' => 'info', 'raw' => true, @@ -361,10 +385,12 @@ class Preferences { 'label-message' => 'youremail', 'section' => 'personal/email', 'help-messages' => $helpMessages, + # 'cssclass' chosen below ); $disableEmailPrefs = false; + $emailauthenticationclass = 'mw-email-not-authenticated'; if ( $wgEmailAuthentication ) { if ( $user->getEmail() ) { if ( $user->getEmailAuthenticationTimestamp() ) { @@ -379,6 +405,7 @@ class Preferences { $emailauthenticated = $context->msg( 'emailauthenticated', $time, $d, $t )->parse() . '<br />'; $disableEmailPrefs = false; + $emailauthenticationclass = 'mw-email-authenticated'; } else { $disableEmailPrefs = true; $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' . @@ -386,10 +413,12 @@ class Preferences { SpecialPage::getTitleFor( 'Confirmemail' ), $context->msg( 'emailconfirmlink' )->escaped() ) . '<br />'; + $emailauthenticationclass="mw-email-not-authenticated"; } } else { $disableEmailPrefs = true; $emailauthenticated = $context->msg( 'noemailprefs' )->escaped(); + $emailauthenticationclass = 'mw-email-none'; } $defaultPreferences['emailauthentication'] = array( @@ -398,9 +427,11 @@ class Preferences { 'section' => 'personal/email', 'label-message' => 'prefs-emailconfirm-label', 'default' => $emailauthenticated, + # Apply the same CSS class used on the input to the message: + 'cssclass' => $emailauthenticationclass, ); - } + $defaultPreferences['emailaddress']['cssclass'] = $emailauthenticationclass; if ( $wgEnableUserEmail && $user->isAllowed( 'sendemail' ) ) { $defaultPreferences['disablemail'] = array( @@ -639,11 +670,6 @@ class Preferences { ); if ( $wgAllowUserCssPrefs ) { - $defaultPreferences['highlightbroken'] = array( - 'type' => 'toggle', - 'section' => 'rendering/advancedrendering', - 'label' => $context->msg( 'tog-highlightbroken' )->text(), // Raw HTML - ); $defaultPreferences['showtoc'] = array( 'type' => 'toggle', 'section' => 'rendering/advancedrendering', @@ -913,6 +939,7 @@ class Preferences { if ( $wgEnableAPI ) { # Some random gibberish as a proposed default + // @todo Fixme: this should use CryptRand but we may not want to read urandom on every view $hash = sha1( mt_rand() . microtime( true ) ); $defaultPreferences['watchlisttoken'] = array( @@ -951,7 +978,7 @@ class Preferences { * @param $defaultPreferences Array */ static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) { - global $wgContLang, $wgEnableMWSuggest, $wgVectorUseSimpleSearch; + global $wgContLang, $wgVectorUseSimpleSearch; ## Search ##################################### $defaultPreferences['searchlimit'] = array( @@ -961,22 +988,21 @@ class Preferences { 'min' => 0, ); - if ( $wgEnableMWSuggest ) { - $defaultPreferences['disablesuggest'] = array( - 'type' => 'toggle', - 'label-message' => 'mwsuggest-disable', - 'section' => 'searchoptions/displaysearchoptions', - ); - } if ( $wgVectorUseSimpleSearch ) { $defaultPreferences['vector-simplesearch'] = array( 'type' => 'toggle', 'label-message' => 'vector-simplesearch-preference', - 'section' => 'searchoptions/displaysearchoptions' + 'section' => 'searchoptions/displaysearchoptions', ); } + $defaultPreferences['disablesuggest'] = array( + 'type' => 'toggle', + 'label-message' => 'mwsuggest-disable', + 'section' => 'searchoptions/displaysearchoptions', + ); + $defaultPreferences['searcheverything'] = array( 'type' => 'toggle', 'label-message' => 'searcheverything-enable', @@ -1428,39 +1454,21 @@ class Preferences { * Try to set a user's email address. * This does *not* try to validate the address. * Caller is responsible for checking $wgAuth. + * + * @deprecated in 1.20; use User::setEmailWithConfirmation() instead. * @param $user User * @param $newaddr string New email address * @return Array (true on success or Status on failure, info string) */ public static function trySetUserEmail( User $user, $newaddr ) { - global $wgEnableEmail, $wgEmailAuthentication; - $info = ''; // none + wfDeprecated( __METHOD__, '1.20' ); - if ( $wgEnableEmail ) { - $oldaddr = $user->getEmail(); - if ( ( $newaddr != '' ) && ( $newaddr != $oldaddr ) ) { - # The user has supplied a new email address on the login page - # new behaviour: set this new emailaddr from login-page into user database record - $user->setEmail( $newaddr ); - if ( $wgEmailAuthentication ) { - # Mail a temporary password to the dirty address. - # User can come back through the confirmation URL to re-enable email. - $type = $oldaddr != '' ? 'changed' : 'set'; - $result = $user->sendConfirmationMail( $type ); - if ( !$result->isGood() ) { - return array( $result, 'mailerror' ); - } - $info = 'eauth'; - } - } elseif ( $newaddr != $oldaddr ) { // if the address is the same, don't change it - $user->setEmail( $newaddr ); - } - if ( $oldaddr != $newaddr ) { - wfRunHooks( 'PrefsEmailAudit', array( $user, $oldaddr, $newaddr ) ); - } + $result = $user->setEmailWithConfirmation( $newaddr ); + if ( $result->isGood() ) { + return array( true, $result->value ); + } else { + return array( $result, 'mailerror' ); } - - return array( true, $info ); } /** @@ -1578,7 +1586,7 @@ class PreferencesForm extends HTMLForm { } /** - * Get the <legend> for a given section key. Normally this is the + * Get the "<legend>" for a given section key. Normally this is the * prefs-$key message but we'll allow extensions to override it. * @param $key string * @return string diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index 0efe1bdd..5d4b35c1 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -1,11 +1,31 @@ <?php /** - * PrefixSearch - Handles searching prefixes of titles and finding any page + * Prefix search of page names. + * + * 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 + */ + +/** + * Handles searching prefixes of titles and finding any page * names that match. Used largely by the OpenSearch implementation. * * @ingroup Search */ - class PrefixSearch { /** * Do a prefix search of titles and return a list of matching page names. diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index dbe06d49..ce0e36b0 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -74,17 +74,17 @@ class ProtectionForm { $this->disabledAttrib = $this->disabled ? array( 'disabled' => 'disabled' ) : array(); - + $this->loadData(); } - + /** * Loads the current state of protection into the object. */ function loadData() { global $wgRequest, $wgUser; global $wgRestrictionLevels; - + $this->mCascade = $this->mTitle->areRestrictionsCascading(); $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); @@ -94,7 +94,7 @@ class ProtectionForm { foreach( $this->mApplicableTypes as $action ) { // @todo FIXME: This form currently requires individual selections, // but the db allows multiples separated by commas. - + // Pull the actual restriction from the DB $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); @@ -151,7 +151,7 @@ class ProtectionForm { * Get the expiry time for a given action, by combining the relevant inputs. * * @param $action string - * + * * @return string 14-char timestamp or "infinity", or false if the input was invalid */ function getExpiry( $action ) { @@ -265,7 +265,7 @@ class ProtectionForm { $reasonstr = $this->mReasonSelection; if ( $reasonstr != 'other' && $this->mReason != '' ) { // Entry from drop down menu + additional comment - $reasonstr .= wfMsgForContent( 'colon-separator' ) . $this->mReason; + $reasonstr .= wfMessage( 'colon-separator' )->text() . $this->mReason; } elseif ( $reasonstr == 'other' ) { $reasonstr = $this->mReason; } @@ -318,10 +318,12 @@ class ProtectionForm { return false; } - if ( $wgRequest->getCheck( 'mwProtectWatch' ) && $wgUser->isLoggedIn() ) { - WatchAction::doWatch( $this->mTitle, $wgUser ); - } elseif ( $this->mTitle->userIsWatching() ) { - WatchAction::doUnwatch( $this->mTitle, $wgUser ); + if ( $wgUser->isLoggedIn() && $wgRequest->getCheck( 'mwProtectWatch' ) != $wgUser->isWatched( $this->mTitle ) ) { + if ( $wgRequest->getCheck( 'mwProtectWatch' ) ) { + WatchAction::doWatch( $this->mTitle, $wgUser ); + } else { + WatchAction::doUnwatch( $this->mTitle, $wgUser ); + } } return true; } @@ -334,8 +336,14 @@ class ProtectionForm { function buildForm() { global $wgUser, $wgLang, $wgOut; - $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonSelection' ); - $mProtectreason = Xml::label( wfMsg( 'protect-otherreason' ), 'mwProtect-reason' ); + $mProtectreasonother = Xml::label( + wfMessage( 'protectcomment' )->text(), + 'wpProtectReasonSelection' + ); + $mProtectreason = Xml::label( + wfMessage( 'protect-otherreason' )->text(), + 'mwProtect-reason' + ); $out = ''; if( !$this->disabled ) { @@ -346,7 +354,7 @@ class ProtectionForm { } $out .= Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'protect-legend' ) ) . + Xml::element( 'legend', null, wfMessage( 'protect-legend' )->text() ) . Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) . Xml::openElement( 'tbody' ); @@ -360,16 +368,22 @@ class ProtectionForm { "<tr><td>" . $this->buildSelector( $action, $selected ) . "</td></tr><tr><td>"; $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection', - wfMsgForContent( 'protect-dropdown' ), - wfMsgForContent( 'protect-otherreason-op' ), + wfMessage( 'protect-dropdown' )->inContentLanguage()->text(), + wfMessage( 'protect-otherreason-op' )->inContentLanguage()->text(), $this->mReasonSelection, 'mwProtect-reason', 4 ); - $scExpiryOptions = wfMsgForContent( 'protect-expiry-options' ); + $scExpiryOptions = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text(); $showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled); - $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpirySelection-$action" ); - $mProtectother = Xml::label( wfMsg( 'protect-othertime' ), "mwProtect-$action-expires" ); + $mProtectexpiry = Xml::label( + wfMessage( 'protectexpiry' )->text(), + "mwProtectExpirySelection-$action" + ); + $mProtectother = Xml::label( + wfMessage( 'protect-othertime' )->text(), + "mwProtect-$action-expires" + ); $expiryFormOptions = ''; if ( $this->mExistingExpiry[$action] && $this->mExistingExpiry[$action] != 'infinity' ) { @@ -378,13 +392,16 @@ class ProtectionForm { $t = $wgLang->time( $this->mExistingExpiry[$action], true ); $expiryFormOptions .= Xml::option( - wfMsg( 'protect-existing-expiry', $timestamp, $d, $t ), + wfMessage( 'protect-existing-expiry', $timestamp, $d, $t )->text(), 'existing', $this->mExpirySelection[$action] == 'existing' ) . "\n"; } - $expiryFormOptions .= Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" ) . "\n"; + $expiryFormOptions .= Xml::option( + wfMessage( 'protect-othertime-op' )->text(), + "othertime" + ) . "\n"; foreach( explode(',', $scExpiryOptions) as $option ) { if ( strpos($option, ":") === false ) { $show = $value = $option; @@ -442,8 +459,12 @@ class ProtectionForm { $out .= '<tr> <td></td> <td class="mw-input">' . - Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade', - $this->mCascade, $this->disabledAttrib ) . + Xml::checkLabel( + wfMessage( 'protect-cascade' )->text(), + 'mwProtect-cascade', + 'mwProtect-cascade', + $this->mCascade, $this->disabledAttrib + ) . "</td> </tr>\n"; $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); @@ -480,7 +501,7 @@ class ProtectionForm { <tr> <td></td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'watchthis' ), + Xml::checkLabel( wfMessage( 'watchthis' )->text(), 'mwProtectWatch', 'mwProtectWatch', $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) ) . "</td> @@ -490,7 +511,10 @@ class ProtectionForm { <tr> <td></td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'confirm' ), array( 'id' => 'mw-Protect-submit' ) ) . + Xml::submitButton( + wfMessage( 'confirm' )->text(), + array( 'id' => 'mw-Protect-submit' ) + ) . "</td> </tr>\n"; $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); @@ -501,7 +525,7 @@ class ProtectionForm { $title = Title::makeTitle( NS_MEDIAWIKI, 'Protect-dropdown' ); $link = Linker::link( $title, - wfMsgHtml( 'protect-edit-reasonlist' ), + wfMessage( 'protect-edit-reasonlist' )->escaped(), array(), array( 'action' => 'edit' ) ); @@ -565,23 +589,23 @@ class ProtectionForm { */ private function getOptionLabel( $permission ) { if( $permission == '' ) { - return wfMsg( 'protect-default' ); + return wfMessage( 'protect-default' )->text(); } else { $msg = wfMessage( "protect-level-{$permission}" ); if( $msg->exists() ) { return $msg->text(); } - return wfMsg( 'protect-fallback', $permission ); + return wfMessage( 'protect-fallback', $permission )->text(); } } - + function buildCleanupScript() { global $wgRestrictionLevels, $wgGroupPermissions, $wgOut; $cascadeableLevels = array(); foreach( $wgRestrictionLevels as $key ) { if ( ( isset( $wgGroupPermissions[$key]['protect'] ) && $wgGroupPermissions[$key]['protect'] ) - || $key == 'protect' + || $key == 'protect' ) { $cascadeableLevels[] = $key; } @@ -606,7 +630,8 @@ class ProtectionForm { */ function showLogExtract( &$out ) { # Show relevant lines from the protection log: - $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'protect' ) ) ); + $protectLogPage = new LogPage( 'protect' ); + $out->addHTML( Xml::element( 'h2', null, $protectLogPage->getName()->text() ) ); LogEventsList::showLogExtract( $out, 'protect', $this->mTitle ); # Let extensions add other relevant log extracts wfRunHooks( 'ProtectionForm::showLogExtract', array($this->mArticle,$out) ); diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php index bdab3be2..349789fe 100644 --- a/includes/ProxyTools.php +++ b/includes/ProxyTools.php @@ -1,6 +1,21 @@ <?php /** - * Functions for dealing with proxies + * Functions for dealing with proxies. + * + * 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 */ @@ -53,11 +68,20 @@ function wfGetIP() { * @return bool */ function wfIsTrustedProxy( $ip ) { - global $wgSquidServers, $wgSquidServersNoPurge; + $trusted = wfIsConfiguredProxy( $ip ); + wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) ); + return $trusted; +} +/** + * Checks if an IP matches a proxy we've configured. + * @param $ip String + * @return bool + */ +function wfIsConfiguredProxy( $ip ) { + global $wgSquidServers, $wgSquidServersNoPurge; $trusted = in_array( $ip, $wgSquidServers ) || in_array( $ip, $wgSquidServersNoPurge ); - wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) ); return $trusted; } diff --git a/includes/QueryPage.php b/includes/QueryPage.php index 69912cbf..ac559dc5 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -1,8 +1,24 @@ <?php /** - * Contain a class for special pages + * Base code for "query" special pages. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file - * @ingroup SpecialPages + * @ingroup SpecialPage */ /** @@ -29,6 +45,7 @@ $wgQueryPages = array( array( 'MIMEsearchPage', 'MIMEsearch' ), array( 'MostcategoriesPage', 'Mostcategories' ), array( 'MostimagesPage', 'Mostimages' ), + array( 'MostinterwikisPage', 'Mostinterwikis' ), array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ), array( 'MostlinkedtemplatesPage', 'Mostlinkedtemplates' ), array( 'MostlinkedPage', 'Mostlinked' ), @@ -259,6 +276,7 @@ abstract class QueryPage extends SpecialPage { * Setting this to return true will ensure formatResult() is called * one more time to make sure that the very last result is formatted * as well. + * @return bool */ function tryLastResult() { return false; @@ -269,6 +287,7 @@ abstract class QueryPage extends SpecialPage { * * @param $limit Integer: limit for SQL statement * @param $ignoreErrors Boolean: whether to ignore database errors + * @return bool|int */ function recache( $limit, $ignoreErrors = true ) { if ( !$this->isCacheable() ) { @@ -293,7 +312,7 @@ abstract class QueryPage extends SpecialPage { $res = $this->reallyDoQuery( $limit, false ); $num = false; if ( $res ) { - $num = $dbr->numRows( $res ); + $num = $res->numRows(); # Fetch results $vals = array(); while ( $res && $row = $dbr->fetchObject( $res ) ) { @@ -358,7 +377,7 @@ abstract class QueryPage extends SpecialPage { $options = isset( $query['options'] ) ? (array)$query['options'] : array(); $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array(); if ( count( $order ) ) { - $options['ORDER BY'] = implode( ', ', $order ); + $options['ORDER BY'] = $order; } if ( $limit !== false ) { $options['LIMIT'] = intval( $limit ); @@ -382,6 +401,7 @@ abstract class QueryPage extends SpecialPage { /** * Somewhat deprecated, you probably want to be using execute() + * @return ResultWrapper */ function doQuery( $offset = false, $limit = false ) { if ( $this->isCached() && $this->isCacheable() ) { @@ -413,9 +433,9 @@ abstract class QueryPage extends SpecialPage { $options['ORDER BY'] = 'qc_value ASC'; } $res = $dbr->select( 'querycache', array( 'qc_type', - 'qc_namespace AS namespace', - 'qc_title AS title', - 'qc_value AS value' ), + 'namespace' => 'qc_namespace', + 'title' => 'qc_title', + 'value' => 'qc_value' ), array( 'qc_type' => $this->getName() ), __METHOD__, $options ); @@ -435,6 +455,7 @@ abstract class QueryPage extends SpecialPage { /** * This is the actual workhorse. It does everything needed to make a * real, honest-to-gosh query page. + * @return int */ function execute( $par ) { global $wgQueryCacheLimit, $wgDisableQueryPageUpdate; @@ -463,10 +484,11 @@ abstract class QueryPage extends SpecialPage { // TODO: Use doQuery() if ( !$this->isCached() ) { - $res = $this->reallyDoQuery( $this->limit, $this->offset ); + # select one extra row for navigation + $res = $this->reallyDoQuery( $this->limit + 1, $this->offset ); } else { - # Get the cached result - $res = $this->fetchFromCache( $this->limit, $this->offset ); + # Get the cached result, select one extra row for navigation + $res = $this->fetchFromCache( $this->limit + 1, $this->offset ); if ( !$this->listoutput ) { # Fetch the timestamp of this update @@ -479,7 +501,7 @@ abstract class QueryPage extends SpecialPage { $updateddate = $lang->userDate( $ts, $user ); $updatedtime = $lang->userTime( $ts, $user ); $out->addMeta( 'Data-Cache-Time', $ts ); - $out->addInlineScript( "var dataCacheTime = '$ts';" ); + $out->addJsConfigVars( 'dataCacheTime', $ts ); $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults ); } else { $out->addWikiMsg( 'perfcached', $maxResults ); @@ -488,7 +510,7 @@ abstract class QueryPage extends SpecialPage { # If updates on this page have been disabled, let the user know # that the data set won't be refreshed for now if ( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) { - $out->addWikiMsg( 'querypage-no-updates' ); + $out->wrapWikiMsg( "<div class=\"mw-querypage-no-updates\">\n$1\n</div>", 'querypage-no-updates' ); } } } @@ -505,10 +527,11 @@ abstract class QueryPage extends SpecialPage { $out->addHTML( $this->getPageHeader() ); if ( $this->numRows > 0 ) { $out->addHTML( $this->msg( 'showingresults' )->numParams( - $this->numRows, $this->offset + 1 )->parseAsBlock() ); + min( $this->numRows, $this->limit ), # do not show the one extra row, if exist + $this->offset + 1 )->parseAsBlock() ); # Disable the "next" link when we reach the end $paging = $this->getLanguage()->viewPrevNext( $this->getTitle( $par ), $this->offset, - $this->limit, $this->linkParameters(), ( $this->numRows < $this->limit ) ); + $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) ); $out->addHTML( '<p>' . $paging . '</p>' ); } else { # No results to show, so don't bother with "showing X of Y" etc. @@ -526,7 +549,7 @@ abstract class QueryPage extends SpecialPage { $this->getSkin(), $dbr, # Should use a ResultWrapper for this $res, - $this->numRows, + min( $this->numRows, $this->limit ), # do not format the one extra row, if exist $this->offset ); # Repeat the paging links at the bottom @@ -536,7 +559,7 @@ abstract class QueryPage extends SpecialPage { $out->addHTML( Xml::closeElement( 'div' ) ); - return $this->numRows; + return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist } /** @@ -621,6 +644,7 @@ abstract class QueryPage extends SpecialPage { /** * Similar to above, but packaging in a syndicated feed instead of a web page + * @return bool */ function doFeed( $class = '', $limit = 50 ) { global $wgFeed, $wgFeedClasses; @@ -660,12 +684,13 @@ abstract class QueryPage extends SpecialPage { /** * Override for custom handling. If the titles/links are ok, just do * feedItemDesc() + * @return FeedItem|null */ function feedResult( $row ) { if ( !isset( $row->title ) ) { return null; } - $title = Title::MakeTitle( intval( $row->namespace ), $row->title ); + $title = Title::makeTitle( intval( $row->namespace ), $row->title ); if ( $title ) { $date = isset( $row->timestamp ) ? $row->timestamp : ''; $comments = ''; @@ -727,6 +752,10 @@ abstract class WantedQueryPage extends QueryPage { * Cache page existence for performance */ function preprocessResults( $db, $res ) { + if ( !$res->numRows() ) { + return; + } + $batch = new LinkBatch; foreach ( $res as $row ) { $batch->add( $row->namespace, $row->title ); @@ -734,9 +763,7 @@ abstract class WantedQueryPage extends QueryPage { $batch->execute(); // Back to start for display - if ( $db->numRows( $res ) > 0 ) - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); + $res->seek( 0 ); } /** @@ -745,6 +772,7 @@ abstract class WantedQueryPage extends QueryPage { * kluge for Special:WantedFiles, which may contain false * positives for files that exist e.g. in a shared repo (bug * 6220). + * @return bool */ function forceExistenceCheck() { return false; diff --git a/includes/RecentChange.php b/includes/RecentChange.php index ca0ed955..332d0390 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -1,4 +1,24 @@ <?php +/** + * Utility class for creating and accessing recent change entries. + * + * 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 + */ /** * Utility class for creating new RC entries @@ -51,6 +71,11 @@ class RecentChange { var $mTitle = false; /** + * @var User + */ + private $mPerformer = false; + + /** * @var Title */ var $mMovedToTitle = false; @@ -88,14 +113,7 @@ class RecentChange { * @return RecentChange */ public static function newFromId( $rcid ) { - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'recentchanges', '*', array( 'rc_id' => $rcid ), __METHOD__ ); - if( $res && $dbr->numRows( $res ) > 0 ) { - $row = $dbr->fetchObject( $res ); - return self::newFromRow( $row ); - } else { - return null; - } + return self::newFromConds( array( 'rc_id' => $rcid ), __METHOD__ ); } /** @@ -107,18 +125,12 @@ class RecentChange { */ public static function newFromConds( $conds, $fname = __METHOD__ ) { $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( - 'recentchanges', - '*', - $conds, - $fname - ); - if( $res instanceof ResultWrapper && $res->numRows() > 0 ) { - $row = $res->fetchObject(); - $res->free(); + $row = $dbr->selectRow( 'recentchanges', '*', $conds, $fname ); + if ( $row !== false ) { return self::newFromRow( $row ); + } else { + return null; } - return null; } # Accessors @@ -151,7 +163,7 @@ class RecentChange { } /** - * @return bool|\Title + * @return bool|Title */ public function getMovedToTitle() { if( $this->mMovedToTitle === false ) { @@ -162,11 +174,27 @@ class RecentChange { } /** + * Get the User object of the person who performed this change. + * + * @return User + */ + public function getPerformer() { + if ( $this->mPerformer === false ) { + if ( $this->mAttribs['rc_user'] ) { + $this->mPerformer = User::newFromID( $this->mAttribs['rc_user'] ); + } else { + $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false ); + } + } + return $this->mPerformer; + } + + /** * Writes the data in this object to the database * @param $noudp bool */ public function save( $noudp = false ) { - global $wgLocalInterwiki, $wgPutIPinRC, $wgContLang; + global $wgLocalInterwiki, $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang; $dbw = wfGetDB( DB_MASTER ); if( !is_array($this->mExtra) ) { @@ -211,26 +239,19 @@ class RecentChange { } # E-mail notifications - global $wgUseEnotif, $wgShowUpdatedMarker, $wgUser; if( $wgUseEnotif || $wgShowUpdatedMarker ) { - // Users - if( $this->mAttribs['rc_user'] ) { - $editor = ($wgUser->getId() == $this->mAttribs['rc_user']) ? - $wgUser : User::newFromID( $this->mAttribs['rc_user'] ); - // Anons - } else { - $editor = ($wgUser->getName() == $this->mAttribs['rc_user_text']) ? - $wgUser : User::newFromName( $this->mAttribs['rc_user_text'], false ); + $editor = $this->getPerformer(); + $title = $this->getTitle(); + + if ( wfRunHooks( 'AbortEmailNotification', array($editor, $title) ) ) { + # @todo FIXME: This would be better as an extension hook + $enotif = new EmailNotification(); + $enotif->notifyOnPageChange( $editor, $title, + $this->mAttribs['rc_timestamp'], + $this->mAttribs['rc_comment'], + $this->mAttribs['rc_minor'], + $this->mAttribs['rc_last_oldid'] ); } - $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); - - # @todo FIXME: This would be better as an extension hook - $enotif = new EmailNotification(); - $status = $enotif->notifyOnPageChange( $editor, $title, - $this->mAttribs['rc_timestamp'], - $this->mAttribs['rc_comment'], - $this->mAttribs['rc_minor'], - $this->mAttribs['rc_last_oldid'] ); } } @@ -339,7 +360,7 @@ class RecentChange { // Actually set the 'patrolled' flag in RC $this->reallyMarkPatrolled(); // Log this patrol event - PatrolLog::record( $this, $auto ); + PatrolLog::record( $this, $auto, $user ); wfRunHooks( 'MarkPatrolledComplete', array($this->getAttribute('rc_id'), &$user, false) ); return array(); } @@ -383,13 +404,9 @@ class RecentChange { */ public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0 ) { - global $wgRequest; - if( !$ip ) { - $ip = $wgRequest->getIP(); - if( !$ip ) $ip = ''; - } - $rc = new RecentChange; + $rc->mTitle = $title; + $rc->mPerformer = $user; $rc->mAttribs = array( 'rc_timestamp' => $timestamp, 'rc_cur_time' => $timestamp, @@ -406,7 +423,7 @@ class RecentChange { 'rc_bot' => $bot ? 1 : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', - 'rc_ip' => $ip, + 'rc_ip' => self::checkIPAddress( $ip ), 'rc_patrolled' => intval($patrol), 'rc_new' => 0, # obsolete 'rc_old_len' => $oldSize, @@ -447,15 +464,9 @@ class RecentChange { */ public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot, $ip='', $size=0, $newId=0, $patrol=0 ) { - global $wgRequest; - if( !$ip ) { - $ip = $wgRequest->getIP(); - if( !$ip ) { - $ip = ''; - } - } - $rc = new RecentChange; + $rc->mTitle = $title; + $rc->mPerformer = $user; $rc->mAttribs = array( 'rc_timestamp' => $timestamp, 'rc_cur_time' => $timestamp, @@ -472,7 +483,7 @@ class RecentChange { 'rc_bot' => $bot ? 1 : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', - 'rc_ip' => $ip, + 'rc_ip' => self::checkIPAddress( $ip ), 'rc_patrolled' => intval($patrol), 'rc_new' => 1, # obsolete 'rc_old_len' => 0, @@ -506,10 +517,11 @@ class RecentChange { * @param $logComment * @param $params * @param $newId int + * @param $actionCommentIRC string * @return bool */ - public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='', $type, - $action, $target, $logComment, $params, $newId=0 ) + public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type, + $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='' ) { global $wgLogRestrictions; # Don't add private logs to RC! @@ -517,7 +529,7 @@ class RecentChange { return false; } $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action, - $target, $logComment, $params, $newId ); + $target, $logComment, $params, $newId, $actionCommentIRC ); $rc->save(); return true; } @@ -534,19 +546,16 @@ class RecentChange { * @param $logComment * @param $params * @param $newId int + * @param $actionCommentIRC string * @return RecentChange */ - public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip='', - $type, $action, $target, $logComment, $params, $newId=0 ) { + public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip, + $type, $action, $target, $logComment, $params, $newId=0, $actionCommentIRC='' ) { global $wgRequest; - if( !$ip ) { - $ip = $wgRequest->getIP(); - if( !$ip ) { - $ip = ''; - } - } $rc = new RecentChange; + $rc->mTitle = $target; + $rc->mPerformer = $user; $rc->mAttribs = array( 'rc_timestamp' => $timestamp, 'rc_cur_time' => $timestamp, @@ -563,7 +572,7 @@ class RecentChange { 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', - 'rc_ip' => $ip, + 'rc_ip' => self::checkIPAddress( $ip ), 'rc_patrolled' => 1, 'rc_new' => 0, # obsolete 'rc_old_len' => null, @@ -574,10 +583,12 @@ class RecentChange { 'rc_log_action' => $action, 'rc_params' => $params ); + $rc->mExtra = array( 'prefixedDBkey' => $title->getPrefixedDBkey(), 'lastTimestamp' => 0, 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage + 'actionCommentIRC' => $actionCommentIRC ); return $rc; } @@ -675,6 +686,8 @@ class RecentChange { $wgCanonicalServer, $wgScript; if( $this->mAttribs['rc_type'] == RC_LOG ) { + // Don't use SpecialPage::getTitleFor, backwards compatibility with + // IRC API which expects "Log". $titleObj = Title::newFromText( 'Log/' . $this->mAttribs['rc_log_type'], NS_SPECIAL ); } else { $titleObj =& $this->getTitle(); @@ -706,6 +719,7 @@ class RecentChange { } elseif($szdiff >= 0) { $szdiff = '+' . $szdiff ; } + // @todo i18n with parentheses in content language? $szdiff = '(' . $szdiff . ')' ; } else { $szdiff = ''; @@ -715,15 +729,15 @@ class RecentChange { if ( $this->mAttribs['rc_type'] == RC_LOG ) { $targetText = $this->getTitle()->getPrefixedText(); - $comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $this->mExtra['actionComment'] ) ); + $comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $this->mExtra['actionCommentIRC'] ) ); $flag = $this->mAttribs['rc_log_action']; } else { $comment = self::cleanupForIRC( $this->mAttribs['rc_comment'] ); $flag = ''; - if ( !$this->mAttribs['rc_patrolled'] && ( $wgUseRCPatrol || $this->mAttribs['rc_new'] && $wgUseNPPatrol ) ) { + if ( !$this->mAttribs['rc_patrolled'] && ( $wgUseRCPatrol || $this->mAttribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) { $flag .= '!'; } - $flag .= ( $this->mAttribs['rc_new'] ? "N" : "" ) . ( $this->mAttribs['rc_minor'] ? "M" : "" ) . ( $this->mAttribs['rc_bot'] ? "B" : "" ); + $flag .= ( $this->mAttribs['rc_type'] == RC_NEW ? "N" : "" ) . ( $this->mAttribs['rc_minor'] ? "M" : "" ) . ( $this->mAttribs['rc_bot'] ? "B" : "" ); } if ( $wgRC2UDPInterwikiPrefix === true && $wgLocalInterwiki !== false ) { @@ -766,4 +780,18 @@ class RecentChange { } return ChangesList::showCharacterDifference( $old, $new ); } + + private static function checkIPAddress( $ip ) { + global $wgRequest; + if ( $ip ) { + if ( !IP::isIPAddress( $ip ) ) { + throw new MWException( "Attempt to write \"" . $ip . "\" as an IP address into recent changes" ); + } + } else { + $ip = $wgRequest->getIP(); + if( !$ip ) + $ip = ''; + } + return $ip; + } } diff --git a/includes/Revision.php b/includes/Revision.php index 445211c9..20cc8f58 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -1,9 +1,29 @@ <?php +/** + * Representation of a page version. + * + * 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 + */ /** * @todo document */ -class Revision { +class Revision implements IDBAccessObject { protected $mId; protected $mPage; protected $mUserText; @@ -21,13 +41,14 @@ class Revision { protected $mTitle; protected $mCurrent; + // Revision deletion constants const DELETED_TEXT = 1; const DELETED_COMMENT = 2; const DELETED_USER = 4; const DELETED_RESTRICTED = 8; - // Convenience field - const SUPPRESSED_USER = 12; - // Audience options for Revision::getText() + const SUPPRESSED_USER = 12; // convenience + + // Audience options for accessors const FOR_PUBLIC = 1; const FOR_THIS_USER = 2; const RAW = 3; @@ -36,11 +57,16 @@ class Revision { * Load a page revision from a given revision ID number. * Returns null if no such revision can be found. * + * $flags include: + * Revision::READ_LATEST : Select the data from the master + * Revision::READ_LOCKING : Select & lock the data from the master + * * @param $id Integer + * @param $flags Integer (optional) * @return Revision or null */ - public static function newFromId( $id ) { - return Revision::newFromConds( array( 'rev_id' => intval( $id ) ) ); + public static function newFromId( $id, $flags = 0 ) { + return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags ); } /** @@ -48,11 +74,16 @@ class Revision { * that's attached to a given title. If not attached * to that title, will return null. * + * $flags include: + * Revision::READ_LATEST : Select the data from the master + * Revision::READ_LOCKING : Select & lock the data from the master + * * @param $title Title * @param $id Integer (optional) + * @param $flags Integer Bitfield (optional) * @return Revision or null */ - public static function newFromTitle( $title, $id = 0 ) { + public static function newFromTitle( $title, $id = 0, $flags = null ) { $conds = array( 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() @@ -60,19 +91,13 @@ class Revision { if ( $id ) { // Use the specified ID $conds['rev_id'] = $id; - } elseif ( wfGetLB()->getServerCount() > 1 ) { - // Get the latest revision ID from the master - $dbw = wfGetDB( DB_MASTER ); - $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); - if ( $latest === false ) { - return null; // page does not exist - } - $conds['rev_id'] = $latest; } else { // Use a join to get the latest revision $conds[] = 'rev_id=page_latest'; + // Callers assume this will be up-to-date + $flags = is_int( $flags ) ? $flags : self::READ_LATEST; // b/c } - return Revision::newFromConds( $conds ); + return self::newFromConds( $conds, (int)$flags ); } /** @@ -80,26 +105,26 @@ class Revision { * that's attached to a given page ID. * Returns null if no such revision can be found. * + * $flags include: + * Revision::READ_LATEST : Select the data from the master + * Revision::READ_LOCKING : Select & lock the data from the master + * * @param $revId Integer * @param $pageId Integer (optional) + * @param $flags Integer Bitfield (optional) * @return Revision or null */ - public static function newFromPageId( $pageId, $revId = 0 ) { + public static function newFromPageId( $pageId, $revId = 0, $flags = null ) { $conds = array( 'page_id' => $pageId ); if ( $revId ) { $conds['rev_id'] = $revId; - } elseif ( wfGetLB()->getServerCount() > 1 ) { - // Get the latest revision ID from the master - $dbw = wfGetDB( DB_MASTER ); - $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); - if ( $latest === false ) { - return null; // page does not exist - } - $conds['rev_id'] = $latest; } else { + // Use a join to get the latest revision $conds[] = 'rev_id = page_latest'; + // Callers assume this will be up-to-date + $flags = is_int( $flags ) ? $flags : self::READ_LATEST; // b/c } - return Revision::newFromConds( $conds ); + return self::newFromConds( $conds, (int)$flags ); } /** @@ -155,7 +180,7 @@ class Revision { * @return Revision or null */ public static function loadFromId( $db, $id ) { - return Revision::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) ); + return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) ); } /** @@ -175,7 +200,7 @@ class Revision { } else { $conds[] = 'rev_id=page_latest'; } - return Revision::loadFromConds( $db, $conds ); + return self::loadFromConds( $db, $conds ); } /** @@ -194,7 +219,7 @@ class Revision { } else { $matchId = 'page_latest'; } - return Revision::loadFromConds( $db, + return self::loadFromConds( $db, array( "rev_id=$matchId", 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ) @@ -212,7 +237,7 @@ class Revision { * @return Revision or null */ public static function loadFromTimestamp( $db, $title, $timestamp ) { - return Revision::loadFromConds( $db, + return self::loadFromConds( $db, array( 'rev_timestamp' => $db->timestamp( $timestamp ), 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ) @@ -223,14 +248,17 @@ class Revision { * Given a set of conditions, fetch a revision. * * @param $conditions Array + * @param $flags integer (optional) * @return Revision or null */ - public static function newFromConds( $conditions ) { - $db = wfGetDB( DB_SLAVE ); - $rev = Revision::loadFromConds( $db, $conditions ); - if( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) { - $dbw = wfGetDB( DB_MASTER ); - $rev = Revision::loadFromConds( $dbw, $conditions ); + private static function newFromConds( $conditions, $flags = 0 ) { + $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE ); + $rev = self::loadFromConds( $db, $conditions, $flags ); + if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) { + if ( !( $flags & self::READ_LATEST ) ) { + $dbw = wfGetDB( DB_MASTER ); + $rev = self::loadFromConds( $dbw, $conditions, $flags ); + } } return $rev; } @@ -241,10 +269,11 @@ class Revision { * * @param $db DatabaseBase * @param $conditions Array + * @param $flags integer (optional) * @return Revision or null */ - private static function loadFromConds( $db, $conditions ) { - $res = Revision::fetchFromConds( $db, $conditions ); + private static function loadFromConds( $db, $conditions, $flags = 0 ) { + $res = self::fetchFromConds( $db, $conditions, $flags ); if( $res ) { $row = $res->fetchObject(); if( $row ) { @@ -265,7 +294,7 @@ class Revision { * @return ResultWrapper */ public static function fetchRevision( $title ) { - return Revision::fetchFromConds( + return self::fetchFromConds( wfGetDB( DB_SLAVE ), array( 'rev_id=page_latest', 'page_namespace' => $title->getNamespace(), @@ -280,20 +309,25 @@ class Revision { * * @param $db DatabaseBase * @param $conditions Array + * @param $flags integer (optional) * @return ResultWrapper */ - private static function fetchFromConds( $db, $conditions ) { + private static function fetchFromConds( $db, $conditions, $flags = 0 ) { $fields = array_merge( self::selectFields(), self::selectPageFields(), self::selectUserFields() ); + $options = array( 'LIMIT' => 1 ); + if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) { + $options[] = 'FOR UPDATE'; + } return $db->select( array( 'revision', 'page', 'user' ), $fields, $conditions, __METHOD__, - array( 'LIMIT' => 1 ), + $options, array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ) ); } @@ -321,6 +355,7 @@ class Revision { /** * Return the list of revision fields that should be selected to create * a new revision. + * @return array */ public static function selectFields() { return array( @@ -342,6 +377,7 @@ class Revision { /** * Return the list of text fields that should be selected to read the * revision text + * @return array */ public static function selectTextFields() { return array( @@ -352,24 +388,51 @@ class Revision { /** * Return the list of page fields that should be selected from page table + * @return array */ public static function selectPageFields() { return array( 'page_namespace', 'page_title', 'page_id', - 'page_latest' + 'page_latest', + 'page_is_redirect', + 'page_len', ); } /** * Return the list of user fields that should be selected from user table + * @return array */ public static function selectUserFields() { return array( 'user_name' ); } /** + * Do a batched query to get the parent revision lengths + * @param $db DatabaseBase + * @param $revIds Array + * @return array + */ + public static function getParentLengths( $db, array $revIds ) { + $revLens = array(); + if ( !$revIds ) { + return $revLens; // empty + } + wfProfileIn( __METHOD__ ); + $res = $db->select( 'revision', + array( 'rev_id', 'rev_len' ), + array( 'rev_id' => $revIds ), + __METHOD__ ); + foreach ( $res as $row ) { + $revLens[$row->rev_id] = $row->rev_len; + } + wfProfileOut( __METHOD__ ); + return $revLens; + } + + /** * Constructor * * @param $row Mixed: either a database row or an array @@ -469,7 +532,7 @@ class Revision { /** * Get revision ID * - * @return Integer + * @return Integer|null */ public function getId() { return $this->mId; @@ -488,7 +551,7 @@ class Revision { /** * Get text row ID * - * @return Integer + * @return Integer|null */ public function getTextId() { return $this->mTextId; @@ -497,7 +560,7 @@ class Revision { /** * Get parent revision ID (the original previous page revision) * - * @return Integer + * @return Integer|null */ public function getParentId() { return $this->mParentId; @@ -506,7 +569,7 @@ class Revision { /** * Returns the length of the text in this revision, or null if unknown. * - * @return Integer + * @return Integer|null */ public function getSize() { return $this->mSize; @@ -515,30 +578,34 @@ class Revision { /** * Returns the base36 sha1 of the text in this revision, or null if unknown. * - * @return String + * @return String|null */ public function getSha1() { return $this->mSha1; } /** - * Returns the title of the page associated with this entry. + * Returns the title of the page associated with this entry or null. + * + * Will do a query, when title is not set and id is given. * - * @return Title + * @return Title|null */ public function getTitle() { if( isset( $this->mTitle ) ) { return $this->mTitle; } - $dbr = wfGetDB( DB_SLAVE ); - $row = $dbr->selectRow( - array( 'page', 'revision' ), - self::selectPageFields(), - array( 'page_id=rev_page', - 'rev_id' => $this->mId ), - __METHOD__ ); - if ( $row ) { - $this->mTitle = Title::newFromRow( $row ); + if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL + $dbr = wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( + array( 'page', 'revision' ), + self::selectPageFields(), + array( 'page_id=rev_page', + 'rev_id' => $this->mId ), + __METHOD__ ); + if ( $row ) { + $this->mTitle = Title::newFromRow( $row ); + } } return $this->mTitle; } @@ -555,7 +622,7 @@ class Revision { /** * Get the page ID * - * @return Integer + * @return Integer|null */ public function getPage() { return $this->mPage; @@ -568,7 +635,7 @@ class Revision { * * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the ID regardless of permissions * @param $user User object to check for, only if FOR_THIS_USER is passed * to the $audience parameter @@ -600,7 +667,7 @@ class Revision { * * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions * @param $user User object to check for, only if FOR_THIS_USER is passed * to the $audience parameter @@ -640,7 +707,7 @@ class Revision { * * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions * @param $user User object to check for, only if FOR_THIS_USER is passed * to the $audience parameter @@ -718,7 +785,7 @@ class Revision { * * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users - * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions * @param $user User object to check for, only if FOR_THIS_USER is passed * to the $audience parameter @@ -781,7 +848,7 @@ class Revision { if( $this->getTitle() ) { $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() ); if( $prev ) { - return Revision::newFromTitle( $this->getTitle(), $prev ); + return self::newFromTitle( $this->getTitle(), $prev ); } } return null; @@ -796,7 +863,7 @@ class Revision { if( $this->getTitle() ) { $next = $this->getTitle()->getNextRevisionID( $this->getId() ); if ( $next ) { - return Revision::newFromTitle( $this->getTitle(), $next ); + return self::newFromTitle( $this->getTitle(), $next ); } } return null; @@ -926,7 +993,7 @@ class Revision { $text = gzdeflate( $text ); $flags[] = 'gzip'; } else { - wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" ); + wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" ); } } return implode( ',', $flags ); @@ -945,7 +1012,7 @@ class Revision { wfProfileIn( __METHOD__ ); $data = $this->mText; - $flags = Revision::compressRevisionText( $data ); + $flags = self::compressRevisionText( $data ); # Write to external storage if required if( $wgDefaultExternalStore ) { @@ -995,7 +1062,7 @@ class Revision { ? $this->getPreviousRevisionId( $dbw ) : $this->mParentId, 'rev_sha1' => is_null( $this->mSha1 ) - ? Revision::base36Sha1( $this->mText ) + ? self::base36Sha1( $this->mText ) : $this->mSha1 ), __METHOD__ ); @@ -1096,7 +1163,8 @@ class Revision { $current = $dbw->selectRow( array( 'page', 'revision' ), - array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' ), + array( 'page_latest', 'page_namespace', 'page_title', + 'rev_text_id', 'rev_len', 'rev_sha1' ), array( 'page_id' => $pageId, 'page_latest=rev_id', @@ -1113,6 +1181,7 @@ class Revision { 'len' => $current->rev_len, 'sha1' => $current->rev_sha1 ) ); + $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) ); } else { $revision = null; } @@ -1181,7 +1250,7 @@ class Revision { $id = 0; } $conds = array( 'rev_id' => $id ); - $conds['rev_page'] = $title->getArticleId(); + $conds['rev_page'] = $title->getArticleID(); $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) { # Not in slave, try master @@ -1199,7 +1268,7 @@ class Revision { * @return Integer */ static function countByPageId( $db, $id ) { - $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount', + $row = $db->selectRow( 'revision', array( 'revCount' => 'COUNT(*)' ), array( 'rev_page' => $id ), __METHOD__ ); if( $row ) { return $row->revCount; @@ -1215,10 +1284,48 @@ class Revision { * @return Integer */ static function countByTitle( $db, $title ) { - $id = $title->getArticleId(); + $id = $title->getArticleID(); if( $id ) { - return Revision::countByPageId( $db, $id ); + return self::countByPageId( $db, $id ); } return 0; } -} + + /** + * Check if no edits were made by other users since + * the time a user started editing the page. Limit to + * 50 revisions for the sake of performance. + * + * @since 1.20 + * + * @param DatabaseBase|int $db the Database to perform the check on. May be given as a Database object or + * a database identifier usable with wfGetDB. + * @param int $pageId the ID of the page in question + * @param int $userId the ID of the user in question + * @param string $since look at edits since this time + * + * @return bool True if the given user was the only one to edit since the given timestamp + */ + public static function userWasLastToEdit( $db, $pageId, $userId, $since ) { + if ( !$userId ) return false; + + if ( is_int( $db ) ) { + $db = wfGetDB( $db ); + } + + $res = $db->select( 'revision', + 'rev_user', + array( + 'rev_page' => $pageId, + 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) ) + ), + __METHOD__, + array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) ); + foreach ( $res as $row ) { + if ( $row->rev_user != $userId ) { + return false; + } + } + return true; + } +}
\ No newline at end of file diff --git a/includes/RevisionList.php b/includes/RevisionList.php index 814e2dfa..3c5cfa8e 100644 --- a/includes/RevisionList.php +++ b/includes/RevisionList.php @@ -1,5 +1,26 @@ <?php /** + * Holders of revision list for a single page + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** * List for revision table items for a single page */ abstract class RevisionListBase extends ContextSource { @@ -31,6 +52,7 @@ abstract class RevisionListBase extends ContextSource { /** * Get the internal type name of this list. Equal to the table name. * Override this function. + * @return null */ public function getType() { return null; @@ -80,6 +102,7 @@ abstract class RevisionListBase extends ContextSource { /** * Get the number of items in the list. + * @return int */ public function length() { if( !$this->res ) { @@ -124,6 +147,7 @@ abstract class RevisionItemBase { /** * Get the DB field name associated with the ID list. * Override this function. + * @return null */ public function getIdField() { return null; @@ -132,6 +156,7 @@ abstract class RevisionItemBase { /** * Get the DB field name storing timestamps. * Override this function. + * @return bool */ public function getTimestampField() { return false; @@ -140,6 +165,7 @@ abstract class RevisionItemBase { /** * Get the DB field name storing user ids. * Override this function. + * @return bool */ public function getAuthorIdField() { return false; @@ -148,6 +174,7 @@ abstract class RevisionItemBase { /** * Get the DB field name storing user names. * Override this function. + * @return bool */ public function getAuthorNameField() { return false; @@ -155,6 +182,7 @@ abstract class RevisionItemBase { /** * Get the ID, as it would appear in the ids URL parameter + * @return */ public function getId() { $field = $this->getIdField(); @@ -163,6 +191,7 @@ abstract class RevisionItemBase { /** * Get the date, formatted in user's languae + * @return String */ public function formatDate() { return $this->list->getLanguage()->userDate( $this->getTimestamp(), @@ -171,6 +200,7 @@ abstract class RevisionItemBase { /** * Get the time, formatted in user's languae + * @return String */ public function formatTime() { return $this->list->getLanguage()->userTime( $this->getTimestamp(), @@ -179,6 +209,7 @@ abstract class RevisionItemBase { /** * Get the timestamp in MW 14-char form + * @return Mixed */ public function getTimestamp() { $field = $this->getTimestampField(); @@ -187,6 +218,7 @@ abstract class RevisionItemBase { /** * Get the author user ID + * @return int */ public function getAuthorId() { $field = $this->getAuthorIdField(); @@ -195,6 +227,7 @@ abstract class RevisionItemBase { /** * Get the author user name + * @return string */ public function getAuthorName() { $field = $this->getAuthorNameField(); @@ -212,7 +245,7 @@ abstract class RevisionItemBase { abstract public function canViewContent(); /** - * Get the HTML of the list item. Should be include <li></li> tags. + * Get the HTML of the list item. Should be include "<li></li>" tags. * This is used to show the list in HTML form, by the special page. */ abstract public function getHTML(); @@ -258,7 +291,7 @@ class RevisionItem extends RevisionItemBase { public function __construct( $list, $row ) { parent::__construct( $list, $row ); $this->revision = new Revision( $row ); - $this->context = $list->context; + $this->context = $list->getContext(); } public function getIdField() { @@ -292,6 +325,7 @@ class RevisionItem extends RevisionItemBase { /** * Get the HTML link to the revision text. * Overridden by RevDel_ArchiveItem. + * @return string */ protected function getRevisionLink() { $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true ); @@ -312,15 +346,16 @@ class RevisionItem extends RevisionItemBase { /** * Get the HTML link to the diff. * Overridden by RevDel_ArchiveItem + * @return string */ protected function getDiffLink() { if ( $this->isDeleted() && !$this->canViewContent() ) { - return wfMsgHtml('diff'); + return $this->context->msg( 'diff' )->escaped(); } else { return Linker::link( $this->list->title, - wfMsgHtml('diff'), + $this->context->msg( 'diff' )->escaped(), array(), array( 'diff' => $this->revision->getId(), @@ -336,13 +371,14 @@ class RevisionItem extends RevisionItemBase { } public function getHTML() { - $difflink = $this->getDiffLink(); + $difflink = $this->context->msg( 'parentheses' ) + ->rawParams( $this->getDiffLink() )->escaped(); $revlink = $this->getRevisionLink(); $userlink = Linker::revUserLink( $this->revision ); $comment = Linker::revComment( $this->revision ); if ( $this->isDeleted() ) { $revlink = "<span class=\"history-deleted\">$revlink</span>"; } - return "<li>($difflink) $revlink $userlink $comment</li>"; + return "<li>$difflink $revlink $userlink $comment</li>"; } } diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 196abd9f..b443ce14 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -1,6 +1,6 @@ <?php /** - * XHTML sanitizer for MediaWiki + * XHTML sanitizer for %MediaWiki. * * Copyright © 2002-2005 Brion Vibber <brion@pobox.com> et al * http://www.mediawiki.org/ @@ -374,7 +374,7 @@ class Sanitizer { if ( !$staticInitialised ) { $htmlpairsStatic = array( # Tags that must be closed - 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', + 'b', 'bdi', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', 'strike', 'strong', 'tt', 'var', 'div', 'center', 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', @@ -613,102 +613,6 @@ class Sanitizer { } /** - * Take an array of attribute names and values and fix some deprecated values - * for the given element type. - * This does not validate properties, so you should ensure that you call - * validateTagAttributes AFTER this to ensure that the resulting style rule - * this may add is safe. - * - * - Converts most presentational attributes like align into inline css - * - * @param $attribs Array - * @param $element String - * @return Array - */ - static function fixDeprecatedAttributes( $attribs, $element ) { - global $wgHtml5, $wgCleanupPresentationalAttributes; - - // presentational attributes were removed from html5, we can leave them - // in when html5 is turned off - if ( !$wgHtml5 || !$wgCleanupPresentationalAttributes ) { - return $attribs; - } - - $table = array( 'table' ); - $cells = array( 'td', 'th' ); - $colls = array( 'col', 'colgroup' ); - $tblocks = array( 'tbody', 'tfoot', 'thead' ); - $h = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ); - - $presentationalAttribs = array( - 'align' => array( 'text-align', array_merge( array( 'caption', 'hr', 'div', 'p', 'tr' ), $table, $cells, $colls, $tblocks, $h ) ), - 'clear' => array( 'clear', array( 'br' ) ), - 'height' => array( 'height', $cells ), - 'nowrap' => array( 'white-space', $cells ), - 'size' => array( 'height', array( 'hr' ) ), - 'type' => array( 'list-style-type', array( 'li', 'ol', 'ul' ) ), - 'valign' => array( 'vertical-align', array_merge( $cells, $colls, $tblocks ) ), - 'width' => array( 'width', array_merge( array( 'hr', 'pre' ), $table, $cells, $colls ) ), - ); - - // Ensure that any upper case or mixed case attributes are converted to lowercase - foreach ( $attribs as $attribute => $value ) { - if ( $attribute !== strtolower( $attribute ) && array_key_exists( strtolower( $attribute ), $presentationalAttribs ) ) { - $attribs[strtolower( $attribute )] = $value; - unset( $attribs[$attribute] ); - } - } - - $style = ""; - foreach ( $presentationalAttribs as $attribute => $info ) { - list( $property, $elements ) = $info; - - // Skip if this attribute is not relevant to this element - if ( !in_array( $element, $elements ) ) { - continue; - } - - // Skip if the attribute is not used - if ( !array_key_exists( $attribute, $attribs ) ) { - continue; - } - - $value = $attribs[$attribute]; - - // For nowrap the value should be nowrap instead of whatever text is in the value - if ( $attribute === 'nowrap' ) { - $value = 'nowrap'; - } - - // clear="all" is clear: both; in css - if ( $attribute === 'clear' && strtolower( $value ) === 'all' ) { - $value = 'both'; - } - - // Size based properties should have px applied to them if they have no unit - if ( in_array( $attribute, array( 'height', 'width', 'size' ) ) ) { - if ( preg_match( '/^[\d.]+$/', $value ) ) { - $value = "{$value}px"; - } - } - - $style .= " $property: $value;"; - - unset( $attribs[$attribute] ); - } - - if ( $style ) { - // Prepend our style rules so that they can be overridden by user css - if ( isset($attribs['style']) ) { - $style .= " " . $attribs['style']; - } - $attribs['style'] = trim($style); - } - - return $attribs; - } - - /** * Take an array of attribute names and values and normalize or discard * illegal values for the given element type. * @@ -956,7 +860,6 @@ class Sanitizer { } $decoded = Sanitizer::decodeTagAttributes( $text ); - $decoded = Sanitizer::fixDeprecatedAttributes( $decoded, $element ); $stripped = Sanitizer::validateTagAttributes( $decoded, $element ); $attribs = array(); @@ -1016,7 +919,7 @@ class Sanitizer { # Stupid hack $encValue = preg_replace_callback( - '/(' . wfUrlProtocols() . ')/', + '/((?i)' . wfUrlProtocols() . ')/', array( 'Sanitizer', 'armorLinksCallback' ), $encValue ); return $encValue; @@ -1243,7 +1146,7 @@ class Sanitizer { * a. named char refs can only be < > & ", others are * numericized (this way we're well-formed even without a DTD) * b. any numeric char refs must be legal chars, not invalid or forbidden - * c. use &#x, not &#X + * c. use lower cased "&#x", not "&#X" * d. fix or reject non-valid attributes * * @param $text String @@ -1411,7 +1314,7 @@ class Sanitizer { /** * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD, * return the UTF-8 encoding of that character. Otherwise, returns - * pseudo-entity source (eg &foo;) + * pseudo-entity source (eg "&foo;") * * @param $name String * @return String @@ -1611,6 +1514,10 @@ class Sanitizer { # 'title' may not be 100% valid here; it's XHTML # http://www.w3.org/TR/REC-MathML/ 'math' => array( 'class', 'style', 'id', 'title' ), + + # HTML 5 section 4.6 + 'bdi' => $common, + ); return $whitelist; } diff --git a/includes/ScopedPHPTimeout.php b/includes/ScopedPHPTimeout.php new file mode 100644 index 00000000..d1493c30 --- /dev/null +++ b/includes/ScopedPHPTimeout.php @@ -0,0 +1,84 @@ +<?php +/** + * Expansion of the PHP execution time limit feature for a function call. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Class to expand PHP execution time for a function call. + * Use this when performing changes that should not be interrupted. + * + * On construction, set_time_limit() is called and set to $seconds. + * If the client aborts the connection, PHP will continue to run. + * When the object goes out of scope, the timer is restarted, with + * the original time limit minus the time the object existed. + */ +class ScopedPHPTimeout { + protected $startTime; // float; seconds + protected $oldTimeout; // integer; seconds + protected $oldIgnoreAbort; // boolean + + protected static $stackDepth = 0; // integer + protected static $totalCalls = 0; // integer + protected static $totalElapsed = 0; // float; seconds + + /* Prevent callers in infinite loops from running forever */ + const MAX_TOTAL_CALLS = 1000000; + const MAX_TOTAL_TIME = 300; // seconds + + /** + * @param $seconds integer + */ + public function __construct( $seconds ) { + if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0 + if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) { + trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." ); + } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) { + trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." ); + } elseif ( self::$stackDepth > 0 ) { // recursion guard + trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." ); + } else { + $this->oldIgnoreAbort = ignore_user_abort( true ); + $this->oldTimeout = ini_set( 'max_execution_time', $seconds ); + $this->startTime = microtime( true ); + ++self::$stackDepth; + ++self::$totalCalls; // proof against < 1us scopes + } + } + } + + /** + * Restore the original timeout. + * This does not account for the timer value on __construct(). + */ + public function __destruct() { + if ( $this->oldTimeout ) { + $elapsed = microtime( true ) - $this->startTime; + // Note: a limit of 0 is treated as "forever" + set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) ); + // If each scoped timeout is for less than one second, we end up + // restoring the original timeout without any decrease in value. + // Thus web scripts in an infinite loop can run forever unless we + // take some measures to prevent this. Track total time and calls. + self::$totalElapsed += $elapsed; + --self::$stackDepth; + ignore_user_abort( $this->oldIgnoreAbort ); + } + } +} diff --git a/includes/SeleniumWebSettings.php b/includes/SeleniumWebSettings.php index 34d829ca..7b98568d 100644 --- a/includes/SeleniumWebSettings.php +++ b/includes/SeleniumWebSettings.php @@ -1,9 +1,28 @@ <?php /** * Dynamically change configuration variables based on the test suite name and a cookie value. + * * For details on how to configure a wiki for a Selenium test, see: * http://www.mediawiki.org/wiki/SeleniumFramework#Test_Wiki_configuration + * + * 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 */ + if ( !defined( 'MEDIAWIKI' ) ) { die( 1 ); } diff --git a/includes/Setup.php b/includes/Setup.php index 3955937c..924c3c07 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -1,6 +1,21 @@ <?php /** - * Include most things that's need to customize the site + * Include most things that's need to customize the site. + * + * 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 */ @@ -49,7 +64,7 @@ if ( !empty($wgActionPaths) && !isset($wgActionPaths['view']) ) { if ( !empty($wgActionPaths) && !isset($wgActionPaths['view']) ) { # 'view' is assumed the default action path everywhere in the code - # but is rarely filled in $wgActionPaths + # but is rarely filled in $wgActionPaths $wgActionPaths['view'] = $wgArticlePath ; } @@ -62,9 +77,6 @@ if ( $wgLogo === false ) $wgLogo = "$wgStylePath/common/images/wiki.png"; if ( $wgUploadPath === false ) $wgUploadPath = "$wgScriptPath/images"; if ( $wgUploadDirectory === false ) $wgUploadDirectory = "$IP/images"; - -if ( $wgTmpDirectory === false ) $wgTmpDirectory = "{$wgUploadDirectory}/tmp"; - if ( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; if ( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; if ( $wgDeletedDirectory === false ) $wgDeletedDirectory = "{$wgUploadDirectory}/deleted"; @@ -320,9 +332,6 @@ if ( !$wgEnotifMinorEdits ) { foreach( $wgDisabledActions as $action ){ $wgActions[$action] = false; } -if( !$wgAllowPageInfo ){ - $wgActions['info'] = false; -} if ( !$wgHtml5Version && $wgHtml5 && $wgAllowRdfaAttributes ) { # see http://www.w3.org/TR/rdfa-in-html/#document-conformance @@ -353,12 +362,14 @@ if ( $wgNewUserLog ) { $wgLogTypes[] = 'newusers'; $wgLogNames['newusers'] = 'newuserlogpage'; $wgLogHeaders['newusers'] = 'newuserlogpagetext'; - # newusers, create, create2, autocreate - $wgLogActionsHandlers['newusers/*'] = 'NewUsersLogFormatter'; + $wgLogActionsHandlers['newusers/newusers'] = 'NewUsersLogFormatter'; + $wgLogActionsHandlers['newusers/create'] = 'NewUsersLogFormatter'; + $wgLogActionsHandlers['newusers/create2'] = 'NewUsersLogFormatter'; + $wgLogActionsHandlers['newusers/autocreate'] = 'NewUsersLogFormatter'; } if ( $wgCookieSecure === 'detect' ) { - $wgCookieSecure = ( substr( $wgServer, 0, 6 ) === 'https:' ); + $wgCookieSecure = ( WebRequest::detectProtocol() === 'https:' ); } // Disable MWDebug for command line mode, this prevents MWDebug from eating up @@ -381,16 +392,30 @@ if ( !defined( 'MW_COMPILED' ) ) { require_once( "$IP/includes/normal/UtfNormalUtil.php" ); require_once( "$IP/includes/GlobalFunctions.php" ); require_once( "$IP/includes/ProxyTools.php" ); - require_once( "$IP/includes/ImageFunctions.php" ); require_once( "$IP/includes/normal/UtfNormalDefines.php" ); wfProfileOut( $fname . '-includes' ); } -# Now that GlobalFunctions is loaded, set the default for $wgCanonicalServer +# Now that GlobalFunctions is loaded, set defaults that depend +# on it. +if ( $wgTmpDirectory === false ) { + $wgTmpDirectory = wfTempDir(); +} + if ( $wgCanonicalServer === false ) { $wgCanonicalServer = wfExpandUrl( $wgServer, PROTO_HTTP ); } +// Initialize $wgHTCPMulticastRouting from backwards-compatible settings +if ( !$wgHTCPMulticastRouting && $wgHTCPMulticastAddress ) { + $wgHTCPMulticastRouting = array( + '' => array( + 'host' => $wgHTCPMulticastAddress, + 'port' => $wgHTCPPort, + ) + ); +} + wfProfileIn( $fname . '-misc1' ); # Raise the memory limit if it's too low @@ -421,16 +446,16 @@ if ( $wgCommandLineMode ) { # Can't stub this one, it sets up $_GET and $_REQUEST in its constructor $wgRequest = new WebRequest; - $debug = "Start request\n\n{$_SERVER['REQUEST_METHOD']} {$wgRequest->getRequestURL()}"; + $debug = "\n\nStart request {$wgRequest->getMethod()} {$wgRequest->getRequestURL()}\n"; if ( $wgDebugPrintHttpHeaders ) { - $debug .= "\nHTTP HEADERS:\n"; + $debug .= "HTTP HEADERS:\n"; foreach ( $wgRequest->getAllHeaders() as $name => $value ) { $debug .= "$name: $value\n"; } } - wfDebug( "$debug\n" ); + wfDebug( $debug ); } wfProfileOut( $fname . '-misc1' ); @@ -439,6 +464,7 @@ wfProfileIn( $fname . '-memcached' ); $wgMemc = wfGetMainCache(); $messageMemc = wfGetMessageCacheStorage(); $parserMemc = wfGetParserCacheStorage(); +$wgLangConvMemc = wfGetLangConverterCacheStorage(); wfDebug( 'CACHES: ' . get_class( $wgMemc ) . '[main] ' . get_class( $messageMemc ) . '[message] ' . @@ -458,11 +484,9 @@ if ( !wfIniGetBool( 'session.auto_start' ) ) { if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) { if ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix . 'Token'] ) ) { - wfIncrStats( 'request_with_session' ); wfSetupSession(); $wgSessionStarted = true; } else { - wfIncrStats( 'request_without_session' ); $wgSessionStarted = false; } } @@ -479,7 +503,7 @@ $wgRequest->interpolateTitle(); $wgUser = RequestContext::getMain()->getUser(); # BackCompat /** - * @var Language + * @var $wgLang Language */ $wgLang = new StubUserLang; @@ -489,7 +513,7 @@ $wgLang = new StubUserLang; $wgOut = RequestContext::getMain()->getOutput(); # BackCompat /** - * @var Parser + * @var $wgParser Parser */ $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index 8a977fb3..6a861d8e 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -1,6 +1,118 @@ <?php /** - * This is a class used to hold configuration settings, particularly for multi-wiki sites. + * Configuration holder, particularly for multi-wiki sites. + * + * 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 + */ + +/** + * This is a class for holding configuration settings, particularly for + * multi-wiki sites. + * + * A basic synopsis: + * + * Consider a wikifarm having three sites: two production sites, one in English + * and one in German, and one testing site. You can assign them easy-to-remember + * identifiers - ISO 639 codes 'en' and 'de' for language wikis, and 'beta' for + * the testing wiki. + * + * You would thus initialize the site configuration by specifying the wiki + * identifiers: + * + * @code + * $conf = new SiteConfiguration; + * $conf->wikis = array( 'de', 'en', 'beta' ); + * @endcode + * + * When configuring the MediaWiki global settings (the $wg variables), + * the identifiers will be available to specify settings on a per wiki basis. + * + * @code + * $conf->settings = array( + * 'wgSomeSetting' => array( + * + * # production: + * 'de' => false, + * 'en' => false, + * + * # test: + * 'beta => true, + * ), + * ); + * @endcode + * + * With three wikis, that is easy to manage. But what about a farm with + * hundreds of wikis? Site configuration provides a special keyword named + * 'default' which is the value used when a wiki is not found. Hence + * the above code could be written: + * + * @code + * $conf->settings = array( + * 'wgSomeSetting' => array( + * + * 'default' => false, + * + * # Enable feature on test + * 'beta' => true, + * ), + * ); + * @endcode + * + * + * Since settings can contain arrays, site configuration provides a way + * to merge an array with the default. This is very useful to avoid + * repeating settings again and again while still maintaining specific changes + * on a per wiki basis. + * + * @code + * $conf->settings = array( + * 'wgMergeSetting' = array( + * # Value that will be shared among all wikis: + * 'default' => array( NS_USER => true ), + * + * # Leading '+' means merging the array of value with the defaults + * '+beta' => array( NS_HELP => true ), + * ), + * ); + * + * # Get configuration for the German site: + * $conf->get( 'wgMergeSetting', 'de' ); + * // --> array( NS_USER => true ); + * + * # Get configuration for the testing site: + * $conf->get( 'wgMergeSetting', 'beta' ); + * // --> array( NS_USER => true, NS_HELP => true ); + * @endcode + * + * Finally, to load all configuration settings, extract them in global context: + * + * @code + * # Name / identifier of the wiki as set in $conf->wikis + * $wikiID = 'beta'; + * $globals = $conf->getAll( $wikiID ); + * extract( $globals ); + * @endcode + * + * TODO: give examples for, + * suffixes: + * $conf->suffixes = array( 'wiki' ); + * localVHosts + * callbacks! */ class SiteConfiguration { @@ -26,6 +138,7 @@ class SiteConfiguration { /** * Optional callback to load full configuration data. + * @var string|array */ public $fullLoadCallback = null; @@ -43,6 +156,8 @@ class SiteConfiguration { * argument and the wiki in the second one. * if suffix and lang are passed they will be used for the return value of * self::siteFromDB() and self::$suffixes will be ignored + * + * @var string|array */ public $siteParamsCallback = null; @@ -77,7 +192,7 @@ class SiteConfiguration { if( array_key_exists( $wiki, $thisSetting ) ) { $retval = $thisSetting[$wiki]; break; - } elseif( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) { + } elseif ( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) { $retval = $thisSetting["+$wiki"]; } @@ -91,8 +206,9 @@ class SiteConfiguration { } break 2; } elseif( array_key_exists( "+$tag", $thisSetting ) && is_array($thisSetting["+$tag"]) ) { - if( !isset( $retval ) ) + if( !isset( $retval ) ) { $retval = array(); + } $retval = self::arrayMerge( $retval, $thisSetting["+$tag"] ); } } @@ -106,9 +222,10 @@ class SiteConfiguration { $retval = $thisSetting[$suffix]; } break; - } elseif( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) { - if (!isset($retval)) + } elseif ( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) { + if ( !isset( $retval ) ) { $retval = array(); + } $retval = self::arrayMerge( $retval, $thisSetting["+$suffix"] ); } } @@ -175,8 +292,9 @@ class SiteConfiguration { } $value = $this->getSetting( $varname, $wiki, $params ); - if ( $append && is_array( $value ) && is_array( $GLOBALS[$var] ) ) + if ( $append && is_array( $value ) && is_array( $GLOBALS[$var] ) ) { $value = self::arrayMerge( $value, $GLOBALS[$var] ); + } if ( !is_null( $value ) ) { $localSettings[$var] = $value; } @@ -210,7 +328,7 @@ class SiteConfiguration { * @param $setting String ID of the setting name to retrieve * @param $wiki String Wiki ID of the wiki in question. * @param $suffix String The suffix of the wiki in question. - * @param $var Reference The variable to insert the value into. + * @param $var array Reference The variable to insert the value into. * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. * @param $wikiTags Array The tags assigned to the wiki. */ @@ -296,8 +414,9 @@ class SiteConfiguration { } foreach( $default as $name => $def ){ - if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) ) + if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) ) { $ret[$name] = $default[$name]; + } } return $ret; @@ -318,18 +437,21 @@ class SiteConfiguration { protected function mergeParams( $wiki, $suffix, /*array*/ $params, /*array*/ $wikiTags ){ $ret = $this->getWikiParams( $wiki ); - if( is_null( $ret['suffix'] ) ) + if( is_null( $ret['suffix'] ) ) { $ret['suffix'] = $suffix; + } $ret['tags'] = array_unique( array_merge( $ret['tags'], $wikiTags ) ); $ret['params'] += $params; // Automatically fill that ones if needed - if( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) ) + if( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) ){ $ret['params']['lang'] = $ret['lang']; - if( !isset( $ret['params']['site'] ) && !is_null( $ret['suffix'] ) ) + } + if( !isset( $ret['params']['site'] ) && !is_null( $ret['suffix'] ) ) { $ret['params']['site'] = $ret['suffix']; + } return $ret; } @@ -343,8 +465,9 @@ class SiteConfiguration { public function siteFromDB( $db ) { // Allow override $def = $this->getWikiParams( $db ); - if( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) + if( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) { return array( $def['suffix'], $def['lang'] ); + } $site = null; $lang = null; @@ -401,7 +524,7 @@ class SiteConfiguration { } public function loadFullData() { - if ($this->fullLoadCallback && !$this->fullLoadDone) { + if ( $this->fullLoadCallback && !$this->fullLoadDone ) { call_user_func( $this->fullLoadCallback, $this ); $this->fullLoadDone = true; } diff --git a/includes/SiteStats.php b/includes/SiteStats.php index abb11306..1c2c454d 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -1,4 +1,24 @@ <?php +/** + * Accessors and mutators for the site-wide statistics. + * + * 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 + */ /** * Static accessor class for site_stats and related things @@ -223,53 +243,91 @@ class SiteStats { * Class for handling updates to the site_stats table */ class SiteStatsUpdate implements DeferrableUpdate { - - var $mViews, $mEdits, $mGood, $mPages, $mUsers; - + protected $views = 0; + protected $edits = 0; + protected $pages = 0; + protected $articles = 0; + protected $users = 0; + protected $images = 0; + + // @TODO: deprecate this constructor function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) { - $this->mViews = $views; - $this->mEdits = $edits; - $this->mGood = $good; - $this->mPages = $pages; - $this->mUsers = $users; + $this->views = $views; + $this->edits = $edits; + $this->articles = $good; + $this->pages = $pages; + $this->users = $users; } /** - * @param $sql - * @param $field - * @param $delta + * @param $deltas Array + * @return SiteStatsUpdate */ - function appendUpdate( &$sql, $field, $delta ) { - if ( $delta ) { - if ( $sql ) { - $sql .= ','; - } - if ( $delta < 0 ) { - $sql .= "$field=$field-1"; - } else { - $sql .= "$field=$field+1"; + public static function factory( array $deltas ) { + $update = new self( 0, 0, 0 ); + + $fields = array( 'views', 'edits', 'pages', 'articles', 'users', 'images' ); + foreach ( $fields as $field ) { + if ( isset( $deltas[$field] ) && $deltas[$field] ) { + $update->$field = $deltas[$field]; } } + + return $update; } - function doUpdate() { - $dbw = wfGetDB( DB_MASTER ); + public function doUpdate() { + global $wgSiteStatsAsyncFactor; + + $rate = $wgSiteStatsAsyncFactor; // convenience + // If set to do so, only do actual DB updates 1 every $rate times. + // The other times, just update "pending delta" values in memcached. + if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) { + $this->doUpdatePendingDeltas(); + } else { + $dbw = wfGetDB( DB_MASTER ); + + $lockKey = wfMemcKey( 'site_stats' ); // prepend wiki ID + if ( $rate ) { + // Lock the table so we don't have double DB/memcached updates + if ( !$dbw->lockIsFree( $lockKey, __METHOD__ ) + || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout + ) { + $this->doUpdatePendingDeltas(); + return; + } + $pd = $this->getPendingDeltas(); + // Piggy-back the async deltas onto those of this stats update.... + $this->views += ( $pd['ss_total_views']['+'] - $pd['ss_total_views']['-'] ); + $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] ); + $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] ); + $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] ); + $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] ); + $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] ); + } - $updates = ''; + // Need a separate transaction because this a global lock + $dbw->begin( __METHOD__ ); - $this->appendUpdate( $updates, 'ss_total_views', $this->mViews ); - $this->appendUpdate( $updates, 'ss_total_edits', $this->mEdits ); - $this->appendUpdate( $updates, 'ss_good_articles', $this->mGood ); - $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages ); - $this->appendUpdate( $updates, 'ss_users', $this->mUsers ); + // Build up an SQL query of deltas and apply them... + $updates = ''; + $this->appendUpdate( $updates, 'ss_total_views', $this->views ); + $this->appendUpdate( $updates, 'ss_total_edits', $this->edits ); + $this->appendUpdate( $updates, 'ss_good_articles', $this->articles ); + $this->appendUpdate( $updates, 'ss_total_pages', $this->pages ); + $this->appendUpdate( $updates, 'ss_users', $this->users ); + $this->appendUpdate( $updates, 'ss_images', $this->images ); + if ( $updates != '' ) { + $dbw->update( 'site_stats', array( $updates ), array(), __METHOD__ ); + } - if ( $updates ) { - $site_stats = $dbw->tableName( 'site_stats' ); - $sql = "UPDATE $site_stats SET $updates"; + if ( $rate ) { + // Decrement the async deltas now that we applied them + $this->removePendingDeltas( $pd ); + // Commit the updates and unlock the table + $dbw->unlock( $lockKey, __METHOD__ ); + } - # Need a separate transaction because this a global lock - $dbw->begin( __METHOD__ ); - $dbw->query( $sql, __METHOD__ ); $dbw->commit( __METHOD__ ); } } @@ -289,8 +347,8 @@ class SiteStatsUpdate implements DeferrableUpdate { array( 'rc_user != 0', 'rc_bot' => 0, - "rc_log_type != 'newusers' OR rc_log_type IS NULL", - "rc_timestamp >= '{$dbw->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 )}'", + 'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL', + 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ) ), ), __METHOD__ ); @@ -302,6 +360,102 @@ class SiteStatsUpdate implements DeferrableUpdate { ); return $activeUsers; } + + protected function doUpdatePendingDeltas() { + $this->adjustPending( 'ss_total_views', $this->views ); + $this->adjustPending( 'ss_total_edits', $this->edits ); + $this->adjustPending( 'ss_good_articles', $this->articles ); + $this->adjustPending( 'ss_total_pages', $this->pages ); + $this->adjustPending( 'ss_users', $this->users ); + $this->adjustPending( 'ss_images', $this->images ); + } + + /** + * @param $sql string + * @param $field string + * @param $delta integer + */ + protected function appendUpdate( &$sql, $field, $delta ) { + if ( $delta ) { + if ( $sql ) { + $sql .= ','; + } + if ( $delta < 0 ) { + $sql .= "$field=$field-" . abs( $delta ); + } else { + $sql .= "$field=$field+" . abs( $delta ); + } + } + } + + /** + * @param $type string + * @param $sign string ('+' or '-') + * @return string + */ + private function getTypeCacheKey( $type, $sign ) { + return wfMemcKey( 'sitestatsupdate', 'pendingdelta', $type, $sign ); + } + + /** + * Adjust the pending deltas for a stat type. + * Each stat type has two pending counters, one for increments and decrements + * @param $type string + * @param $delta integer Delta (positive or negative) + * @return void + */ + protected function adjustPending( $type, $delta ) { + global $wgMemc; + + if ( $delta < 0 ) { // decrement + $key = $this->getTypeCacheKey( $type, '-' ); + } else { // increment + $key = $this->getTypeCacheKey( $type, '+' ); + } + + $magnitude = abs( $delta ); + if ( !$wgMemc->incr( $key, $magnitude ) ) { // not there? + if ( !$wgMemc->add( $key, $magnitude ) ) { // race? + $wgMemc->incr( $key, $magnitude ); + } + } + } + + /** + * Get pending delta counters for each stat type + * @return Array Positive and negative deltas for each type + * @return void + */ + protected function getPendingDeltas() { + global $wgMemc; + + $pending = array(); + foreach ( array( 'ss_total_views', 'ss_total_edits', + 'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ) as $type ) + { + // Get pending increments and pending decrements + $pending[$type]['+'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '+' ) ); + $pending[$type]['-'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '-' ) ); + } + + return $pending; + } + + /** + * Reduce pending delta counters after updates have been applied + * @param Array $pd Result of getPendingDeltas(), used for DB update + * @return void + */ + protected function removePendingDeltas( array $pd ) { + global $wgMemc; + + foreach ( $pd as $type => $deltas ) { + foreach ( $deltas as $sign => $magnitude ) { + // Lower the pending counter now that we applied these changes + $wgMemc->decr( $this->getTypeCacheKey( $type, $sign ), $magnitude ); + } + } + } } /** diff --git a/includes/Skin.php b/includes/Skin.php index 430537d4..968f215e 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -1,11 +1,28 @@ <?php /** - * @defgroup Skins Skins + * Base class for all skins. + * + * 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 */ -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} +/** + * @defgroup Skins Skins + */ /** * The main skin class that provide methods and properties for all other skins. @@ -22,7 +39,7 @@ abstract class Skin extends ContextSource { /** * Fetch the set of available skins. - * @return associative array of strings + * @return array associative array of strings */ static function getSkinNames() { global $wgValidSkinNames; @@ -55,7 +72,7 @@ abstract class Skin extends ContextSource { } return $wgValidSkinNames; } - + /** * Fetch the skinname messages for available skins. * @return array of strings @@ -98,7 +115,7 @@ abstract class Skin extends ContextSource { $skinNames = Skin::getSkinNames(); - if ( $key == '' ) { + if ( $key == '' || $key == 'default' ) { // Don't return the default immediately; // in a misconfiguration we need to fall back. $key = $wgDefaultSkin; @@ -147,11 +164,6 @@ abstract class Skin extends ContextSource { if ( !MWInit::classExists( $className ) ) { if ( !defined( 'MW_COMPILED' ) ) { - // Preload base classes to work around APC/PHP5 bug - $deps = "{$wgStyleDirectory}/{$skinName}.deps.php"; - if ( file_exists( $deps ) ) { - include_once( $deps ); - } require_once( "{$wgStyleDirectory}/{$skinName}.php" ); } @@ -314,10 +326,10 @@ abstract class Skin extends ContextSource { } /** - * Make a <script> tag containing global variables + * Make a "<script>" tag containing global variables * * @deprecated in 1.19 - * @param $unused Unused + * @param $unused * @return string HTML fragment */ public static function makeGlobalVariablesScript( $unused ) { @@ -351,7 +363,7 @@ abstract class Skin extends ContextSource { * inside ->getOutput() is deprecated. The $out arg is kept * for compatibility purposes with skins. * @param $out OutputPage - * @delete + * @todo delete */ abstract function setupSkinUserCss( OutputPage $out ); @@ -385,7 +397,7 @@ abstract class Skin extends ContextSource { /** * This will be called by OutputPage::headElement when it is creating the - * <body> tag, skins can override it if they have a need to add in any + * "<body>" tag, skins can override it if they have a need to add in any * body attributes or classes of their own. * @param $out OutputPage * @param $bodyAttrs Array @@ -425,7 +437,7 @@ abstract class Skin extends ContextSource { if ( !empty( $allCats['normal'] ) ) { $t = $embed . implode( "{$pop}{$embed}" , $allCats['normal'] ) . $pop; - $msg = $this->msg( 'pagecategories', count( $allCats['normal'] ) )->escaped(); + $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped(); $linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text(); $s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' . Linker::link( Title::newFromText( $linkPage ), $msg ) @@ -443,7 +455,7 @@ abstract class Skin extends ContextSource { } $s .= "<div id=\"mw-hidden-catlinks\" class=\"mw-hidden-catlinks$class\">" . - $this->msg( 'hidden-categories', count( $allCats['hidden'] ) )->escaped() . + $this->msg( 'hidden-categories' )->numParams( count( $allCats['hidden'] ) )->escaped() . $colon . '<ul>' . $embed . implode( "{$pop}{$embed}" , $allCats['hidden'] ) . $pop . '</ul>' . '</div>'; } @@ -556,77 +568,13 @@ abstract class Skin extends ContextSource { * @return String HTML containing debug data, if enabled (otherwise empty). */ protected function generateDebugHTML() { - global $wgShowDebug; - - $html = MWDebug::getDebugHTML( $this->getContext() ); - - if ( $wgShowDebug ) { - $listInternals = $this->formatDebugHTML( $this->getOutput()->mDebugtext ); - $html .= "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">" . - $listInternals . "</ul>\n"; - } - - return $html; - } - - /** - * @param $debugText string - * @return string - */ - private function formatDebugHTML( $debugText ) { - global $wgDebugTimestamps; - - $lines = explode( "\n", $debugText ); - $curIdent = 0; - $ret = '<li>'; - - foreach ( $lines as $line ) { - $pre = ''; - if ( $wgDebugTimestamps ) { - $matches = array(); - if ( preg_match( '/^(\d+\.\d+ {1,3}\d+.\dM\s{2})/', $line, $matches ) ) { - $pre = $matches[1]; - $line = substr( $line, strlen( $pre ) ); - } - } - $display = ltrim( $line ); - $ident = strlen( $line ) - strlen( $display ); - $diff = $ident - $curIdent; - - $display = $pre . $display; - if ( $display == '' ) { - $display = "\xc2\xa0"; - } - - if ( !$ident && $diff < 0 && substr( $display, 0, 9 ) != 'Entering ' && substr( $display, 0, 8 ) != 'Exiting ' ) { - $ident = $curIdent; - $diff = 0; - $display = '<span style="background:yellow;">' . htmlspecialchars( $display ) . '</span>'; - } else { - $display = htmlspecialchars( $display ); - } - - if ( $diff < 0 ) { - $ret .= str_repeat( "</li></ul>\n", -$diff ) . "</li><li>\n"; - } elseif ( $diff == 0 ) { - $ret .= "</li><li>\n"; - } else { - $ret .= str_repeat( "<ul><li>\n", $diff ); - } - $ret .= "<tt>$display</tt>\n"; - - $curIdent = $ident; - } - - $ret .= str_repeat( '</li></ul>', $curIdent ) . '</li>'; - - return $ret; + return MWDebug::getHTMLDebugLog(); } /** - * This gets called shortly before the </body> tag. + * This gets called shortly before the "</body>" tag. * - * @return String HTML-wrapped JS code to be put before </body> + * @return String HTML-wrapped JS code to be put before "</body>" */ function bottomScripts() { // TODO and the suckage continues. This function is really just a wrapper around @@ -647,10 +595,10 @@ abstract class Skin extends ContextSource { function printSource() { $oldid = $this->getRevisionId(); if ( $oldid ) { - $url = htmlspecialchars( $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid ) ); + $url = htmlspecialchars( wfExpandIRI( $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid ) ) ); } else { // oldid not available for non existing pages - $url = htmlspecialchars( $this->getTitle()->getCanonicalURL() ); + $url = htmlspecialchars( wfExpandIRI( $this->getTitle()->getCanonicalURL() ) ); } return $this->msg( 'retrievedfrom', '<a href="' . $url . '">' . $url . '</a>' )->text(); } @@ -662,7 +610,7 @@ abstract class Skin extends ContextSource { $action = $this->getRequest()->getVal( 'action', 'view' ); if ( $this->getUser()->isAllowed( 'deletedhistory' ) && - ( $this->getTitle()->getArticleId() == 0 || $action == 'history' ) ) { + ( $this->getTitle()->getArticleID() == 0 || $action == 'history' ) ) { $n = $this->getTitle()->isDeleted(); @@ -688,6 +636,7 @@ abstract class Skin extends ContextSource { * @return string */ function subPageSubtitle() { + global $wgLang; $out = $this->getOutput(); $subpages = ''; @@ -709,7 +658,7 @@ abstract class Skin extends ContextSource { $display .= $link; $linkObj = Title::newFromText( $growinglink ); - if ( is_object( $linkObj ) && $linkObj->exists() ) { + if ( is_object( $linkObj ) && $linkObj->isKnown() ) { $getlink = Linker::linkKnown( $linkObj, htmlspecialchars( $display ) @@ -718,7 +667,7 @@ abstract class Skin extends ContextSource { $c++; if ( $c > 1 ) { - $subpages .= $this->msg( 'pipe-separator' )->escaped(); + $subpages .= $wgLang->getDirMarkEntity() . $this->msg( 'pipe-separator' )->escaped(); } else { $subpages .= '< '; } @@ -886,7 +835,7 @@ abstract class Skin extends ContextSource { */ function logoText( $align = '' ) { if ( $align != '' ) { - $a = " align='{$align}'"; + $a = " style='float: {$align};'"; } else { $a = ''; } @@ -1054,13 +1003,23 @@ abstract class Skin extends ContextSource { } /** - * @param $name string - * @param $urlaction string + * Make a URL for a Special Page using the given query and protocol. + * + * If $proto is set to null, make a local URL. Otherwise, make a full + * URL with the protocol specified. + * + * @param $name string Name of the Special page + * @param $urlaction string Query to append + * @param $proto Protocol to use or null for a local URL * @return String */ - static function makeSpecialUrl( $name, $urlaction = '' ) { + static function makeSpecialUrl( $name, $urlaction = '', $proto = null ) { $title = SpecialPage::getSafeTitleFor( $name ); - return $title->getLocalURL( $urlaction ); + if( is_null( $proto ) ) { + return $title->getLocalURL( $urlaction ); + } else { + return $title->getFullURL( $urlaction, false, $proto ); + } } /** @@ -1080,7 +1039,7 @@ abstract class Skin extends ContextSource { * @return String */ static function makeI18nUrl( $name, $urlaction = '' ) { - $title = Title::newFromText( wfMsgForContent( $name ) ); + $title = Title::newFromText( wfMessage( $name )->inContentLanguage()->text() ); self::checkTitle( $title, $name ); return $title->getLocalURL( $urlaction ); } @@ -1104,7 +1063,7 @@ abstract class Skin extends ContextSource { * @return String URL */ static function makeInternalOrExternalUrl( $name ) { - if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $name ) ) { + if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $name ) ) { return $name; } else { return self::makeUrl( $name ); @@ -1212,7 +1171,7 @@ abstract class Skin extends ContextSource { * @param $message String */ function addToSidebar( &$bar, $message ) { - $this->addToSidebarPlain( $bar, wfMsgForContentNoTrans( $message ) ); + $this->addToSidebarPlain( $bar, wfMessage( $message )->inContentLanguage()->plain() ); } /** @@ -1268,7 +1227,7 @@ abstract class Skin extends ContextSource { $text = $line[1]; } - if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $link ) ) { + if ( preg_match( '/^(?i:' . wfUrlProtocols() . ')/', $link ) ) { $href = $link; // Parser::getExternalLinkAttribs won't work here because of the Namespace things @@ -1329,29 +1288,59 @@ abstract class Skin extends ContextSource { $ntl = ''; if ( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) { - $userTitle = $this->getUser()->getUserPage(); - $userTalkTitle = $userTitle->getTalkPage(); - - if ( !$userTalkTitle->equals( $out->getTitle() ) ) { + $uTalkTitle = $this->getUser()->getTalkPage(); + + if ( !$uTalkTitle->equals( $out->getTitle() ) ) { + $lastSeenRev = isset( $newtalks[0]['rev'] ) ? $newtalks[0]['rev'] : null; + $nofAuthors = 0; + if ( $lastSeenRev !== null ) { + $plural = true; // Default if we have a last seen revision: if unknown, use plural + $latestRev = Revision::newFromTitle( $uTalkTitle, false, Revision::READ_NORMAL ); + if ( $latestRev !== null ) { + // Singular if only 1 unseen revision, plural if several unseen revisions. + $plural = $latestRev->getParentId() !== $lastSeenRev->getId(); + $nofAuthors = $uTalkTitle->countAuthorsBetween( + $lastSeenRev, $latestRev, 10, 'include_new' ); + } + } else { + // Singular if no revision -> diff link will show latest change only in any case + $plural = false; + } + $plural = $plural ? 2 : 1; + // 2 signifies "more than one revision". We don't know how many, and even if we did, + // the number of revisions or authors is not necessarily the same as the number of + // "messages". $newMessagesLink = Linker::linkKnown( - $userTalkTitle, - $this->msg( 'newmessageslink' )->escaped(), + $uTalkTitle, + $this->msg( 'newmessageslinkplural' )->params( $plural )->escaped(), array(), array( 'redirect' => 'no' ) ); $newMessagesDiffLink = Linker::linkKnown( - $userTalkTitle, - $this->msg( 'newmessagesdifflink' )->escaped(), + $uTalkTitle, + $this->msg( 'newmessagesdifflinkplural' )->params( $plural )->escaped(), array(), - array( 'diff' => 'cur' ) + $lastSeenRev !== null + ? array( 'oldid' => $lastSeenRev->getId(), 'diff' => 'cur' ) + : array( 'diff' => 'cur' ) ); - $ntl = $this->msg( - 'youhavenewmessages', - $newMessagesLink, - $newMessagesDiffLink - )->text(); + if ( $nofAuthors >= 1 && $nofAuthors <= 10 ) { + $ntl = $this->msg( + 'youhavenewmessagesfromusers', + $newMessagesLink, + $newMessagesDiffLink + )->numParams( $nofAuthors ); + } else { + // $nofAuthors === 11 signifies "11 or more" ("more than 10") + $ntl = $this->msg( + $nofAuthors > 10 ? 'youhavenewmessagesmanyusers' : 'youhavenewmessages', + $newMessagesLink, + $newMessagesDiffLink + ); + } + $ntl = $ntl->text(); # Disable Squid cache $out->setSquidMaxage( 0 ); } @@ -1495,13 +1484,17 @@ abstract class Skin extends ContextSource { public function doEditSectionLink( Title $nt, $section, $tooltip = null, $lang = false ) { // HTML generated here should probably have userlangattributes // added to it for LTR text on RTL pages + + $lang = wfGetLangObj( $lang ); + $attribs = array(); if ( !is_null( $tooltip ) ) { # Bug 25462: undo double-escaping. $tooltip = Sanitizer::decodeCharReferences( $tooltip ); - $attribs['title'] = wfMsgExt( 'editsectionhint', array( 'language' => $lang, 'parsemag', 'replaceafter' ), $tooltip ); + $attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip ) + ->inLanguage( $lang )->text(); } - $link = Linker::link( $nt, wfMsgExt( 'editsection', array( 'language' => $lang ) ), + $link = Linker::link( $nt, wfMessage( 'editsection' )->inLanguage( $lang )->text(), $attribs, array( 'action' => 'edit', 'section' => $section ), array( 'noclasses', 'known' ) @@ -1511,7 +1504,8 @@ abstract class Skin extends ContextSource { # we can rid of it someday. $attribs = ''; if ( $tooltip ) { - $attribs = wfMsgExt( 'editsectionhint', array( 'language' => $lang, 'parsemag', 'escape', 'replaceafter' ), $tooltip ); + $attribs = wfMessage( 'editsectionhint' )->rawParams( $tooltip ) + ->inLanguage( $lang )->escaped(); $attribs = " title=\"$attribs\""; } $result = null; @@ -1521,13 +1515,15 @@ abstract class Skin extends ContextSource { # run, and even add them to hook-provided text. (This is the main # reason that the EditSectionLink hook is deprecated in favor of # DoEditSectionLink: it can't change the brackets or the span.) - $result = wfMsgExt( 'editsection-brackets', array( 'escape', 'replaceafter', 'language' => $lang ), $result ); + $result = wfMessage( 'editsection-brackets' )->rawParams( $result ) + ->inLanguage( $lang )->escaped(); return "<span class=\"editsection\">$result</span>"; } # Add the brackets and the span, and *then* run the nice new hook, with # clean and non-redundant arguments. - $result = wfMsgExt( 'editsection-brackets', array( 'escape', 'replaceafter', 'language' => $lang ), $link ); + $result = wfMessage( 'editsection-brackets' )->rawParams( $link ) + ->inLanguage( $lang )->escaped(); $result = "<span class=\"editsection\">$result</span>"; wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result, $lang ) ); @@ -1540,6 +1536,7 @@ abstract class Skin extends ContextSource { * * @param $fname String Name of called method * @param $args Array Arguments to the method + * @return mixed */ function __call( $fname, $args ) { $realFunction = array( 'Linker', $fname ); diff --git a/includes/SkinLegacy.php b/includes/SkinLegacy.php index 77c85a88..e695ba6c 100644 --- a/includes/SkinLegacy.php +++ b/includes/SkinLegacy.php @@ -1,12 +1,25 @@ <?php /** - * @defgroup Skins Skins + * Base class for legacy skins. + * + * 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 */ -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} - class SkinLegacy extends SkinTemplate { var $useHeadElement = true; protected $mWatchLinkNum = 0; // Appended to end of watch link id's @@ -82,8 +95,9 @@ class LegacyTemplate extends BaseTemplate { } /** - * This will be called immediately after the <body> tag. Split into + * This will be called immediately after the "<body>" tag. Split into * two functions to make it easier to subclass. + * @return string */ function beforeContent() { return $this->doBeforeContent(); @@ -106,21 +120,21 @@ class LegacyTemplate extends BaseTemplate { } $s .= "\n<div id='content'>\n<div id='topbar'>\n" . - "<table border='0' cellspacing='0' width='100%'>\n<tr>\n"; + "<table cellspacing='0' width='100%'>\n<tr>\n"; if ( $this->getSkin()->qbSetting() == 0 ) { - $s .= "<td class='top' align='left' valign='top' rowspan='{$rows}'>\n" . + $s .= "<td class='top' style='text-align: left; vertical-align: top;' rowspan='{$rows}'>\n" . $this->getSkin()->logoText( $wgLang->alignStart() ) . '</td>'; } $l = $wgLang->alignStart(); - $s .= "<td {$borderhack} align='$l' valign='top'>\n"; + $s .= "<td {$borderhack} style='text-align: $l; vertical-align: top;'>\n"; $s .= $this->topLinks(); $s .= '<p class="subtitle">' . $this->pageTitleLinks() . "</p>\n"; $r = $wgLang->alignEnd(); - $s .= "</td>\n<td {$borderhack} valign='top' align='$r' nowrap='nowrap'>"; + $s .= "</td>\n<td {$borderhack} style='text-align: $r; vertical-align: top;' nowrap='nowrap'>"; $s .= $this->nameAndLogin(); $s .= "\n<br />" . $this->searchForm() . '</td>'; @@ -145,14 +159,16 @@ class LegacyTemplate extends BaseTemplate { } /** - * This gets called shortly before the </body> tag. - * @return String HTML to be put before </body> + * This gets called shortly before the "</body>" tag. + * @return String HTML to be put before "</body>" */ function afterContent() { return $this->doAfterContent(); } - /** overloaded by derived classes */ + /** overloaded by derived classes + * @return string + */ function doAfterContent() { return '</div></div>'; } @@ -166,12 +182,12 @@ class LegacyTemplate extends BaseTemplate { . $this->getSkin()->escapeSearchLink() . "\">\n" . '<input type="text" id="searchInput' . $this->searchboxes . '" name="search" size="19" value="' . htmlspecialchars( substr( $search, 0, 256 ) ) . "\" />\n" - . '<input type="submit" name="go" value="' . wfMsg( 'searcharticle' ) . '" />'; + . '<input type="submit" name="go" value="' . wfMessage( 'searcharticle' )->text() . '" />'; if ( $wgUseTwoButtonsSearchForm ) { - $s .= ' <input type="submit" name="fulltext" value="' . wfMsg( 'searchbutton' ) . "\" />\n"; + $s .= ' <input type="submit" name="fulltext" value="' . wfMessage( 'searchbutton' )->text() . "\" />\n"; } else { - $s .= ' <a href="' . $this->getSkin()->escapeSearchLink() . '" rel="search">' . wfMsg( 'powersearch-legend' ) . "</a>\n"; + $s .= ' <a href="' . $this->getSkin()->escapeSearchLink() . '" rel="search">' . wfMessage( 'powersearch-legend' )->text() . "</a>\n"; } $s .= '</form>'; @@ -220,7 +236,7 @@ class LegacyTemplate extends BaseTemplate { } // @todo FIXME: Is using Language::pipeList impossible here? Do not quite understand the use of the newline - return implode( $s, wfMsgExt( 'pipe-separator', 'escapenoentities' ) . "\n" ); + return implode( $s, wfMessage( 'pipe-separator' )->escaped() . "\n" ); } /** @@ -247,7 +263,7 @@ class LegacyTemplate extends BaseTemplate { } $s = $wgLang->pipeList( array( $s, - '<a href="' . htmlspecialchars( $title->getLocalURL( 'variant=' . $code ) ) . '">' . htmlspecialchars( $varname ) . '</a>' + '<a href="' . htmlspecialchars( $title->getLocalURL( 'variant=' . $code ) ) . '" lang="' . $code . '" hreflang="' . $code . '">' . htmlspecialchars( $varname ) . '</a>' ) ); } } @@ -276,7 +292,7 @@ class LegacyTemplate extends BaseTemplate { if ( count( $s ) ) { global $wgLang; - $out = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); + $out = wfMessage( 'pipe-separator' )->escaped(); $out .= $wgLang->pipeList( $s ); } @@ -285,7 +301,7 @@ class LegacyTemplate extends BaseTemplate { function bottomLinks() { global $wgOut, $wgUser; - $sep = wfMsgExt( 'pipe-separator', 'escapenoentities' ) . "\n"; + $sep = wfMessage( 'pipe-separator' )->escaped() . "\n"; $s = ''; if ( $wgOut->isArticleRelated() ) { @@ -321,7 +337,7 @@ class LegacyTemplate extends BaseTemplate { $s = implode( $element, $sep ); - if ( $title->getArticleId() ) { + if ( $title->getArticleID() ) { $s .= "\n<br />"; // Delete/protect/move links for privileged users @@ -345,7 +361,7 @@ class LegacyTemplate extends BaseTemplate { } function otherLanguages() { - global $wgOut, $wgLang, $wgContLang, $wgHideInterlanguageLinks; + global $wgOut, $wgLang, $wgHideInterlanguageLinks; if ( $wgHideInterlanguageLinks ) { return ''; @@ -357,7 +373,7 @@ class LegacyTemplate extends BaseTemplate { return ''; } - $s = wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' ); + $s = wfMessage( 'otherlanguages' )->text() . wfMessage( 'colon-separator' )->text(); $first = true; if ( $wgLang->isRTL() ) { @@ -366,13 +382,13 @@ class LegacyTemplate extends BaseTemplate { foreach ( $a as $l ) { if ( !$first ) { - $s .= wfMsgExt( 'pipe-separator', 'escapenoentities' ); + $s .= wfMessage( 'pipe-separator' )->escaped(); } $first = false; $nt = Title::newFromText( $l ); - $text = $wgContLang->getLanguageName( $nt->getInterwiki() ); + $text = Language::fetchLanguageName( $nt->getInterwiki() ); $s .= Html::element( 'a', array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ), @@ -388,6 +404,7 @@ class LegacyTemplate extends BaseTemplate { /** * Show a drop-down box of special pages + * @return string */ function specialPagesList() { global $wgScript; @@ -400,8 +417,9 @@ class LegacyTemplate extends BaseTemplate { $obj->getTitle()->getPrefixedDBkey() ); } - return Html::rawElement( 'form', array( 'id' => 'specialpages', 'method' => 'get', - 'action' => $wgScript ), $select->getHTML() . Xml::submitButton( wfMsg( 'go' ) ) ); + return Html::rawElement( 'form', + array( 'id' => 'specialpages', 'method' => 'get', 'action' => $wgScript ), + $select->getHTML() . Xml::submitButton( wfMessage( 'go' )->text() ) ); } function pageTitleLinks() { @@ -429,21 +447,21 @@ class LegacyTemplate extends BaseTemplate { if ( $wgOut->isArticleRelated() ) { if ( $title->getNamespace() == NS_FILE ) { - $name = $title->getDBkey(); $image = wfFindFile( $title ); if ( $image ) { - $link = htmlspecialchars( $image->getURL() ); - $style = Linker::getInternalLinkAttributes( $link, $name ); - $s[] = "<a href=\"{$link}\"{$style}>{$name}</a>"; + $href = $image->getURL(); + $s[] = Html::element( 'a', array( 'href' => $href, + 'title' => $href ), $title->getText() ); + } } } if ( 'history' == $action || isset( $diff ) || isset( $oldid ) ) { $s[] .= Linker::linkKnown( - $title, - wfMsg( 'currentrev' ) + $title, + wfMessage( 'currentrev' )->text() ); } @@ -453,18 +471,18 @@ class LegacyTemplate extends BaseTemplate { if ( !$title->equals( $wgUser->getTalkPage() ) ) { $tl = Linker::linkKnown( $wgUser->getTalkPage(), - wfMsgHtml( 'newmessageslink' ), + wfMessage( 'newmessageslink' )->escaped(), array(), array( 'redirect' => 'no' ) ); $dl = Linker::linkKnown( $wgUser->getTalkPage(), - wfMsgHtml( 'newmessagesdifflink' ), + wfMessage( 'newmessagesdifflink' )->escaped(), array(), array( 'diff' => 'cur' ) ); - $s[] = '<strong>' . wfMsg( 'youhavenewmessages', $tl, $dl ) . '</strong>'; + $s[] = '<strong>' . wfMessage( 'youhavenewmessages', $tl, $dl )->text() . '</strong>'; # disable caching $wgOut->setSquidMaxage( 0 ); $wgOut->enableClientCache( false ); @@ -497,7 +515,7 @@ class LegacyTemplate extends BaseTemplate { if ( $sub == '' ) { global $wgExtraSubtitle; - $sub = wfMsgExt( 'tagline', 'parsemag' ) . $wgExtraSubtitle; + $sub = wfMessage( 'tagline' )->parse() . $wgExtraSubtitle; } $subpages = $this->getSkin()->subPageSubtitle(); @@ -515,14 +533,15 @@ class LegacyTemplate extends BaseTemplate { if ( !$wgOut->isPrintable() ) { $printurl = htmlspecialchars( $this->getSkin()->getTitle()->getLocalUrl( $wgRequest->appendQueryValue( 'printable', 'yes', true ) ) ); - $s[] = "<a href=\"$printurl\" rel=\"alternate\">" . wfMsg( 'printableversion' ) . '</a>'; + $s[] = "<a href=\"$printurl\" rel=\"alternate\">" + . wfMessage( 'printableversion' )->text() . '</a>'; } if ( $wgOut->isSyndicated() ) { foreach ( $wgOut->getSyndicationLinks() as $format => $link ) { $feedurl = htmlspecialchars( $link ); $s[] = "<a href=\"$feedurl\" rel=\"alternate\" type=\"application/{$format}+xml\"" - . " class=\"feedlink\">" . wfMsgHtml( "feed-$format" ) . "</a>"; + . " class=\"feedlink\">" . wfMessage( "feed-$format" )->escaped() . "</a>"; } } return $wgLang->pipeList( $s ); @@ -530,6 +549,7 @@ class LegacyTemplate extends BaseTemplate { /** * @deprecated in 1.19 + * @return string */ function getQuickbarCompensator( $rows = 1 ) { wfDeprecated( __METHOD__, '1.19' ); @@ -540,15 +560,15 @@ class LegacyTemplate extends BaseTemplate { global $wgOut; if ( !$wgOut->isArticleRelated() ) { - $s = wfMsg( 'protectedpage' ); + $s = wfMessage( 'protectedpage' )->text(); } else { $title = $this->getSkin()->getTitle(); if ( $title->quickUserCan( 'edit' ) && $title->exists() ) { - $t = wfMsg( 'editthispage' ); + $t = wfMessage( 'editthispage' )->text(); } elseif ( $title->quickUserCan( 'create' ) && !$title->exists() ) { - $t = wfMsg( 'create-this-page' ); + $t = wfMessage( 'create-this-page' )->text(); } else { - $t = wfMsg( 'viewsource' ); + $t = wfMessage( 'viewsource' )->text(); } $s = Linker::linkKnown( @@ -568,8 +588,8 @@ class LegacyTemplate extends BaseTemplate { $diff = $wgRequest->getVal( 'diff' ); $title = $this->getSkin()->getTitle(); - if ( $title->getArticleId() && ( !$diff ) && $wgUser->isAllowed( 'delete' ) ) { - $t = wfMsg( 'deletethispage' ); + if ( $title->getArticleID() && ( !$diff ) && $wgUser->isAllowed( 'delete' ) ) { + $t = wfMessage( 'deletethispage' )->text(); $s = Linker::linkKnown( $title, @@ -590,12 +610,12 @@ class LegacyTemplate extends BaseTemplate { $diff = $wgRequest->getVal( 'diff' ); $title = $this->getSkin()->getTitle(); - if ( $title->getArticleId() && ( ! $diff ) && $wgUser->isAllowed( 'protect' ) ) { + if ( $title->getArticleID() && ( ! $diff ) && $wgUser->isAllowed( 'protect' ) ) { if ( $title->isProtected() ) { - $text = wfMsg( 'unprotectthispage' ); + $text = wfMessage( 'unprotectthispage' )->text(); $query = array( 'action' => 'unprotect' ); } else { - $text = wfMsg( 'protectthispage' ); + $text = wfMessage( 'protectthispage' )->text(); $query = array( 'action' => 'protect' ); } @@ -620,15 +640,15 @@ class LegacyTemplate extends BaseTemplate { $title = $this->getSkin()->getTitle(); if ( $wgOut->isArticleRelated() ) { - if ( $title->userIsWatching() ) { - $text = wfMsg( 'unwatchthispage' ); + if ( $wgUser->isWatched( $title ) ) { + $text = wfMessage( 'unwatchthispage' )->text(); $query = array( 'action' => 'unwatch', 'token' => UnwatchAction::getUnwatchToken( $title, $wgUser ), ); $id = 'mw-unwatch-link' . $this->mWatchLinkNum; } else { - $text = wfMsg( 'watchthispage' ); + $text = wfMessage( 'watchthispage' )->text(); $query = array( 'action' => 'watch', 'token' => WatchAction::getWatchToken( $title, $wgUser ), @@ -643,7 +663,7 @@ class LegacyTemplate extends BaseTemplate { $query ); } else { - $s = wfMsg( 'notanarticle' ); + $s = wfMessage( 'notanarticle' )->text(); } return $s; @@ -653,7 +673,7 @@ class LegacyTemplate extends BaseTemplate { if ( $this->getSkin()->getTitle()->quickUserCan( 'move' ) ) { return Linker::linkKnown( SpecialPage::getTitleFor( 'Movepage' ), - wfMsg( 'movethispage' ), + wfMessage( 'movethispage' )->text(), array(), array( 'target' => $this->getSkin()->getTitle()->getPrefixedDBkey() ) ); @@ -666,7 +686,7 @@ class LegacyTemplate extends BaseTemplate { function historyLink() { return Linker::link( $this->getSkin()->getTitle(), - wfMsgHtml( 'history' ), + wfMessage( 'history' )->escaped(), array( 'rel' => 'archives' ), array( 'action' => 'history' ) ); @@ -675,21 +695,21 @@ class LegacyTemplate extends BaseTemplate { function whatLinksHere() { return Linker::linkKnown( SpecialPage::getTitleFor( 'Whatlinkshere', $this->getSkin()->getTitle()->getPrefixedDBkey() ), - wfMsgHtml( 'whatlinkshere' ) + wfMessage( 'whatlinkshere' )->escaped() ); } function userContribsLink() { return Linker::linkKnown( SpecialPage::getTitleFor( 'Contributions', $this->getSkin()->getTitle()->getDBkey() ), - wfMsgHtml( 'contributions' ) + wfMessage( 'contributions' )->escaped() ); } function emailUserLink() { return Linker::linkKnown( SpecialPage::getTitleFor( 'Emailuser', $this->getSkin()->getTitle()->getDBkey() ), - wfMsgHtml( 'emailuser' ) + wfMessage( 'emailuser' )->escaped() ); } @@ -697,11 +717,11 @@ class LegacyTemplate extends BaseTemplate { global $wgOut; if ( !$wgOut->isArticleRelated() ) { - return '(' . wfMsg( 'notanarticle' ) . ')'; + return wfMessage( 'parentheses', wfMessage( 'notanarticle' )->text() )->escaped(); } else { return Linker::linkKnown( SpecialPage::getTitleFor( 'Recentchangeslinked', $this->getSkin()->getTitle()->getPrefixedDBkey() ), - wfMsgHtml( 'recentchangeslinked-toolbox' ) + wfMessage( 'recentchangeslinked-toolbox' )->escaped() ); } } @@ -719,41 +739,41 @@ class LegacyTemplate extends BaseTemplate { $link = $title->getSubjectPage(); switch( $link->getNamespace() ) { case NS_MAIN: - $text = wfMsg( 'articlepage' ); + $text = wfMessage( 'articlepage' ); break; case NS_USER: - $text = wfMsg( 'userpage' ); + $text = wfMessage( 'userpage' ); break; case NS_PROJECT: - $text = wfMsg( 'projectpage' ); + $text = wfMessage( 'projectpage' ); break; case NS_FILE: - $text = wfMsg( 'imagepage' ); + $text = wfMessage( 'imagepage' ); # Make link known if image exists, even if the desc. page doesn't. if ( wfFindFile( $link ) ) $linkOptions[] = 'known'; break; case NS_MEDIAWIKI: - $text = wfMsg( 'mediawikipage' ); + $text = wfMessage( 'mediawikipage' ); break; case NS_TEMPLATE: - $text = wfMsg( 'templatepage' ); + $text = wfMessage( 'templatepage' ); break; case NS_HELP: - $text = wfMsg( 'viewhelppage' ); + $text = wfMessage( 'viewhelppage' ); break; case NS_CATEGORY: - $text = wfMsg( 'categorypage' ); + $text = wfMessage( 'categorypage' ); break; default: - $text = wfMsg( 'articlepage' ); + $text = wfMessage( 'articlepage' ); } } else { $link = $title->getTalkPage(); - $text = wfMsg( 'talkpage' ); + $text = wfMessage( 'talkpage' ); } - $s = Linker::link( $link, $text, array(), array(), $linkOptions ); + $s = Linker::link( $link, $text->text(), array(), array(), $linkOptions ); return $s; } @@ -775,7 +795,7 @@ class LegacyTemplate extends BaseTemplate { return Linker::linkKnown( $title, - wfMsg( 'postcomment' ), + wfMessage( 'postcomment' )->text(), array(), array( 'action' => 'edit', @@ -789,11 +809,13 @@ class LegacyTemplate extends BaseTemplate { if ( $wgUploadNavigationUrl ) { # Using an empty class attribute to avoid automatic setting of "external" class - return Linker::makeExternalLink( $wgUploadNavigationUrl, wfMsgHtml( 'upload' ), false, null, array( 'class' => '' ) ); + return Linker::makeExternalLink( $wgUploadNavigationUrl, + wfMessage( 'upload' )->escaped(), + false, null, array( 'class' => '' ) ); } else { return Linker::linkKnown( SpecialPage::getTitleFor( 'Upload' ), - wfMsgHtml( 'upload' ) + wfMessage( 'upload' )->escaped() ); } } @@ -810,10 +832,11 @@ class LegacyTemplate extends BaseTemplate { $talkLink = Linker::link( $wgUser->getTalkPage(), $wgLang->getNsText( NS_TALK ) ); + $talkLink = wfMessage( 'parentheses' )->rawParams( $talkLink )->escaped(); - $ret .= "$name ($talkLink)"; + $ret .= "$name $talkLink"; } else { - $ret .= wfMsg( 'notloggedin' ); + $ret .= wfMessage( 'notloggedin' )->text(); } $query = array(); @@ -827,18 +850,19 @@ class LegacyTemplate extends BaseTemplate { : 'login'; $ret .= "\n<br />" . Linker::link( SpecialPage::getTitleFor( 'Userlogin' ), - wfMsg( $loginlink ), array(), $query + wfMessage( $loginlink )->text(), array(), $query ); } else { $talkLink = Linker::link( $wgUser->getTalkPage(), $wgLang->getNsText( NS_TALK ) ); + $talkLink = wfMessage( 'parentheses' )->rawParams( $talkLink )->escaped(); $ret .= Linker::link( $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) ); - $ret .= " ($talkLink)<br />"; + $ret .= " $talkLink<br />"; $ret .= $wgLang->pipeList( array( Linker::link( - SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ), + SpecialPage::getTitleFor( 'Userlogout' ), wfMessage( 'logout' )->text(), array(), array( 'returnto' => $returnTo->getPrefixedDBkey() ) ), Linker::specialLink( 'Preferences' ), @@ -848,13 +872,11 @@ class LegacyTemplate extends BaseTemplate { $ret = $wgLang->pipeList( array( $ret, Linker::link( - Title::newFromText( wfMsgForContent( 'helppage' ) ), - wfMsg( 'help' ) + Title::newFromText( wfMessage( 'helppage' )->inContentLanguage()->text() ), + wfMessage( 'help' )->text() ), ) ); return $ret; } - } - diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index 2dd00980..bda43957 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -1,6 +1,6 @@ <?php /** - * Base class for template-based skins + * Base class for template-based skins. * * 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 @@ -20,10 +20,6 @@ * @file */ -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} - /** * Wrapper object for MediaWiki's localization functions, * to be passed to the template engine. @@ -44,7 +40,7 @@ class MediaWiki_I18N { // Hack for i18n:attributes in PHPTAL 1.0.0 dev version as of 2004-10-23 $value = preg_replace( '/^string:/', '', $value ); - $value = wfMsg( $value ); + $value = wfMessage( $value )->text(); // interpolate variables $m = array(); while( preg_match( '/\$([0-9]*?)/sm', $value, $m ) ) { @@ -95,7 +91,7 @@ class SkinTemplate extends Skin { var $template = 'QuickTemplate'; /** - * Whether this skin use OutputPage::headElement() to generate the <head> + * Whether this skin use OutputPage::headElement() to generate the "<head>" * tag */ var $useHeadElement = false; @@ -139,7 +135,6 @@ class SkinTemplate extends Skin { global $wgDisableCounters, $wgSitename, $wgLogo, $wgHideInterlanguageLinks; global $wgMaxCredits, $wgShowCreditsIfMax; global $wgPageShowWatchingUsers; - global $wgDebugComments; global $wgArticlePath, $wgScriptPath, $wgServer; wfProfileIn( __METHOD__ ); @@ -216,7 +211,7 @@ class SkinTemplate extends Skin { $tpl->setRef( 'thispage', $this->thispage ); $tpl->setRef( 'titleprefixeddbkey', $this->thispage ); $tpl->set( 'titletext', $title->getText() ); - $tpl->set( 'articleid', $title->getArticleId() ); + $tpl->set( 'articleid', $title->getArticleID() ); $tpl->set( 'isarticle', $out->isArticle() ); @@ -262,7 +257,7 @@ class SkinTemplate extends Skin { /* XXX currently unused, might get useful later $tpl->set( 'editable', ( !$title->isSpecialPage() ) ); $tpl->set( 'exists', $title->getArticleID() != 0 ); - $tpl->set( 'watch', $title->userIsWatching() ? 'unwatch' : 'watch' ); + $tpl->set( 'watch', $user->isWatched( $title ) ? 'unwatch' : 'watch' ); $tpl->set( 'protect', count( $title->isProtected() ) ? 'unprotect' : 'protect' ); $tpl->set( 'helppage', $this->msg( 'helppage' )->text() ); */ @@ -276,20 +271,20 @@ class SkinTemplate extends Skin { $tpl->setRef( 'logopath', $wgLogo ); $tpl->setRef( 'sitename', $wgSitename ); - $lang = $this->getLanguage(); - $userlang = $lang->getHtmlCode(); - $userdir = $lang->getDir(); + $userLang = $this->getLanguage(); + $userLangCode = $userLang->getHtmlCode(); + $userLangDir = $userLang->getDir(); - $tpl->set( 'lang', $userlang ); - $tpl->set( 'dir', $userdir ); - $tpl->set( 'rtl', $lang->isRTL() ); + $tpl->set( 'lang', $userLangCode ); + $tpl->set( 'dir', $userLangDir ); + $tpl->set( 'rtl', $userLang->isRTL() ); - $tpl->set( 'capitalizeallnouns', $lang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' ); + $tpl->set( 'capitalizeallnouns', $userLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' ); $tpl->set( 'showjumplinks', $user->getOption( 'showjumplinks' ) ); $tpl->set( 'username', $this->loggedin ? $this->username : null ); $tpl->setRef( 'userpage', $this->userpage ); $tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] ); - $tpl->set( 'userlang', $userlang ); + $tpl->set( 'userlang', $userLangCode ); // Users can have their language set differently than the // content of the wiki. For these users, tell the web browser @@ -297,9 +292,9 @@ class SkinTemplate extends Skin { $tpl->set( 'userlangattributes', '' ); $tpl->set( 'specialpageattributes', '' ); # obsolete - if ( $userlang !== $wgContLang->getHtmlCode() || $userdir !== $wgContLang->getDir() ) { - $escUserlang = htmlspecialchars( $userlang ); - $escUserdir = htmlspecialchars( $userdir ); + if ( $userLangCode !== $wgContLang->getHtmlCode() || $userLangDir !== $wgContLang->getDir() ) { + $escUserlang = htmlspecialchars( $userLangCode ); + $escUserdir = htmlspecialchars( $userLangDir ); // Attributes must be in double quotes because htmlspecialchars() doesn't // escape single quotes $attrs = " lang=\"$escUserlang\" dir=\"$escUserdir\""; @@ -326,13 +321,13 @@ class SkinTemplate extends Skin { } } - if( $wgPageShowWatchingUsers ) { + if ( $wgPageShowWatchingUsers ) { $dbr = wfGetDB( DB_SLAVE ); $num = $dbr->selectField( 'watchlist', 'COUNT(*)', array( 'wl_title' => $title->getDBkey(), 'wl_namespace' => $title->getNamespace() ), __METHOD__ ); - if( $num > 0 ) { + if ( $num > 0 ) { $tpl->set( 'numberofwatchingusers', $this->msg( 'number_of_watching_users_pageview' )->numParams( $num )->parse() ); @@ -391,12 +386,6 @@ class SkinTemplate extends Skin { } } - if ( $wgDebugComments ) { - $tpl->setRef( 'debug', $out->mDebugtext ); - } else { - $tpl->set( 'debug', '' ); - } - $tpl->set( 'sitenotice', $this->getSiteNotice() ); $tpl->set( 'bottomscripts', $this->bottomScripts() ); $tpl->set( 'printfooter', $this->printSource() ); @@ -408,10 +397,10 @@ class SkinTemplate extends Skin { # when the content is different from the UI language, i.e.: # not for special pages or file pages AND only when viewing AND if the page exists # (or is in MW namespace, because that has default content) - if( !in_array( $title->getNamespace(), array( NS_SPECIAL, NS_FILE ) ) && + if ( !in_array( $title->getNamespace(), array( NS_SPECIAL, NS_FILE ) ) && in_array( $request->getVal( 'action', 'view' ), array( 'view', 'historysubmit' ) ) && ( $title->exists() || $title->getNamespace() == NS_MEDIAWIKI ) ) { - $pageLang = $title->getPageLanguage(); + $pageLang = $title->getPageViewLanguage(); $realBodyAttribs['lang'] = $pageLang->getHtmlCode(); $realBodyAttribs['dir'] = $pageLang->getDir(); $realBodyAttribs['class'] = 'mw-content-'.$pageLang->getDir(); @@ -430,10 +419,15 @@ class SkinTemplate extends Skin { unset( $tmp ); $nt = Title::newFromText( $l ); if ( $nt ) { + $ilLangName = Language::fetchLanguageName( $nt->getInterwiki() ); + if ( strval( $ilLangName ) === '' ) { + $ilLangName = $l; + } else { + $ilLangName = $this->getLanguage()->ucfirst( $ilLangName ); + } $language_urls[] = array( 'href' => $nt->getFullURL(), - 'text' => ( $wgContLang->getLanguageName( $nt->getInterwiki() ) != '' ? - $wgContLang->getLanguageName( $nt->getInterwiki() ) : $l ), + 'text' => $ilLangName, 'title' => $nt->getText(), 'class' => $class, 'lang' => $nt->getInterwiki(), @@ -442,7 +436,7 @@ class SkinTemplate extends Skin { } } } - if( count( $language_urls ) ) { + if ( count( $language_urls ) ) { $tpl->setRef( 'language_urls', $language_urls ); } else { $tpl->set( 'language_urls', false ); @@ -467,6 +461,7 @@ class SkinTemplate extends Skin { $tpl->set( 'headscripts', $out->getHeadScripts() . $out->getHeadItems() ); } + $tpl->set( 'debug', '' ); $tpl->set( 'debughtml', $this->generateDebugHTML() ); $tpl->set( 'reporttime', wfReportTime() ); @@ -522,6 +517,7 @@ class SkinTemplate extends Skin { * This is setup as a method so that like with $wgLogo and getLogo() a skin * can override this setting and always output one or the other if it has * a reason it can't output one of the two modes. + * @return bool */ function useCombinedLoginLink() { global $wgUseCombinedLoginLink; @@ -561,7 +557,8 @@ class SkinTemplate extends Skin { 'text' => $this->username, 'href' => &$this->userpageUrlDetails['href'], 'class' => $this->userpageUrlDetails['exists'] ? false : 'new', - 'active' => ( $this->userpageUrlDetails['href'] == $pageurl ) + 'active' => ( $this->userpageUrlDetails['href'] == $pageurl ), + 'dir' => 'auto' ); $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage ); $personal_urls['mytalk'] = array( @@ -584,10 +581,12 @@ class SkinTemplate extends Skin { ); # We need to do an explicit check for Special:Contributions, as we - # have to match both the title, and the target (which could come - # from request values or be specified in "sub page" form. The plot + # have to match both the title, and the target, which could come + # from request values (Special:Contributions?target=Jimbo_Wales) + # or be specified in "sub page" form + # (Special:Contributions/Jimbo_Wales). The plot # thickens, because the Title object is altered for special pages, - # so doesn't contain the original alias-with-subpage. + # so it doesn't contain the original alias-with-subpage. $origTitle = Title::newFromText( $request->getText( 'title' ) ); if( $origTitle instanceof Title && $origTitle->isSpecialPage() ) { list( $spName, $spPar ) = SpecialPageFactory::resolveAlias( $origTitle->getText() ); @@ -618,37 +617,25 @@ class SkinTemplate extends Skin { $loginlink = $this->getUser()->isAllowed( 'createaccount' ) && $useCombinedLoginLink ? 'nav-login-createaccount' : 'login'; - $is_signup = $request->getText('type') == "signup"; + $is_signup = $request->getText( 'type' ) == 'signup'; # anonlogin & login are the same + global $wgSecureLogin; + $proto = $wgSecureLogin ? PROTO_HTTPS : null; + + $login_id = $this->showIPinHeader() ? 'anonlogin' : 'login'; $login_url = array( 'text' => $this->msg( $loginlink )->text(), - 'href' => self::makeSpecialUrl( 'Userlogin', $returnto ), - 'active' => $title->isSpecial( 'Userlogin' ) && ( $loginlink == "nav-login-createaccount" || !$is_signup ) + 'href' => self::makeSpecialUrl( 'Userlogin', $returnto, $proto ), + 'active' => $title->isSpecial( 'Userlogin' ) && ( $loginlink == 'nav-login-createaccount' || !$is_signup ), + 'class' => $wgSecureLogin ? 'link-https' : '' + ); + $createaccount_url = array( + 'text' => $this->msg( 'createaccount' )->text(), + 'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup", $proto ), + 'active' => $title->isSpecial( 'Userlogin' ) && $is_signup, + 'class' => $wgSecureLogin ? 'link-https' : '' ); - if ( $this->getUser()->isAllowed( 'createaccount' ) && !$useCombinedLoginLink ) { - $createaccount_url = array( - 'text' => $this->msg( 'createaccount' )->text(), - 'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ), - 'active' => $title->isSpecial( 'Userlogin' ) && $is_signup - ); - } - global $wgServer, $wgSecureLogin; - if( substr( $wgServer, 0, 5 ) === 'http:' && $wgSecureLogin ) { - $title = SpecialPage::getTitleFor( 'Userlogin' ); - $https_url = preg_replace( '/^http:/', 'https:', $title->getFullURL() ); - $login_url['href'] = $https_url; - # @todo FIXME: Class depends on skin - $login_url['class'] = 'link-https'; - if ( isset($createaccount_url) ) { - $https_url = preg_replace( '/^http:/', 'https:', - $title->getFullURL("type=signup") ); - $createaccount_url['href'] = $https_url; - # @todo FIXME: Class depends on skin - $createaccount_url['class'] = 'link-https'; - } - } - if( $this->showIPinHeader() ) { $href = &$this->userpageUrlDetails['href']; @@ -666,13 +653,13 @@ class SkinTemplate extends Skin { 'class' => $usertalkUrlDetails['exists'] ? false : 'new', 'active' => ( $pageurl == $href ) ); - $personal_urls['anonlogin'] = $login_url; - } else { - $personal_urls['login'] = $login_url; } - if ( isset($createaccount_url) ) { + + if ( $this->getUser()->isAllowed( 'createaccount' ) && !$useCombinedLoginLink ) { $personal_urls['createaccount'] = $createaccount_url; } + + $personal_urls[$login_id] = $login_url; } wfRunHooks( 'PersonalUrls', array( &$personal_urls, &$title ) ); @@ -702,9 +689,9 @@ class SkinTemplate extends Skin { // wfMessageFallback will nicely accept $message as an array of fallbacks // or just a single key $msg = wfMessageFallback( $message )->setContext( $this->getContext() ); - if ( is_array($message) ) { + if ( is_array( $message ) ) { // for hook compatibility just keep the last message name - $message = end($message); + $message = end( $message ); } if ( $msg->exists() ) { $text = $msg->text(); @@ -789,8 +776,9 @@ class SkinTemplate extends Skin { wfProfileIn( __METHOD__ ); - $title = $this->getRelevantTitle(); // Display tabs for the relevant title rather than always the title itself - $onPage = $title->equals($this->getTitle()); + // Display tabs for the relevant title rather than always the title itself + $title = $this->getRelevantTitle(); + $onPage = $title->equals( $this->getTitle() ); $out = $this->getOutput(); $request = $this->getRequest(); @@ -834,7 +822,7 @@ class SkinTemplate extends Skin { // Adds namespace links $subjectMsg = array( "nstab-$subjectId" ); if ( $subjectPage->isMainPage() ) { - array_unshift($subjectMsg, 'mainpage-nstab'); + array_unshift( $subjectMsg, 'mainpage-nstab' ); } $content_navigation['namespaces'][$subjectId] = $this->tabAction( $subjectPage, $subjectMsg, !$isTalk && !$preventActiveTabs, '', $userCanRead @@ -851,9 +839,10 @@ class SkinTemplate extends Skin { $content_navigation['views']['view'] = $this->tabAction( $isTalk ? $talkPage : $subjectPage, array( "$skname-view-view", 'view' ), - ( $onPage && ($action == 'view' || $action == 'purge' ) ), '', true + ( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true ); - $content_navigation['views']['view']['redundant'] = true; // signal to hide this from simple content_actions + // signal to hide this from simple content_actions + $content_navigation['views']['view']['redundant'] = true; } wfProfileIn( __METHOD__ . '-edit' ); @@ -871,14 +860,14 @@ class SkinTemplate extends Skin { $section = $request->getVal( 'section' ); $msgKey = $title->exists() || ( $title->getNamespace() == NS_MEDIAWIKI && $title->getDefaultMessageText() !== false ) ? - "edit" : "create"; + 'edit' : 'create'; $content_navigation['views']['edit'] = array( 'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection ) ? 'selected' : '' ) . $isTalkClass, 'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey )->setContext( $this->getContext() )->text(), 'href' => $title->getLocalURL( $this->editUrlOptions() ), 'primary' => true, // don't collapse this in vector ); - + // section link if ( $showNewSection ) { // Adds new section link @@ -932,7 +921,7 @@ class SkinTemplate extends Skin { // article doesn't exist or is deleted if ( $user->isAllowed( 'deletedhistory' ) ) { $n = $title->isDeleted(); - if( $n ) { + if ( $n ) { $undelTitle = SpecialPage::getTitleFor( 'Undelete' ); // If the user can't undelete but can view deleted history show them a "View .. deleted" tab instead $msgKey = $user->isAllowed( 'undelete' ) ? 'undelete' : 'viewdeleted'; @@ -968,11 +957,12 @@ class SkinTemplate extends Skin { * a change to that procedure these messages will have to remain as * the global versions. */ - $mode = $title->userIsWatching() ? 'unwatch' : 'watch'; + $mode = $user->isWatched( $title ) ? 'unwatch' : 'watch'; $token = WatchAction::getWatchToken( $title, $user, $mode ); $content_navigation['actions'][$mode] = array( 'class' => $onPage && ( $action == 'watch' || $action == 'unwatch' ) ? 'selected' : false, - 'text' => $this->msg( $mode )->text(), // uses 'watch' or 'unwatch' message + // uses 'watch' or 'unwatch' message + 'text' => $this->msg( $mode )->text(), 'href' => $title->getLocalURL( array( 'action' => $mode, 'token' => $token ) ) ); } @@ -986,8 +976,8 @@ class SkinTemplate extends Skin { $variants = $pageLang->getVariants(); // Checks that language conversion is enabled and variants exist // And if it is not in the special namespace - if( count( $variants ) > 1 ) { - // Gets preferred variant (note that user preference is + if ( count( $variants ) > 1 ) { + // Gets preferred variant (note that user preference is // only possible for wiki content language variant) $preferred = $pageLang->getPreferredVariant(); // Loops over each variant @@ -1003,7 +993,9 @@ class SkinTemplate extends Skin { $content_navigation['variants'][] = array( 'class' => ( $code == $preferred ) ? 'selected' : false, 'text' => $varname, - 'href' => $title->getLocalURL( array( 'variant' => $code ) ) + 'href' => $title->getLocalURL( array( 'variant' => $code ) ), + 'lang' => $code, + 'hreflang' => $code ); } } @@ -1013,7 +1005,7 @@ class SkinTemplate extends Skin { $content_navigation['namespaces']['special'] = array( 'class' => 'selected', 'text' => $this->msg( 'nstab-special' )->text(), - 'href' => $request->getRequestURL(), // @bug 2457, 2510 + 'href' => $request->getRequestURL(), // @see: bug 2457, bug 2510 'context' => 'subject' ); @@ -1032,7 +1024,7 @@ class SkinTemplate extends Skin { $xmlID = 'ca-nstab-' . $xmlID; } elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) { $xmlID = 'ca-talk'; - } elseif ( $section == "variants" ) { + } elseif ( $section == 'variants' ) { $xmlID = 'ca-varlang-' . $xmlID; } else { $xmlID = 'ca-' . $xmlID; @@ -1047,14 +1039,14 @@ class SkinTemplate extends Skin { # give the edit tab an accesskey, because that's fairly su- # perfluous and conflicts with an accesskey (Ctrl-E) often # used for editing in Safari. - if( in_array( $action, array( 'edit', 'submit' ) ) ) { - if ( isset($content_navigation['views']['edit']) ) { + if ( in_array( $action, array( 'edit', 'submit' ) ) ) { + if ( isset( $content_navigation['views']['edit'] ) ) { $content_navigation['views']['edit']['tooltiponly'] = true; } - if ( isset($content_navigation['actions']['watch']) ) { + if ( isset( $content_navigation['actions']['watch'] ) ) { $content_navigation['actions']['watch']['tooltiponly'] = true; } - if ( isset($content_navigation['actions']['unwatch']) ) { + if ( isset( $content_navigation['actions']['unwatch'] ) ) { $content_navigation['actions']['unwatch']['tooltiponly'] = true; } } @@ -1083,7 +1075,7 @@ class SkinTemplate extends Skin { foreach ( $links as $key => $value ) { - if ( isset($value["redundant"]) && $value["redundant"] ) { + if ( isset( $value['redundant'] ) && $value['redundant'] ) { // Redundant tabs are dropped from content_actions continue; } @@ -1092,11 +1084,11 @@ class SkinTemplate extends Skin { // so the xmlID based id is much closer to the actual $key that we want // for that reason we'll just strip out the ca- if present and use // the latter potion of the "id" as the $key - if ( isset($value["id"]) && substr($value["id"], 0, 3) == "ca-" ) { - $key = substr($value["id"], 3); + if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) { + $key = substr( $value['id'], 3 ); } - if ( isset($content_actions[$key]) ) { + if ( isset( $content_actions[$key] ) ) { wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening content_navigation into content_actions." ); continue; } @@ -1147,7 +1139,7 @@ class SkinTemplate extends Skin { // A print stylesheet is attached to all pages, but nobody ever // figures that out. :) Add a link... - if( $out->isArticle() ) { + if ( $out->isArticle() ) { if ( !$out->isPrintable() ) { $nav_urls['print'] = array( 'text' => $this->msg( 'printableversion' )->text(), @@ -1161,7 +1153,7 @@ class SkinTemplate extends Skin { if ( $revid ) { $nav_urls['permalink'] = array( 'text' => $this->msg( 'permalink' )->text(), - 'href' => $out->getTitle()->getLocalURL( "oldid=$revid" ) + 'href' => $this->getTitle()->getLocalURL( "oldid=$revid" ) ); } @@ -1174,7 +1166,7 @@ class SkinTemplate extends Skin { $nav_urls['whatlinkshere'] = array( 'href' => SpecialPage::getTitleFor( 'Whatlinkshere', $this->thispage )->getLocalUrl() ); - if ( $this->getTitle()->getArticleId() ) { + if ( $this->getTitle()->getArticleID() ) { $nav_urls['recentchangeslinked'] = array( 'href' => SpecialPage::getTitleFor( 'Recentchangeslinked', $this->thispage )->getLocalUrl() ); @@ -1189,12 +1181,9 @@ class SkinTemplate extends Skin { 'href' => self::makeSpecialUrlSubpage( 'Contributions', $rootUser ) ); - if ( $user->isLoggedIn() ) { - $logPage = SpecialPage::getTitleFor( 'Log' ); - $nav_urls['log'] = array( - 'href' => $logPage->getLocalUrl( array( 'user' => $rootUser ) ) - ); - } + $nav_urls['log'] = array( + 'href' => self::makeSpecialUrlSubpage( 'Log', $rootUser ) + ); if ( $this->getUser()->isAllowed( 'block' ) ) { $nav_urls['blockip'] = array( @@ -1319,6 +1308,7 @@ abstract class QuickTemplate { /** * @private + * @return bool */ function haveData( $str ) { return isset( $this->data[$str] ); @@ -1354,7 +1344,7 @@ abstract class BaseTemplate extends QuickTemplate { /** * Get a Message object with its context set * - * @param $name Str message name + * @param $name string message name * @return Message */ public function getMsg( $name ) { @@ -1378,6 +1368,7 @@ abstract class BaseTemplate extends QuickTemplate { * stored by SkinTemplate. * The resulting array is built acording to a format intended to be passed * through makeListItem to generate the html. + * @return array */ function getToolbox() { wfProfileIn( __METHOD__ ); @@ -1411,12 +1402,13 @@ abstract class BaseTemplate extends QuickTemplate { } if ( isset( $this->data['nav_urls']['print'] ) && $this->data['nav_urls']['print'] ) { $toolbox['print'] = $this->data['nav_urls']['print']; + $toolbox['print']['id'] = 't-print'; $toolbox['print']['rel'] = 'alternate'; $toolbox['print']['msg'] = 'printableversion'; } if ( isset( $this->data['nav_urls']['permalink'] ) && $this->data['nav_urls']['permalink'] ) { $toolbox['permalink'] = $this->data['nav_urls']['permalink']; - if( $toolbox['permalink']['href'] === '' ) { + if ( $toolbox['permalink']['href'] === '' ) { unset( $toolbox['permalink']['href'] ); $toolbox['ispermalink']['tooltiponly'] = true; $toolbox['ispermalink']['id'] = 't-ispermalink'; @@ -1438,23 +1430,28 @@ abstract class BaseTemplate extends QuickTemplate { * This is in reality the same list as already stored in personal_urls * however it is reformatted so that you can just pass the individual items * to makeListItem instead of hardcoding the element creation boilerplate. + * @return array */ function getPersonalTools() { $personal_tools = array(); - foreach( $this->data['personal_urls'] as $key => $ptool ) { + foreach ( $this->data['personal_urls'] as $key => $plink ) { # The class on a personal_urls item is meant to go on the <a> instead # of the <li> so we have to use a single item "links" array instead - # of using most of the personal_url's keys directly - $personal_tools[$key] = array(); - $personal_tools[$key]["links"][] = array(); - $personal_tools[$key]["links"][0]["single-id"] = $personal_tools[$key]["id"] = "pt-$key"; - if ( isset($ptool["active"]) ) { - $personal_tools[$key]["active"] = $ptool["active"]; + # of using most of the personal_url's keys directly. + $ptool = array( + 'links' => array( + array( 'single-id' => "pt-$key" ), + ), + 'id' => "pt-$key", + ); + if ( isset( $plink['active'] ) ) { + $ptool['active'] = $plink['active']; } - foreach ( array("href", "class", "text") as $k ) { - if ( isset($ptool[$k]) ) - $personal_tools[$key]["links"][0][$k] = $ptool[$k]; + foreach ( array( 'href', 'class', 'text' ) as $k ) { + if ( isset( $plink[$k] ) ) + $ptool['links'][0][$k] = $plink[$k]; } + $personal_tools[$key] = $ptool; } return $personal_tools; } @@ -1471,7 +1468,7 @@ abstract class BaseTemplate extends QuickTemplate { if ( !isset( $sidebar['LANGUAGES'] ) ) { $sidebar['LANGUAGES'] = true; } - + if ( !isset( $options['search'] ) || $options['search'] !== true ) { unset( $sidebar['SEARCH'] ); } @@ -1481,7 +1478,7 @@ abstract class BaseTemplate extends QuickTemplate { if ( isset( $options['languages'] ) && $options['languages'] === false ) { unset( $sidebar['LANGUAGES'] ); } - + $boxes = array(); foreach ( $sidebar as $boxName => $content ) { if ( $content === false ) { @@ -1491,7 +1488,7 @@ abstract class BaseTemplate extends QuickTemplate { case 'SEARCH': // Search is a special case, skins should custom implement this $boxes[$boxName] = array( - 'id' => "p-search", + 'id' => 'p-search', 'header' => $this->getMsg( 'search' )->text(), 'generated' => false, 'content' => true, @@ -1500,7 +1497,7 @@ abstract class BaseTemplate extends QuickTemplate { case 'TOOLBOX': $msgObj = $this->getMsg( 'toolbox' ); $boxes[$boxName] = array( - 'id' => "p-tb", + 'id' => 'p-tb', 'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox', 'generated' => false, 'content' => $this->getToolbox(), @@ -1510,12 +1507,12 @@ abstract class BaseTemplate extends QuickTemplate { if ( $this->data['language_urls'] ) { $msgObj = $this->getMsg( 'otherlanguages' ); $boxes[$boxName] = array( - 'id' => "p-lang", + 'id' => 'p-lang', 'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages', 'generated' => false, 'content' => $this->data['language_urls'], ); - } + } break; default: $msgObj = $this->getMsg( $boxName ); @@ -1528,7 +1525,7 @@ abstract class BaseTemplate extends QuickTemplate { break; } } - + // HACK: Compatibility with extensions still using SkinTemplateToolboxEnd $hookContents = null; if ( isset( $boxes['TOOLBOX'] ) ) { @@ -1543,17 +1540,17 @@ abstract class BaseTemplate extends QuickTemplate { } } // END hack - + if ( isset( $options['htmlOnly'] ) && $options['htmlOnly'] === true ) { foreach ( $boxes as $boxName => $box ) { if ( is_array( $box['content'] ) ) { - $content = "<ul>"; + $content = '<ul>'; foreach ( $box['content'] as $key => $val ) { $content .= "\n " . $this->makeListItem( $key, $val ); } // HACK, shove the toolbox end onto the toolbox if we're rendering itself if ( $hookContents ) { - $content .= "\n $hookContents"; + $content .= "\n $hookContents"; } // END hack $content .= "\n</ul>\n"; @@ -1563,7 +1560,7 @@ abstract class BaseTemplate extends QuickTemplate { } else { if ( $hookContents ) { $boxes['TOOLBOXEND'] = array( - 'id' => "p-toolboxend", + 'id' => 'p-toolboxend', 'header' => $boxes['TOOLBOX']['header'], 'generated' => false, 'content' => "<ul>{$hookContents}</ul>", @@ -1583,7 +1580,7 @@ abstract class BaseTemplate extends QuickTemplate { // END hack } } - + return $boxes; } @@ -1591,26 +1588,40 @@ abstract class BaseTemplate extends QuickTemplate { * Makes a link, usually used by makeListItem to generate a link for an item * in a list used in navigation lists, portlets, portals, sidebars, etc... * - * $key is a string, usually a key from the list you are generating this link from - * $item is an array containing some of a specific set of keys. - * The text of the link will be generated either from the contents of the "text" - * key in the $item array, if a "msg" key is present a message by that name will - * be used, and if neither of those are set the $key will be used as a message name. + * @param $key string usually a key from the list you are generating this + * link from. + * @param $item array contains some of a specific set of keys. + * + * The text of the link will be generated either from the contents of the + * "text" key in the $item array, if a "msg" key is present a message by + * that name will be used, and if neither of those are set the $key will be + * used as a message name. + * * If a "href" key is not present makeLink will just output htmlescaped text. - * The href, id, class, rel, and type keys are used as attributes for the link if present. - * If an "id" or "single-id" (if you don't want the actual id to be output on the link) - * is present it will be used to generate a tooltip and accesskey for the link. + * The "href", "id", "class", "rel", and "type" keys are used as attributes + * for the link if present. + * + * If an "id" or "single-id" (if you don't want the actual id to be output + * on the link) is present it will be used to generate a tooltip and + * accesskey for the link. + * * If you don't want an accesskey, set $item['tooltiponly'] = true; - * $options can be used to affect the output of a link: - * You can use a text-wrapper key to specify a list of elements to wrap the - * text of a link in. This should be an array of arrays containing a 'tag' and - * optionally an 'attributes' key. If you only have one element you don't need - * to wrap it in another array. eg: To use <a><span>...</span></a> in all links - * use array( 'text-wrapper' => array( 'tag' => 'span' ) ) for your options. - * A link-class key can be used to specify additional classes to apply to all links. - * A link-fallback can be used to specify a tag to use instead of <a> if there is - * no link. eg: If you specify 'link-fallback' => 'span' than any non-link will - * output a <span> instead of just text. + * + * @param $options array can be used to affect the output of a link. + * Possible options are: + * - 'text-wrapper' key to specify a list of elements to wrap the text of + * a link in. This should be an array of arrays containing a 'tag' and + * optionally an 'attributes' key. If you only have one element you don't + * need to wrap it in another array. eg: To use <a><span>...</span></a> + * in all links use array( 'text-wrapper' => array( 'tag' => 'span' ) ) + * for your options. + * - 'link-class' key can be used to specify additional classes to apply + * to all links. + * - 'link-fallback' can be used to specify a tag to use instead of "<a>" + * if there is no link. eg: If you specify 'link-fallback' => 'span' than + * any non-link will output a "<span>" instead of just text. + * + * @return string */ function makeLink( $key, $item, $options = array() ) { if ( isset( $item['text'] ) ) { @@ -1671,17 +1682,22 @@ abstract class BaseTemplate extends QuickTemplate { } /** - * Generates a list item for a navigation, portlet, portal, sidebar... etc list - * $key is a string, usually a key from the list you are generating this link from - * $item is an array of list item data containing some of a specific set of keys. + * Generates a list item for a navigation, portlet, portal, sidebar... list + * + * @param $key string, usually a key from the list you are generating this link from. + * @param $item array, of list item data containing some of a specific set of keys. * The "id" and "class" keys will be used as attributes for the list item, * if "active" contains a value of true a "active" class will also be appended to class. - * If you want something other than a <li> you can pass a tag name such as + * + * @param $options array + * + * If you want something other than a "<li>" you can pass a tag name such as * "tag" => "span" in the $options array to change the tag used. * link/content data for the list item may come in one of two forms * A "links" key may be used, in which case it should contain an array with - * a list of links to include inside the list item, see makeLink for the format - * of individual links array items. + * a list of links to include inside the list item, see makeLink for the + * format of individual links array items. + * * Otherwise the relevant keys from the list item $item array will be passed * to makeLink instead. Note however that "id" and "class" are used by the * list item directly so they will not be passed to makeLink @@ -1689,6 +1705,8 @@ abstract class BaseTemplate extends QuickTemplate { * If you need an id or class on a single link you should include a "links" * array with just one link item inside of it. * $options is also passed on to makeLink calls + * + * @return string */ function makeListItem( $key, $item, $options = array() ) { if ( isset( $item['links'] ) ) { @@ -1783,6 +1801,7 @@ abstract class BaseTemplate extends QuickTemplate { * If you pass "flat" as an option then the returned array will be a flat array * of footer icons instead of a key/value array of footerlinks arrays broken * up into categories. + * @return array|mixed */ function getFooterLinks( $option = null ) { $footerlinks = $this->data['footerlinks']; @@ -1821,6 +1840,7 @@ abstract class BaseTemplate extends QuickTemplate { * in the list of footer icons. This is mostly useful for skins which only * display the text from footericons instead of the images and don't want a * duplicate copyright statement because footerlinks already rendered one. + * @return */ function getFooterIcons( $option = null ) { // Generate additional footer icons @@ -1857,15 +1877,9 @@ abstract class BaseTemplate extends QuickTemplate { * body and html tags. */ function printTrail() { ?> -<?php $this->html('bottomscripts'); /* JS call to runBodyOnloadHook */ ?> -<?php $this->html('reporttime') ?> -<?php if ( $this->data['debug'] ): ?> -<!-- Debug output: -<?php $this->text( 'debug' ); ?> - ---> -<?php endif; +<?php $this->html( 'bottomscripts' ); /* JS call to runBodyOnloadHook */ ?> +<?php $this->html( 'reporttime' ) ?> +<?php echo MWDebug::getDebugHTML( $this->getSkin()->getContext() ); } } - diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 17a36ecf..2e5e02b0 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -1,25 +1,24 @@ <?php /** - * SpecialPage: handling special pages and lists thereof. + * Parent class for all special pages. * - * To add a special page in an extension, add to $wgSpecialPages either - * an object instance or an array containing the name and constructor - * parameters. The latter is preferred for performance reasons. + * 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. * - * The object instantiated must be either an instance of SpecialPage or a - * sub-class thereof. It must have an execute() method, which sends the HTML - * for the special page to $wgOut. The parent class has an execute() method - * which distributes the call to the historical global functions. Additionally, - * execute() also checks if the user has the necessary access privileges - * and bails out if not. + * 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. * - * To add a core special page, use the similar static list in - * SpecialPage::$mList. To remove a core static special page at runtime, use - * a SpecialPage_initList hook. + * 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 SpecialPage - * @defgroup SpecialPage SpecialPage */ /** @@ -124,19 +123,18 @@ class SpecialPage { * * @param $page Mixed: SpecialPage or string * @param $group String - * @return null * @deprecated since 1.18 call SpecialPageFactory method directly */ static function setGroup( $page, $group ) { wfDeprecated( __METHOD__, '1.18' ); - return SpecialPageFactory::setGroup( $page, $group ); + SpecialPageFactory::setGroup( $page, $group ); } /** * Get the group that the special page belongs in on Special:SpecialPage * * @param $page SpecialPage - * @return null + * @return string * @deprecated since 1.18 call SpecialPageFactory method directly */ static function getGroup( &$page ) { @@ -200,7 +198,7 @@ class SpecialPage { * * @param $user User object to check permissions, $wgUser will be used * if not provided - * @return Associative array mapping page's name to its SpecialPage object + * @return array Associative array mapping page's name to its SpecialPage object * @deprecated since 1.18 call SpecialPageFactory method directly */ static function getUsablePages( User $user = null ) { @@ -211,7 +209,7 @@ class SpecialPage { /** * Return categorised listable special pages for all users * - * @return Associative array mapping page's name to its SpecialPage object + * @return array Associative array mapping page's name to its SpecialPage object * @deprecated since 1.18 call SpecialPageFactory method directly */ static function getRegularPages() { @@ -223,7 +221,7 @@ class SpecialPage { * Return categorised listable special pages which are available * for the current user, but not for everyone * - * @return Associative array mapping page's name to its SpecialPage object + * @return array Associative array mapping page's name to its SpecialPage object * @deprecated since 1.18 call SpecialPageFactory method directly */ static function getRestrictedPages() { @@ -353,7 +351,7 @@ class SpecialPage { $this->mFunction = $function; } if ( $file === 'default' ) { - $this->mFile = dirname( __FILE__ ) . "/specials/Special$name.php"; + $this->mFile = __DIR__ . "/specials/Special$name.php"; } else { $this->mFile = $file; } @@ -592,14 +590,69 @@ class SpecialPage { } /** + * Entry point. + * + * @since 1.20 + * + * @param $subPage string|null + */ + public final function run( $subPage ) { + /** + * Gets called before @see SpecialPage::execute. + * + * @since 1.20 + * + * @param $special SpecialPage + * @param $subPage string|null + */ + wfRunHooks( 'SpecialPageBeforeExecute', array( $this, $subPage ) ); + + $this->beforeExecute( $subPage ); + $this->execute( $subPage ); + $this->afterExecute( $subPage ); + + /** + * Gets called after @see SpecialPage::execute. + * + * @since 1.20 + * + * @param $special SpecialPage + * @param $subPage string|null + */ + wfRunHooks( 'SpecialPageAfterExecute', array( $this, $subPage ) ); + } + + /** + * Gets called before @see SpecialPage::execute. + * + * @since 1.20 + * + * @param $subPage string|null + */ + protected function beforeExecute( $subPage ) { + // No-op + } + + /** + * Gets called after @see SpecialPage::execute. + * + * @since 1.20 + * + * @param $subPage string|null + */ + protected function afterExecute( $subPage ) { + // No-op + } + + /** * Default execute method * Checks user permissions, calls the function given in mFunction * * This must be overridden by subclasses; it will be made abstract in a future version * - * @param $par String subpage string, if one was specified + * @param $subPage string|null */ - function execute( $par ) { + public function execute( $subPage ) { $this->setHeaders(); $this->checkPermissions(); @@ -609,7 +662,7 @@ class SpecialPage { require_once( $this->mFile ); } $this->outputHeader(); - call_user_func( $func, $par, $this ); + call_user_func( $func, $subPage, $this ); } /** @@ -628,7 +681,7 @@ class SpecialPage { } else { $msg = $summaryMessageKey; } - if ( !$this->msg( $msg )->isBlank() && !$this->including() ) { + if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) { $this->getOutput()->wrapWikiMsg( "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg ); } @@ -768,7 +821,15 @@ class SpecialPage { // Works fine as the first parameter, which appears elsewhere in the // code base. Sighhhh. $args = func_get_args(); - return call_user_func_array( array( $this->getContext(), 'msg' ), $args ); + $message = call_user_func_array( array( $this->getContext(), 'msg' ), $args ); + // RequestContext passes context to wfMessage, and the language is set from + // the context, but setting the language for Message class removes the + // interface message status, which breaks for example usernameless gender + // invokations. Restore the flag when not including special page in content. + if ( $this->including() ) { + $message->setInterfaceMessageFlag( false ); + } + return $message; } /** @@ -891,7 +952,7 @@ abstract class FormSpecialPage extends SpecialPage { $this->checkPermissions(); if ( $this->requiresUnblock() && $user->isBlocked() ) { - $block = $user->mBlock; + $block = $user->getBlock(); throw new UserBlockedError( $block ); } @@ -988,7 +1049,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { * False otherwise. * * @param $par String Subpage string - * @return Title|false + * @return Title|bool */ abstract public function getRedirect( $par ); @@ -1076,16 +1137,102 @@ class SpecialCreateAccount extends SpecialRedirectToSpecial { */ /** + * Superclass for any RedirectSpecialPage which redirects the user + * to a particular article (as opposed to user contributions, logs, etc.). + * + * For security reasons these special pages are restricted to pass on + * the following subset of GET parameters to the target page while + * removing all others: + * + * - useskin, uselang, printable: to alter the appearance of the resulting page + * + * - redirect: allows viewing one's user page or talk page even if it is a + * redirect. + * + * - rdfrom: allows redirecting to one's user page or talk page from an + * external wiki with the "Redirect from..." notice. + * + * - limit, offset: Useful for linking to history of one's own user page or + * user talk page. For example, this would be a link to "the last edit to your + * user talk page in the year 2010": + * http://en.wikipedia.org/w/index.php?title=Special:MyPage&offset=20110000000000&limit=1&action=history + * + * - feed: would allow linking to the current user's RSS feed for their user + * talk page: + * http://en.wikipedia.org/w/index.php?title=Special:MyTalk&action=history&feed=rss + * + * - preloadtitle: Can be used to provide a default section title for a + * preloaded new comment on one's own talk page. + * + * - summary : Can be used to provide a default edit summary for a preloaded + * edit to one's own user page or talk page. + * + * - preview: Allows showing/hiding preview on first edit regardless of user + * preference, useful for preloaded edits where you know preview wouldn't be + * useful. + * + * - internaledit, externaledit, mode: Allows forcing the use of the + * internal/external editor, e.g. to force the internal editor for + * short/simple preloaded edits. + * + * - redlink: Affects the message the user sees if their talk page/user talk + * page does not currently exist. Avoids confusion for newbies with no user + * pages over why they got a "permission error" following this link: + * http://en.wikipedia.org/w/index.php?title=Special:MyPage&redlink=1 + * + * - debug: determines whether the debug parameter is passed to load.php, + * which disables reformatting and allows scripts to be debugged. Useful + * when debugging scripts that manipulate one's own user page or talk page. + * + * @par Hook extension: + * Extensions can add to the redirect parameters list by using the hook + * RedirectSpecialArticleRedirectParams + * + * This hook allows extensions which add GET parameters like FlaggedRevs to + * retain those parameters when redirecting using special pages. + * + * @par Hook extension example: + * @code + * $wgHooks['RedirectSpecialArticleRedirectParams'][] = + * 'MyExtensionHooks::onRedirectSpecialArticleRedirectParams'; + * public static function onRedirectSpecialArticleRedirectParams( &$redirectParams ) { + * $redirectParams[] = 'stable'; + * return true; + * } + * @endcode + * @ingroup SpecialPage + */ +abstract class RedirectSpecialArticle extends RedirectSpecialPage { + function __construct( $name ) { + parent::__construct( $name ); + $redirectParams = array( + 'action', + 'redirect', 'rdfrom', + # Options for preloaded edits + 'preload', 'editintro', 'preloadtitle', 'summary', + # Options for overriding user settings + 'preview', 'internaledit', 'externaledit', 'mode', + # Options for history/diffs + 'section', 'oldid', 'diff', 'dir', + 'limit', 'offset', 'feed', + # Misc options + 'redlink', 'debug', + # Options for action=raw; missing ctype can break JS or CSS in some browsers + 'ctype', 'maxage', 'smaxage', + ); + + wfRunHooks( "RedirectSpecialArticleRedirectParams", array(&$redirectParams) ); + $this->mAllowedRedirectParams = $redirectParams; + } +} + +/** * Shortcut to construct a special page pointing to current user user's page. * @ingroup SpecialPage */ -class SpecialMypage extends RedirectSpecialPage { +class SpecialMypage extends RedirectSpecialArticle { function __construct() { parent::__construct( 'Mypage' ); - $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro', - 'section', 'oldid', 'diff', 'dir', - // Options for action=raw; missing ctype can break JS or CSS in some browsers - 'ctype', 'maxage', 'smaxage' ); } function getRedirect( $subpage ) { @@ -1101,11 +1248,9 @@ class SpecialMypage extends RedirectSpecialPage { * Shortcut to construct a special page pointing to current user talk page. * @ingroup SpecialPage */ -class SpecialMytalk extends RedirectSpecialPage { +class SpecialMytalk extends RedirectSpecialArticle { function __construct() { parent::__construct( 'Mytalk' ); - $this->mAllowedRedirectParams = array( 'action' , 'preload' , 'editintro', - 'section', 'oldid', 'diff', 'dir' ); } function getRedirect( $subpage ) { diff --git a/includes/SpecialPageFactory.php b/includes/SpecialPageFactory.php index 0a1631b0..95f75a8e 100644 --- a/includes/SpecialPageFactory.php +++ b/includes/SpecialPageFactory.php @@ -1,6 +1,29 @@ <?php /** - * SpecialPage: handling special pages and lists thereof. + * Factory for handling the special page list and generating SpecialPage objects. + * + * 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 SpecialPage + * @defgroup SpecialPage SpecialPage + */ + +/** + * Factory for handling the special page list and generating SpecialPage objects. * * To add a special page in an extension, add to $wgSpecialPages either * an object instance or an array containing the name and constructor @@ -17,13 +40,6 @@ * SpecialPage::$mList. To remove a core static special page at runtime, use * a SpecialPage_initList hook. * - * @file - * @ingroup SpecialPage - * @defgroup SpecialPage SpecialPage - */ - -/** - * Factory for handling the special page list and generating SpecialPage objects * @ingroup SpecialPage * @since 1.17 */ @@ -118,6 +134,7 @@ class SpecialPageFactory { // High use pages 'Mostlinkedcategories' => 'MostlinkedCategoriesPage', 'Mostimages' => 'MostimagesPage', + 'Mostinterwikis' => 'MostinterwikisPage', 'Mostlinked' => 'MostlinkedPage', 'Mostlinkedtemplates' => 'MostlinkedTemplatesPage', 'Mostcategories' => 'MostcategoriesPage', @@ -276,6 +293,7 @@ class SpecialPageFactory { * Get the group that the special page belongs in on Special:SpecialPage * * @param $page SpecialPage + * @return String */ public static function getGroup( &$page ) { $name = $page->getName(); @@ -473,7 +491,7 @@ class SpecialPageFactory { // Execute special page $profName = 'Special:' . $page->getName(); wfProfileIn( $profName ); - $page->execute( $par ); + $page->run( $par ); wfProfileOut( $profName ); wfProfileOut( __METHOD__ ); return true; diff --git a/includes/SqlDataUpdate.php b/includes/SqlDataUpdate.php new file mode 100644 index 00000000..52c9be00 --- /dev/null +++ b/includes/SqlDataUpdate.php @@ -0,0 +1,150 @@ +<?php +/** + * Base code for update jobs that put some secondary data extracted + * from article content into the database. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Abstract base class for update jobs that put some secondary data extracted + * from article content into the database. + * + * @note: subclasses should NOT start or commit transactions in their doUpdate() method, + * a transaction will automatically be wrapped around the update. Starting another + * one would break the outer transaction bracket. If need be, subclasses can override + * the beginTransaction() and commitTransaction() methods. + */ +abstract class SqlDataUpdate extends DataUpdate { + + protected $mDb; //!< Database connection reference + protected $mOptions; //!< SELECT options to be used (array) + + private $mHasTransaction; //!< bool whether a transaction is open on this object (internal use only!) + protected $mUseTransaction; //!< bool whether this update should be wrapped in a transaction + + /** + * Constructor + * + * @param bool $withTransaction whether this update should be wrapped in a transaction (default: true). + * A transaction is only started if no transaction is already in progress, + * see beginTransaction() for details. + **/ + public function __construct( $withTransaction = true ) { + global $wgAntiLockFlags; + + parent::__construct( ); + + if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) { + $this->mOptions = array(); + } else { + $this->mOptions = array( 'FOR UPDATE' ); + } + + // @todo: get connection only when it's needed? make sure that doesn't break anything, especially transactions! + $this->mDb = wfGetDB( DB_MASTER ); + + $this->mWithTransaction = $withTransaction; + $this->mHasTransaction = false; + } + + /** + * Begin a database transaction, if $withTransaction was given as true in the constructor for this SqlDataUpdate. + * + * Because nested transactions are not supported by the Database class, this implementation + * checks Database::trxLevel() and only opens a transaction if none is already active. + */ + public function beginTransaction() { + if ( !$this->mWithTransaction ) { + return; + } + + // NOTE: nested transactions are not supported, only start a transaction if none is open + if ( $this->mDb->trxLevel() === 0 ) { + $this->mDb->begin( get_class( $this ) . '::beginTransaction' ); + $this->mHasTransaction = true; + } + } + + /** + * Commit the database transaction started via beginTransaction (if any). + */ + public function commitTransaction() { + if ( $this->mHasTransaction ) { + $this->mDb->commit( get_class( $this ) . '::commitTransaction' ); + $this->mHasTransaction = false; + } + } + + /** + * Abort the database transaction started via beginTransaction (if any). + */ + public function abortTransaction() { + if ( $this->mHasTransaction ) { + $this->mDb->rollback( get_class( $this ) . '::abortTransaction' ); + $this->mHasTransaction = false; + } + } + + /** + * Invalidate the cache of a list of pages from a single namespace. + * This is intended for use by subclasses. + * + * @param $namespace Integer + * @param $dbkeys Array + */ + protected function invalidatePages( $namespace, Array $dbkeys ) { + if ( !count( $dbkeys ) ) { + return; + } + + /** + * Determine which pages need to be updated + * This is necessary to prevent the job queue from smashing the DB with + * large numbers of concurrent invalidations of the same page + */ + $now = $this->mDb->timestamp(); + $ids = array(); + $res = $this->mDb->select( 'page', array( 'page_id' ), + array( + 'page_namespace' => $namespace, + 'page_title' => $dbkeys, + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), __METHOD__ + ); + foreach ( $res as $row ) { + $ids[] = $row->page_id; + } + if ( !count( $ids ) ) { + return; + } + + /** + * Do the update + * We still need the page_touched condition, in case the row has changed since + * the non-locking select above. + */ + $this->mDb->update( 'page', array( 'page_touched' => $now ), + array( + 'page_id' => $ids, + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), __METHOD__ + ); + } + +} diff --git a/includes/SquidPurgeClient.php b/includes/SquidPurgeClient.php index 506ada96..8eb0f6bf 100644 --- a/includes/SquidPurgeClient.php +++ b/includes/SquidPurgeClient.php @@ -1,5 +1,26 @@ <?php /** + * Squid and Varnish cache purging. + * + * 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 + */ + +/** * An HTTP 1.0 client built for the purposes of purging Squid and Varnish. * Uses asynchronous I/O, allowing purges to be done in a highly parallel * manner. @@ -23,7 +44,15 @@ class SquidPurgeClient { * The socket resource, or null for unconnected, or false for disabled due to error */ var $socket; - + + var $readBuffer; + + var $bodyRemaining; + + /** + * @param $server string + * @param $options array + */ public function __construct( $server, $options = array() ) { $parts = explode( ':', $server, 2 ); $this->host = $parts[0]; @@ -34,7 +63,7 @@ class SquidPurgeClient { * Open a socket if there isn't one open already, return it. * Returns false on error. * - * @return false|resource + * @return bool|resource */ protected function getSocket() { if ( $this->socket !== null ) { @@ -319,6 +348,9 @@ class SquidPurgeClient { $this->bodyRemaining = null; } + /** + * @param $msg string + */ protected function log( $msg ) { wfDebugLog( 'squid', __CLASS__." ($this->host): $msg\n" ); } @@ -332,6 +364,9 @@ class SquidPurgeClientPool { var $clients = array(); var $timeout = 5; + /** + * @param $options array + */ function __construct( $options = array() ) { if ( isset( $options['timeout'] ) ) { $this->timeout = $options['timeout']; @@ -351,6 +386,9 @@ class SquidPurgeClientPool { $startTime = microtime( true ); while ( !$done ) { $readSockets = $writeSockets = array(); + /** + * @var $client SquidPurgeClient + */ foreach ( $this->clients as $clientIndex => $client ) { $sockets = $client->getReadSocketsForSelect(); foreach ( $sockets as $i => $socket ) { diff --git a/includes/Status.php b/includes/Status.php index e9f3fb91..10dfb516 100644 --- a/includes/Status.php +++ b/includes/Status.php @@ -1,4 +1,24 @@ <?php +/** + * Generic operation result. + * + * 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 + */ /** * Generic operation result class @@ -28,6 +48,7 @@ class Status { * Factory function for fatal errors * * @param $message String: message name + * @return Status */ static function newFatal( $message /*, parameters...*/ ) { $params = func_get_args(); @@ -41,6 +62,7 @@ class Status { * Factory function for good results * * @param $value Mixed + * @return Status */ static function newGood( $value = null ) { $result = new self; @@ -143,35 +165,6 @@ class Status { } /** - * @param $item - * @return string - */ - protected function getItemXML( $item ) { - $params = $this->cleanParams( $item['params'] ); - $xml = "<{$item['type']}>\n" . - Xml::element( 'message', null, $item['message'] ) . "\n" . - Xml::element( 'text', null, wfMsg( $item['message'], $params ) ) ."\n"; - foreach ( $params as $param ) { - $xml .= Xml::element( 'param', null, $param ); - } - $xml .= "</{$item['type']}>\n"; - return $xml; - } - - /** - * Get the error list as XML - * @return string - */ - function getXML() { - $xml = "<errors>\n"; - foreach ( $this->errors as $error ) { - $xml .= $this->getItemXML( $error ); - } - $xml .= "</errors>\n"; - return $xml; - } - - /** * Get the error list as a wikitext formatted list * * @param $shortContext String: a short enclosing context message name, to @@ -192,17 +185,17 @@ class Status { if ( count( $this->errors ) == 1 ) { $s = $this->getWikiTextForError( $this->errors[0], $this->errors[0] ); if ( $shortContext ) { - $s = wfMsgNoTrans( $shortContext, $s ); + $s = wfMessage( $shortContext, $s )->plain(); } elseif ( $longContext ) { - $s = wfMsgNoTrans( $longContext, "* $s\n" ); + $s = wfMessage( $longContext, "* $s\n" )->plain(); } } else { $s = '* '. implode("\n* ", $this->getWikiTextArray( $this->errors ) ) . "\n"; if ( $longContext ) { - $s = wfMsgNoTrans( $longContext, $s ); + $s = wfMessage( $longContext, $s )->plain(); } elseif ( $shortContext ) { - $s = wfMsgNoTrans( $shortContext, "\n$s\n" ); + $s = wfMessage( $shortContext, "\n$s\n" )->plain(); } } return $s; @@ -220,15 +213,15 @@ class Status { protected function getWikiTextForError( $error ) { if ( is_array( $error ) ) { if ( isset( $error['message'] ) && isset( $error['params'] ) ) { - return wfMsgNoTrans( $error['message'], - array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) ); + return wfMessage( $error['message'], + array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ) )->plain(); } else { $message = array_shift($error); - return wfMsgNoTrans( $message, - array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) ); + return wfMessage( $message, + array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) )->plain(); } } else { - return wfMsgNoTrans( $error ); + return wfMessage( $error )->plain(); } } @@ -355,4 +348,11 @@ class Status { public function getMessage() { return $this->getWikiText(); } + + /** + * @return mixed + */ + public function getValue() { + return $this->value; + } } diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 0de03c83..95c69a20 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -1,9 +1,28 @@ <?php /** - * Functions related to the output of file content + * Functions related to the output of file content. + * + * 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 */ + +/** + * Functions related to the output of file content + */ class StreamFile { const READY_STREAM = 1; const NOT_MODIFIED = 2; @@ -12,25 +31,36 @@ class StreamFile { * Stream a file to the browser, adding all the headings and fun stuff. * Headers sent include: Content-type, Content-Length, Last-Modified, * and Content-Disposition. - * + * * @param $fname string Full name and path of the file to stream * @param $headers array Any additional headers to send * @param $sendErrors bool Send error messages if errors occur (like 404) * @return bool Success */ public static function stream( $fname, $headers = array(), $sendErrors = true ) { + wfProfileIn( __METHOD__ ); + + if ( FileBackend::isStoragePath( $fname ) ) { // sanity + throw new MWException( __FUNCTION__ . " given storage path '$fname'." ); + } + wfSuppressWarnings(); $stat = stat( $fname ); wfRestoreWarnings(); $res = self::prepareForStream( $fname, $stat, $headers, $sendErrors ); if ( $res == self::NOT_MODIFIED ) { - return true; // use client cache + $ok = true; // use client cache } elseif ( $res == self::READY_STREAM ) { - return readfile( $fname ); + wfProfileIn( __METHOD__ . '-send' ); + $ok = readfile( $fname ); + wfProfileOut( __METHOD__ . '-send' ); } else { - return false; // failed + $ok = false; // failed } + + wfProfileOut( __METHOD__ ); + return $ok; } /** @@ -41,16 +71,14 @@ class StreamFile { * (c) sends Content-Length header based on HTTP_IF_MODIFIED_SINCE check * * @param $path string Storage path or file system path - * @param $info Array|false File stat info with 'mtime' and 'size' fields + * @param $info Array|bool File stat info with 'mtime' and 'size' fields * @param $headers Array Additional headers to send * @param $sendErrors bool Send error messages if errors occur (like 404) - * @return int|false READY_STREAM, NOT_MODIFIED, or false on failure + * @return int|bool READY_STREAM, NOT_MODIFIED, or false on failure */ public static function prepareForStream( $path, $info, $headers = array(), $sendErrors = true ) { - global $wgLanguageCode; - if ( !is_array( $info ) ) { if ( $sendErrors ) { header( 'HTTP/1.0 404 Not Found' ); @@ -91,9 +119,6 @@ class StreamFile { return false; } - header( "Content-Disposition: inline;filename*=utf-8'$wgLanguageCode'" . - urlencode( basename( $path ) ) ); - // Send additional headers foreach ( $headers as $header ) { header( $header ); @@ -116,7 +141,7 @@ class StreamFile { /** * Determine the file type of a file based on the path - * + * * @param $filename string Storage path or file system path * @param $safe bool Whether to do retroactive upload blacklist checks * @return null|string diff --git a/includes/StringUtils.php b/includes/StringUtils.php index f405e616..43275a66 100644 --- a/includes/StringUtils.php +++ b/includes/StringUtils.php @@ -1,5 +1,26 @@ <?php /** + * Methods to play with strings. + * + * 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 + */ + +/** * A collection of static methods to play with strings. */ class StringUtils { @@ -54,6 +75,7 @@ class StringUtils { * @param $callback Callback: function to call on each match * @param $subject String * @param $flags String: regular expression flags + * @throws MWException * @return string */ static function delimiterReplaceCallback( $startDelim, $endDelim, $callback, $subject, $flags = '' ) { @@ -191,7 +213,7 @@ class StringUtils { * Returns an Iterator * @param $separator * @param $subject - * @return \ArrayIterator|\ExplodeIterator + * @return ArrayIterator|ExplodeIterator */ static function explode( $separator, $subject ) { if ( substr_count( $subject, $separator ) > 1000 ) { @@ -207,6 +229,10 @@ class StringUtils { * StringUtils::delimiterReplaceCallback() */ class Replacer { + + /** + * @return array + */ function cb() { return array( &$this, 'replace' ); } @@ -217,10 +243,18 @@ class Replacer { */ class RegexlikeReplacer extends Replacer { var $r; + + /** + * @param $r string + */ function __construct( $r ) { $this->r = $r; } + /** + * @param $matches array + * @return string + */ function replace( $matches ) { $pairs = array(); foreach ( $matches as $i => $match ) { @@ -235,12 +269,22 @@ class RegexlikeReplacer extends Replacer { * Class to perform secondary replacement within each replacement string */ class DoubleReplacer extends Replacer { + + /** + * @param $from + * @param $to + * @param $index int + */ function __construct( $from, $to, $index = 0 ) { $this->from = $from; $this->to = $to; $this->index = $index; } + /** + * @param $matches array + * @return mixed + */ function replace( $matches ) { return str_replace( $this->from, $this->to, $matches[$this->index] ); } @@ -252,11 +296,19 @@ class DoubleReplacer extends Replacer { class HashtableReplacer extends Replacer { var $table, $index; + /** + * @param $table + * @param $index int + */ function __construct( $table, $index = 0 ) { $this->table = $table; $this->index = $index; } + /** + * @param $matches array + * @return mixed + */ function replace( $matches ) { return $this->table[$matches[$this->index]]; } @@ -273,11 +325,15 @@ class ReplacementArray { /** * Create an object with the specified replacement array * The array should have the same form as the replacement array for strtr() + * @param array $data */ function __construct( $data = array() ) { $this->data = $data; } + /** + * @return array + */ function __sleep() { return array( 'data' ); } @@ -294,39 +350,61 @@ class ReplacementArray { $this->fss = false; } + /** + * @return array|bool + */ function getArray() { return $this->data; } /** * Set an element of the replacement array + * @param $from string + * @param $to stromg */ function setPair( $from, $to ) { $this->data[$from] = $to; $this->fss = false; } + /** + * @param $data array + */ function mergeArray( $data ) { $this->data = array_merge( $this->data, $data ); $this->fss = false; } + /** + * @param $other + */ function merge( $other ) { $this->data = array_merge( $this->data, $other->data ); $this->fss = false; } + /** + * @param $from string + */ function removePair( $from ) { unset($this->data[$from]); $this->fss = false; } + /** + * @param $data array + */ function removeArray( $data ) { - foreach( $data as $from => $to ) + foreach( $data as $from => $to ) { $this->removePair( $from ); + } $this->fss = false; } + /** + * @param $subject string + * @return string + */ function replace( $subject ) { if ( function_exists( 'fss_prep_replace' ) ) { wfProfileIn( __METHOD__.'-fss' ); @@ -369,8 +447,10 @@ class ExplodeIterator implements Iterator { // The current token var $current; - /** + /** * Construct a DelimIterator + * @param $delim string + * @param $s string */ function __construct( $delim, $s ) { $this->subject = $s; @@ -389,7 +469,6 @@ class ExplodeIterator implements Iterator { $this->refreshCurrent(); } - function refreshCurrent() { if ( $this->curPos === false ) { $this->current = false; @@ -410,6 +489,9 @@ class ExplodeIterator implements Iterator { return $this->curPos; } + /** + * @return string + */ function next() { if ( $this->endPos === false ) { $this->curPos = false; @@ -425,8 +507,10 @@ class ExplodeIterator implements Iterator { return $this->current; } + /** + * @return bool + */ function valid() { return $this->curPos !== false; } } - diff --git a/includes/StubObject.php b/includes/StubObject.php index 647ad929..615bcb5f 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -1,4 +1,24 @@ <?php +/** + * Delayed loading of global objects. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ /** * Class to implement stub globals, which are globals that delay loading the @@ -52,6 +72,7 @@ class StubObject { * * @param $name String: name of the function called * @param $args Array: arguments + * @return mixed */ function _call( $name, $args ) { $this->_unstub( $name, 5 ); @@ -72,6 +93,7 @@ class StubObject { * * @param $name String: name of the function called * @param $args Array: arguments + * @return mixed */ function __call( $name, $args ) { return $this->_call( $name, $args ); diff --git a/includes/Timestamp.php b/includes/Timestamp.php new file mode 100644 index 00000000..c9ba8d91 --- /dev/null +++ b/includes/Timestamp.php @@ -0,0 +1,229 @@ +<?php +/** + * Creation and parsing of MW-style timestamps. + * + * 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 + * @since 1.20 + * @author Tyler Romeo, 2012 + */ + +/** + * Library for creating and parsing MW-style timestamps. Based on the JS + * library that does the same thing. + * + * @since 1.20 + */ +class MWTimestamp { + /** + * Standard gmdate() formats for the different timestamp types. + */ + private static $formats = array( + TS_UNIX => 'U', + TS_MW => 'YmdHis', + TS_DB => 'Y-m-d H:i:s', + TS_ISO_8601 => 'Y-m-d\TH:i:s\Z', + TS_ISO_8601_BASIC => 'Ymd\THis\Z', + TS_EXIF => 'Y:m:d H:i:s', // This shouldn't ever be used, but is included for completeness + TS_RFC2822 => 'D, d M Y H:i:s', + TS_ORACLE => 'd-m-Y H:i:s.000000', // Was 'd-M-y h.i.s A' . ' +00:00' before r51500 + TS_POSTGRES => 'Y-m-d H:i:s', + TS_DB2 => 'Y-m-d H:i:s', + ); + + /** + * Different units for human readable timestamps. + * @see MWTimestamp::getHumanTimestamp + */ + private static $units = array( + "milliseconds" => 1, + "seconds" => 1000, // 1000 milliseconds per second + "minutes" => 60, // 60 seconds per minute + "hours" => 60, // 60 minutes per hour + "days" => 24 // 24 hours per day + ); + + /** + * The actual timestamp being wrapped. Either a DateTime + * object or a string with a Unix timestamp depending on + * PHP. + * @var string|DateTime + */ + private $timestamp; + + /** + * Make a new timestamp and set it to the specified time, + * or the current time if unspecified. + * + * @param $timestamp bool|string Timestamp to set, or false for current time + */ + public function __construct( $timestamp = false ) { + $this->setTimestamp( $timestamp ); + } + + /** + * Set the timestamp to the specified time, or the current time if unspecified. + * + * Parse the given timestamp into either a DateTime object or a Unix timestamp, + * and then store it. + * + * @param $ts string|bool Timestamp to store, or false for now + * @throws TimestampException + */ + public function setTimestamp( $ts = false ) { + $da = array(); + $strtime = ''; + + if ( !$ts || $ts === "\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ) { // We want to catch 0, '', null... but not date strings starting with a letter. + $uts = time(); + $strtime = "@$uts"; + } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) { + # TS_DB + } elseif ( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) { + # TS_EXIF + } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) { + # TS_MW + } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) { + # TS_UNIX + $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php + } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) { + # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6 + $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", + str_replace( '+00:00', 'UTC', $ts ) ); + } elseif ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) { + # TS_ISO_8601 + } elseif ( preg_match( '/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?:\.*\d*)?Z$/', $ts, $da ) ) { + #TS_ISO_8601_BASIC + } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/', $ts, $da ) ) { + # TS_POSTGRES + } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/', $ts, $da ) ) { + # TS_POSTGRES + } elseif (preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.\d\d\d$/', $ts, $da ) ) { + # TS_DB2 + } elseif ( preg_match( '/^[ \t\r\n]*([A-Z][a-z]{2},[ \t\r\n]*)?' . # Day of week + '\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' . # dd Mon yyyy + '[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d/S', $ts ) ) { # hh:mm:ss + # TS_RFC2822, accepting a trailing comment. See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171 + # The regex is a superset of rfc2822 for readability + $strtime = strtok( $ts, ';' ); + } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) { + # TS_RFC850 + $strtime = $ts; + } elseif ( preg_match( '/^[A-Z][a-z]{2} [A-Z][a-z]{2} +\d{1,2} \d\d:\d\d:\d\d \d{4}/', $ts ) ) { + # asctime + $strtime = $ts; + } else { + throw new TimestampException( __METHOD__ . " : Invalid timestamp - $ts" ); + } + + if( !$strtime ) { + $da = array_map( 'intval', $da ); + $da[0] = "%04d-%02d-%02dT%02d:%02d:%02d.00+00:00"; + $strtime = call_user_func_array( "sprintf", $da ); + } + + if( function_exists( "date_create" ) ) { + try { + $final = new DateTime( $strtime, new DateTimeZone( 'GMT' ) ); + } catch(Exception $e) { + throw new TimestampException( __METHOD__ . ' Invalid timestamp format.' ); + } + } else { + $final = strtotime( $strtime ); + } + + if( $final === false ) { + throw new TimestampException( __METHOD__ . ' Invalid timestamp format.' ); + } + $this->timestamp = $final; + } + + /** + * Get the timestamp represented by this object in a certain form. + * + * Convert the internal timestamp to the specified format and then + * return it. + * + * @param $style int Constant Output format for timestamp + * @throws TimestampException + * @return string The formatted timestamp + */ + public function getTimestamp( $style = TS_UNIX ) { + if( !isset( self::$formats[$style] ) ) { + throw new TimestampException( __METHOD__ . ' : Illegal timestamp output type.' ); + } + + if( is_object( $this->timestamp ) ) { + // DateTime object was used, call DateTime::format. + $output = $this->timestamp->format( self::$formats[$style] ); + } elseif( TS_UNIX == $style ) { + // Unix timestamp was used and is wanted, just return it. + $output = $this->timestamp; + } else { + // Unix timestamp was used, use gmdate(). + $output = gmdate( self::$formats[$style], $this->timestamp ); + } + + if ( ( $style == TS_RFC2822 ) || ( $style == TS_POSTGRES ) ) { + $output .= ' GMT'; + } + + return $output; + } + + /** + * Get the timestamp in a human-friendly relative format, e.g., "3 days ago". + * + * Determine the difference between the timestamp and the current time, and + * generate a readable timestamp by returning "<N> <units> ago", where the + * largest possible unit is used. + * + * @return string Formatted timestamp + */ + public function getHumanTimestamp() { + $then = $this->getTimestamp( TS_UNIX ); + $now = time(); + $timeago = ($now - $then) * 1000; + $message = false; + + foreach( self::$units as $unit => $factor ) { + $next = $timeago / $factor; + if( $next < 1 ) { + break; + } else { + $timeago = $next; + $message = array( $unit, floor( $timeago ) ); + } + } + + if( $message ) { + $initial = call_user_func_array( 'wfMessage', $message ); + return wfMessage( 'ago', $initial ); + } else { + return wfMessage( 'just-now' ); + } + } + + /** + * @return string + */ + public function __toString() { + return $this->getTimestamp(); + } +} + +class TimestampException extends MWException {} diff --git a/includes/Title.php b/includes/Title.php index f3cf79d4..1b5e21d2 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1,5 +1,7 @@ <?php /** + * Representation a title within %MediaWiki. + * * See title.txt * * This program is free software; you can redistribute it and/or modify @@ -82,7 +84,7 @@ class Title { var $mLength = -1; // /< The page length, 0 for special pages var $mRedirect = null; // /< Is the article at this title a redirect? var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false - var $mBacklinkCache = null; // /< Cache of links to this title + var $mHasSubpage; // /< Whether a page has any subpages // @} @@ -119,7 +121,8 @@ class Title { * fied by a prefix. If you want to force a specific namespace even if * $text might begin with a namespace prefix, use makeTitle() or * makeTitleSafe(). - * @return Title, or null on an error. + * @throws MWException + * @return Title|null - Title or null on an error. */ public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { if ( is_object( $text ) ) { @@ -179,13 +182,12 @@ class Title { * @return Title the new object, or NULL on an error */ public static function newFromURL( $url ) { - global $wgLegalTitleChars; $t = new Title(); # For compatibility with old buggy URLs. "+" is usually not valid in titles, # but some URLs used it as a space replacement and they still come # from some external search tools. - if ( strpos( $wgLegalTitleChars, '+' ) === false ) { + if ( strpos( self::legalChars(), '+' ) === false ) { $url = str_replace( '+', ' ', $url ); } @@ -206,7 +208,15 @@ class Title { */ public static function newFromID( $id, $flags = 0 ) { $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); - $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ ); + $row = $db->selectRow( + 'page', + array( + 'page_namespace', 'page_title', 'page_id', + 'page_len', 'page_is_redirect', 'page_latest', + ), + array( 'page_id' => $id ), + __METHOD__ + ); if ( $row !== false ) { $title = Title::newFromRow( $row ); } else { @@ -260,8 +270,7 @@ class Title { * Load Title object fields from a DB row. * If false is given, the title will be treated as non-existing. * - * @param $row Object|false database row - * @return void + * @param $row Object|bool database row */ public function loadFromRow( $row ) { if ( $row ) { // page found @@ -318,6 +327,10 @@ class Title { * @return Title the new object, or NULL on an error */ public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { + if ( !MWNamespace::exists( $ns ) ) { + return null; + } + $t = new Title(); $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki ); if ( $t->secureAndSplit() ) { @@ -333,7 +346,7 @@ class Title { * @return Title the new object */ public static function newMainPage() { - $title = Title::newFromText( wfMsgForContent( 'mainpage' ) ); + $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() ); // Don't give fatal errors if the message is broken if ( !$title ) { $title = Title::newFromText( 'Main Page' ); @@ -708,17 +721,9 @@ class Title { } } - // Strip off subpages - $pagename = $this->getText(); - if ( strpos( $pagename, '/' ) !== false ) { - list( $username , ) = explode( '/', $pagename, 2 ); - } else { - $username = $pagename; - } - if ( $wgContLang->needsGenderDistinction() && MWNamespace::hasGenderDistinction( $this->mNamespace ) ) { - $gender = GenderCache::singleton()->getGenderOf( $username, __METHOD__ ); + $gender = GenderCache::singleton()->getGenderOf( $this->getText(), __METHOD__ ); return $wgContLang->getGenderNsText( $this->mNamespace, $gender ); } @@ -819,7 +824,7 @@ class Title { /** * Returns true if the title is inside the specified namespace. - * + * * Please make use of this instead of comparing to getNamespace() * This function is much more resistant to changes we may make * to namespaces than code that makes direct comparisons. @@ -863,6 +868,8 @@ class Title { * This is MUCH simpler than individually testing for equivilance * against both NS_USER and NS_USER_TALK, and is also forward compatible. * @since 1.19 + * @param $ns int + * @return bool */ public function hasSubjectNamespace( $ns ) { return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); @@ -928,7 +935,7 @@ class Title { */ public function isConversionTable() { return $this->getNamespace() == NS_MEDIAWIKI && - strpos( $this->getText(), 'Conversiontable' ) !== false; + strpos( $this->getText(), 'Conversiontable/' ) === 0; } /** @@ -1226,6 +1233,9 @@ class Title { * andthe wfArrayToCGI moved to getLocalURL(); * * @since 1.19 (r105919) + * @param $query + * @param $query2 bool + * @return String */ private static function fixUrlQueryArgs( $query, $query2 = false ) { if( $query2 !== false ) { @@ -1259,9 +1269,11 @@ class Title { * See getLocalURL for the arguments. * * @see self::getLocalURL + * @see wfExpandUrl + * @param $proto Protocol type to use in URL * @return String the URL */ - public function getFullURL( $query = '', $query2 = false ) { + public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { $query = self::fixUrlQueryArgs( $query, $query2 ); # Hand off all the decisions on urls to getLocalURL @@ -1270,7 +1282,7 @@ class Title { # Expand the url to make it a full url. Note that getLocalURL has the # potential to output full urls for a variety of reasons, so we use # wfExpandUrl instead of simply prepending $wgServer - $url = wfExpandUrl( $url, PROTO_RELATIVE ); + $url = wfExpandUrl( $url, $proto ); # Finally, add the fragment. $url .= $this->getFragmentForURL(); @@ -1284,7 +1296,7 @@ class Title { * with action=render, $wgServer is prepended. * - * @param $query \twotypes{\string,\array} an optional query string, + * @param $query string|array an optional query string, * not used for interwiki links. Can be specified as an associative array as well, * e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped). * Some query patterns will trigger various shorturl path replacements. @@ -1407,6 +1419,8 @@ class Title { * See getLocalURL for the arguments. * * @see self::getLocalURL + * @param $query string + * @param $query2 bool|string * @return String the URL */ public function escapeLocalURL( $query = '', $query2 = false ) { @@ -1478,6 +1492,7 @@ class Title { * * @see self::getLocalURL * @since 1.18 + * @return string */ public function escapeCanonicalURL( $query = '', $query2 = false ) { wfDeprecated( __METHOD__, '1.19' ); @@ -1502,6 +1517,7 @@ class Title { /** * Is $wgUser watching this page? * + * @deprecated in 1.20; use User::isWatched() instead. * @return Bool */ public function userIsWatching() { @@ -1577,7 +1593,7 @@ class Title { * queries by skipping checks for cascading protections and user blocks. * @param $ignoreErrors Array of Strings Set this to a list of message keys * whose corresponding errors may be ignored. - * @return Array of arguments to wfMsg to explain permissions problems. + * @return Array of arguments to wfMessage to explain permissions problems. */ public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); @@ -1734,7 +1750,7 @@ class Title { # Check $wgNamespaceProtection for restricted namespaces if ( $this->isNamespaceProtected( $user ) ) { $ns = $this->mNamespace == NS_MAIN ? - wfMsg( 'nstab-main' ) : $this->getNsText(); + wfMessage( 'nstab-main' )->text() : $this->getNsText(); $errors[] = $this->mNamespace == NS_MEDIAWIKI ? array( 'protectedinterface' ) : array( 'namespaceprotected', $ns ); } @@ -1867,7 +1883,7 @@ class Title { $title_protection['pt_create_perm'] = 'protect'; // B/C } if( $title_protection['pt_create_perm'] == '' || - !$user->isAllowed( $title_protection['pt_create_perm'] ) ) + !$user->isAllowed( $title_protection['pt_create_perm'] ) ) { $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] ); } @@ -1925,7 +1941,7 @@ class Title { // Don't block the user from editing their own talk page unless they've been // explicitly blocked from that too. } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { - $block = $user->mBlock; + $block = $user->getBlock(); // This is from OutputPage::blockedPage // Copied at r23888 by werdna @@ -1933,7 +1949,7 @@ class Title { $id = $user->blockedBy(); $reason = $user->blockedFor(); if ( $reason == '' ) { - $reason = wfMsg( 'blockednoreason' ); + $reason = wfMessage( 'blockednoreason' )->text(); } $ip = $user->getRequest()->getIP(); @@ -1945,15 +1961,15 @@ class Title { $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; $blockid = $block->getId(); - $blockExpiry = $user->mBlock->mExpiry; - $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true ); + $blockExpiry = $block->getExpiry(); + $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true ); if ( $blockExpiry == 'infinity' ) { $blockExpiry = wfMessage( 'infiniteblock' )->text(); } else { $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); } - $intended = strval( $user->mBlock->getTarget() ); + $intended = strval( $block->getTarget() ); $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ); @@ -2020,9 +2036,8 @@ class Title { $name = $this->getPrefixedText(); $dbName = $this->getPrefixedDBKey(); - // Check with and without underscores + // Check for explicit whitelisting with and without underscores if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { - # Check for explicit whitelisting $whitelisted = true; } elseif ( $this->getNamespace() == NS_MAIN ) { # Old settings might have the title prefixed with @@ -2092,7 +2107,7 @@ class Title { * @param $user User to check * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries. * @param $short Bool Set this to true to stop after the first permission error. - * @return Array of arrays of the arguments to wfMsg to explain permissions problems. + * @return Array of arrays of the arguments to wfMessage to explain permissions problems. */ protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { wfProfileIn( __METHOD__ ); @@ -2471,8 +2486,9 @@ class Title { /** * Get the expiry time for the restriction against a given action * + * @param $action * @return String|Bool 14-char timestamp, or 'infinity' if the page is protected forever - * or not protected at all, or false if the action is not recognised. + * or not protected at all, or false if the action is not recognised. */ public function getRestrictionExpiry( $action ) { if ( !$this->mRestrictionsLoaded ) { @@ -2537,7 +2553,7 @@ class Title { if ( $oldFashionedRestrictions === null ) { $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', - array( 'page_id' => $this->getArticleId() ), __METHOD__ ); + array( 'page_id' => $this->getArticleID() ), __METHOD__ ); } if ( $oldFashionedRestrictions != '' ) { @@ -2549,7 +2565,10 @@ class Title { $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); } else { - $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) ); + $restriction = trim( $temp[1] ); + if( $restriction != '' ) { //some old entries are empty + $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); + } } } @@ -2608,7 +2627,7 @@ class Title { $res = $dbr->select( 'page_restrictions', '*', - array( 'pr_page' => $this->getArticleId() ), + array( 'pr_page' => $this->getArticleID() ), __METHOD__ ); @@ -2841,7 +2860,7 @@ class Title { * @return Int or 0 if the page doesn't exist */ public function getLatestRevID( $flags = 0 ) { - if ( $this->mLatestID !== false ) { + if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) { return intval( $this->mLatestID ); } # Calling getArticleID() loads the field from cache as needed @@ -2860,7 +2879,7 @@ class Title { * * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow * loading of the new page_id. It's also called from - * WikiPage::doDeleteArticle() + * WikiPage::doDeleteArticleReal() * * @param $newid Int the new Article ID */ @@ -3166,7 +3185,7 @@ class Title { * @return Array of Title objects linking here */ public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { - $id = $this->getArticleId(); + $id = $this->getArticleID(); # If the page doesn't exist; there can't be any link from this page if ( !$id ) { @@ -3230,7 +3249,7 @@ class Title { * @return Array of Title the Title objects */ public function getBrokenLinksFrom() { - if ( $this->getArticleId() == 0 ) { + if ( $this->getArticleID() == 0 ) { # All links from article ID 0 are false positives return array(); } @@ -3240,7 +3259,7 @@ class Title { array( 'page', 'pagelinks' ), array( 'pl_namespace', 'pl_title' ), array( - 'pl_from' => $this->getArticleId(), + 'pl_from' => $this->getArticleID(), 'page_namespace IS NULL' ), __METHOD__, array(), @@ -3267,16 +3286,14 @@ class Title { * @return Array of String the URLs */ public function getSquidURLs() { - global $wgContLang; - $urls = array( $this->getInternalURL(), $this->getInternalURL( 'action=history' ) ); - // purge variant urls as well - if ( $wgContLang->hasVariants() ) { - $variants = $wgContLang->getVariants(); + $pageLang = $this->getPageLanguage(); + if ( $pageLang->hasVariants() ) { + $variants = $pageLang->getVariants(); foreach ( $variants as $vCode ) { $urls[] = $this->getInternalURL( '', $vCode ); } @@ -3458,6 +3475,10 @@ class Title { $wgUser->spreadAnyEditBlock(); return $err; } + // Check suppressredirect permission + if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) { + $createRedirect = true; + } // If it is a file, move it first. // It is done before all other moving stuff is done because it's hard to revert. @@ -3475,17 +3496,12 @@ class Title { RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache } - $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own. + $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own. $pageid = $this->getArticleID( self::GAID_FOR_UPDATE ); $protected = $this->isProtected(); // Do the actual move - $err = $this->moveToInternal( $nt, $reason, $createRedirect ); - if ( is_array( $err ) ) { - # @todo FIXME: What about the File we have already moved? - $dbw->rollback(); - return $err; - } + $this->moveToInternal( $nt, $reason, $createRedirect ); // Refresh the sortkey for this row. Be careful to avoid resetting // cl_timestamp, which may disturb time-based lists on some sites. @@ -3529,9 +3545,13 @@ class Title { ); # Update the protection log $log = new LogPage( 'protect' ); - $comment = wfMsgForContent( 'prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); + $comment = wfMessage( + 'prot_1movedto2', + $this->getPrefixedText(), + $nt->getPrefixedText() + )->inContentLanguage()->text(); if ( $reason ) { - $comment .= wfMsgForContent( 'colon-separator' ) . $reason; + $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } // @todo FIXME: $params? $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) ); @@ -3547,7 +3567,7 @@ class Title { WatchedItem::duplicateEntries( $this, $nt ); } - $dbw->commit(); + $dbw->commit( __METHOD__ ); wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); return true; @@ -3559,8 +3579,9 @@ class Title { * * @param $nt Title the page to move to, which should be a redirect or nonexistent * @param $reason String The reason for the move - * @param $createRedirect Bool Whether to leave a redirect at the old title. Ignored - * if the user doesn't have the suppressredirect right + * @param $createRedirect Bool Whether to leave a redirect at the old title. Does not check + * if the user has the suppressredirect right + * @throws MWException */ private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) { global $wgUser, $wgContLang; @@ -3573,7 +3594,7 @@ class Title { $logType = 'move'; } - $redirectSuppressed = !$createRedirect && $wgUser->isAllowed( 'suppressredirect' ); + $redirectSuppressed = !$createRedirect; $logEntry = new ManualLogEntry( 'move', $logType ); $logEntry->setPerformer( $wgUser ); @@ -3588,13 +3609,12 @@ class Title { $formatter->setContext( RequestContext::newExtraneousContext( $this ) ); $comment = $formatter->getPlainActionText(); if ( $reason ) { - $comment .= wfMsgForContent( 'colon-separator' ) . $reason; + $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } # Truncate for whole multibyte characters. $comment = $wgContLang->truncate( $comment, 255 ); $oldid = $this->getArticleID(); - $latest = $this->getLatestRevID(); $dbw = wfGetDB( DB_MASTER ); @@ -3635,7 +3655,7 @@ class Title { $newpage->updateRevisionOn( $dbw, $nullRevision ); wfRunHooks( 'NewRevisionFromEditComplete', - array( $newpage, $nullRevision, $latest, $wgUser ) ); + array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) ); $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) ); @@ -3714,8 +3734,8 @@ class Title { // We don't know whether this function was called before // or after moving the root page, so check both // $this and $nt - if ( $oldSubpage->getArticleId() == $this->getArticleId() || - $oldSubpage->getArticleID() == $nt->getArticleId() ) + if ( $oldSubpage->getArticleID() == $this->getArticleID() || + $oldSubpage->getArticleID() == $nt->getArticleID() ) { // When moving a page to a subpage of itself, // don't move it twice @@ -3803,7 +3823,7 @@ class Title { return false; } # Get the article text - $rev = Revision::newFromTitle( $nt ); + $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST ); if( !is_object( $rev ) ){ return false; } @@ -3839,7 +3859,7 @@ class Title { $data = array(); - $titleKey = $this->getArticleId(); + $titleKey = $this->getArticleID(); if ( $titleKey === 0 ) { return $data; @@ -3915,14 +3935,20 @@ class Title { */ public function getPreviousRevisionID( $revId, $flags = 0 ) { $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); - return $db->selectField( 'revision', 'rev_id', + $revId = $db->selectField( 'revision', 'rev_id', array( - 'rev_page' => $this->getArticleId( $flags ), + 'rev_page' => $this->getArticleID( $flags ), 'rev_id < ' . intval( $revId ) ), __METHOD__, array( 'ORDER BY' => 'rev_id DESC' ) ); + + if ( $revId === false ) { + return false; + } else { + return intval( $revId ); + } } /** @@ -3934,14 +3960,20 @@ class Title { */ public function getNextRevisionID( $revId, $flags = 0 ) { $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); - return $db->selectField( 'revision', 'rev_id', + $revId = $db->selectField( 'revision', 'rev_id', array( - 'rev_page' => $this->getArticleId( $flags ), + 'rev_page' => $this->getArticleID( $flags ), 'rev_id > ' . intval( $revId ) ), __METHOD__, array( 'ORDER BY' => 'rev_id' ) ); + + if ( $revId === false ) { + return false; + } else { + return intval( $revId ); + } } /** @@ -3951,10 +3983,10 @@ class Title { * @return Revision|Null if page doesn't exist */ public function getFirstRevision( $flags = 0 ) { - $pageId = $this->getArticleId( $flags ); + $pageId = $this->getArticleID( $flags ); if ( $pageId ) { $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); - $row = $db->selectRow( 'revision', '*', + $row = $db->selectRow( 'revision', Revision::selectFields(), array( 'rev_page' => $pageId ), __METHOD__, array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) @@ -4016,7 +4048,7 @@ class Title { if ( $this->mEstimateRevisions === null ) { $dbr = wfGetDB( DB_SLAVE ); $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', - array( 'rev_page' => $this->getArticleId() ), __METHOD__ ); + array( 'rev_page' => $this->getArticleID() ), __METHOD__ ); } return $this->mEstimateRevisions; @@ -4043,7 +4075,7 @@ class Title { $dbr = wfGetDB( DB_SLAVE ); return (int)$dbr->selectField( 'revision', 'count(*)', array( - 'rev_page' => $this->getArticleId(), + 'rev_page' => $this->getArticleID(), 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) ), @@ -4052,30 +4084,60 @@ class Title { } /** - * Get the number of authors between the given revision IDs. + * Get the number of authors between the given revisions or revision IDs. * Used for diffs and other things that really need it. * - * @param $old int|Revision Old revision or rev ID (first before range) - * @param $new int|Revision New revision or rev ID (first after range) - * @param $limit Int Maximum number of authors - * @return Int Number of revision authors between these revisions. - */ - public function countAuthorsBetween( $old, $new, $limit ) { + * @param $old int|Revision Old revision or rev ID (first before range by default) + * @param $new int|Revision New revision or rev ID (first after range by default) + * @param $limit int Maximum number of authors + * @param $options string|array (Optional): Single option, or an array of options: + * 'include_old' Include $old in the range; $new is excluded. + * 'include_new' Include $new in the range; $old is excluded. + * 'include_both' Include both $old and $new in the range. + * Unknown option values are ignored. + * @return int Number of revision authors in the range; zero if not both revisions exist + */ + public function countAuthorsBetween( $old, $new, $limit, $options = array() ) { if ( !( $old instanceof Revision ) ) { $old = Revision::newFromTitle( $this, (int)$old ); } if ( !( $new instanceof Revision ) ) { $new = Revision::newFromTitle( $this, (int)$new ); } + // XXX: what if Revision objects are passed in, but they don't refer to this title? + // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID() + // in the sanity check below? if ( !$old || !$new ) { return 0; // nothing to compare } + $old_cmp = '>'; + $new_cmp = '<'; + $options = (array) $options; + if ( in_array( 'include_old', $options ) ) { + $old_cmp = '>='; + } + if ( in_array( 'include_new', $options ) ) { + $new_cmp = '<='; + } + if ( in_array( 'include_both', $options ) ) { + $old_cmp = '>='; + $new_cmp = '<='; + } + // No DB query needed if $old and $new are the same or successive revisions: + if ( $old->getId() === $new->getId() ) { + return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; + } else if ( $old->getId() === $new->getParentId() ) { + if ( $old_cmp === '>' || $new_cmp === '<' ) { + return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; + } + return ( $old->getRawUserText() === $new->getRawUserText() ) ? 1 : 2; + } $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'revision', 'DISTINCT rev_user_text', array( 'rev_page' => $this->getArticleID(), - 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), - 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) + "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), + "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) ), __METHOD__, array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated ); @@ -4117,7 +4179,7 @@ class Title { * @return Bool */ public function exists() { - return $this->getArticleId() != 0; + return $this->getArticleID() != 0; } /** @@ -4137,9 +4199,28 @@ class Title { * @return Bool */ public function isAlwaysKnown() { + $isKnown = null; + + /** + * Allows overriding default behaviour for determining if a page exists. + * If $isKnown is kept as null, regular checks happen. If it's + * a boolean, this value is returned by the isKnown method. + * + * @since 1.20 + * + * @param Title $title + * @param boolean|null $isKnown + */ + wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) ); + + if ( !is_null( $isKnown ) ) { + return $isKnown; + } + if ( $this->mInterwiki != '' ) { return true; // any interwiki link might be viewable, for all we know } + switch( $this->mNamespace ) { case NS_MEDIA: case NS_FILE: @@ -4164,6 +4245,9 @@ class Title { * viewed? In particular, this function may be used to determine if * links to the title should be rendered as "bluelinks" (as opposed to * "redlinks" to non-existent pages). + * Adding something else to this function will cause inconsistency + * since LinkHolderArray calls isAlwaysKnown() and does its own + * page existence check. * * @return Bool */ @@ -4292,9 +4376,10 @@ class Title { $dbr = wfGetDB( DB_SLAVE ); $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', 'wl_notificationtimestamp', - array( 'wl_namespace' => $this->getNamespace(), + array( + 'wl_user' => $user->getId(), + 'wl_namespace' => $this->getNamespace(), 'wl_title' => $this->getDBkey(), - 'wl_user' => $user->getId() ), __METHOD__ ); @@ -4347,6 +4432,11 @@ class Title { 'rd_title' => $this->getDBkey(), 'rd_from = page_id' ); + if ( $this->isExternal() ) { + $where['rd_interwiki'] = $this->getInterwiki(); + } else { + $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'; + } if ( !is_null( $ns ) ) { $where['page_namespace'] = $ns; } @@ -4391,11 +4481,8 @@ class Title { * * @return BacklinkCache */ - function getBacklinkCache() { - if ( is_null( $this->mBacklinkCache ) ) { - $this->mBacklinkCache = new BacklinkCache( $this ); - } - return $this->mBacklinkCache; + public function getBacklinkCache() { + return BacklinkCache::get( $this ); } /** @@ -4444,19 +4531,19 @@ class Title { } /** - * Get the language in which the content of this page is written. - * Defaults to $wgContLang, but in certain cases it can be e.g. - * $wgLang (such as special pages, which are in the user language). + * Get the language in which the content of this page is written in + * wikitext. Defaults to $wgContLang, but in certain cases it can be + * e.g. $wgLang (such as special pages, which are in the user language). * * @since 1.18 - * @return object Language + * @return Language */ public function getPageLanguage() { global $wgLang; if ( $this->isSpecialPage() ) { // special pages are in the user language return $wgLang; - } elseif ( $this->isCssOrJsPage() ) { + } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) { // css/js should always be LTR and is, in fact, English return wfGetLangObj( 'en' ); } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) { @@ -4471,4 +4558,29 @@ class Title { wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) ); return wfGetLangObj( $pageLang ); } + + /** + * Get the language in which the content of this page is written when + * viewed by user. Defaults to $wgContLang, but in certain cases it can be + * e.g. $wgLang (such as special pages, which are in the user language). + * + * @since 1.20 + * @return Language + */ + public function getPageViewLanguage() { + $pageLang = $this->getPageLanguage(); + // If this is nothing special (so the content is converted when viewed) + if ( !$this->isSpecialPage() + && !$this->isCssOrJsPage() && !$this->isCssJsSubpage() + && $this->getNamespace() !== NS_MEDIAWIKI + ) { + // If the user chooses a variant, the content is actually + // in a language whose code is the variant code. + $variant = $pageLang->getPreferredVariant(); + if ( $pageLang->getCode() !== $variant ) { + $pageLang = Language::factory( $variant ); + } + } + return $pageLang; + } } diff --git a/includes/TitleArray.php b/includes/TitleArray.php index 96960089..5cdec16d 100644 --- a/includes/TitleArray.php +++ b/includes/TitleArray.php @@ -1,8 +1,27 @@ <?php /** + * Classes to walk into a list of Title objects. + * * Note: this entire file is a byte-for-byte copy of UserArray.php with * s/User/Title/. If anyone can figure out how to do this nicely with inheri- * tance or something, please do so. + * + * 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 */ /** diff --git a/includes/User.php b/includes/User.php index 1529da1e..0a3db4c0 100644 --- a/includes/User.php +++ b/includes/User.php @@ -66,6 +66,11 @@ class User { const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX; /** + * Maximum items in $mWatchedItems + */ + const MAX_WATCHED_ITEMS_CACHE = 100; + + /** * Array of Strings List of member variables which are saved to the * shared cache (memcached). Any operation which changes the * corresponding database fields must call a cache-clearing function. @@ -114,9 +119,11 @@ class User { 'delete', 'deletedhistory', 'deletedtext', + 'deletelogentry', 'deleterevision', 'edit', 'editinterface', + 'editprotected', 'editusercssjs', #deprecated 'editusercss', 'edituserjs', @@ -134,12 +141,15 @@ class User { 'nominornewtalk', 'noratelimit', 'override-export-depth', + 'passwordreset', 'patrol', + 'patrolmarks', 'protect', 'proxyunbannable', 'purge', 'read', 'reupload', + 'reupload-own', 'reupload-shared', 'rollback', 'sendemail', @@ -165,8 +175,8 @@ class User { //@{ var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, $mEmail, $mTouched, $mToken, $mEmailAuthenticated, - $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides, - $mCookiePassword, $mEditCount, $mAllowUsertalk; + $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount, + $mGroups, $mOptionOverrides; //@} /** @@ -210,10 +220,20 @@ class User { var $mBlock; /** + * @var bool + */ + var $mAllowUsertalk; + + /** * @var Block */ private $mBlockedFromCreateAccount = false; + /** + * @var Array + */ + private $mWatchedItems = array(); + static $idCacheByName = array(); /** @@ -445,22 +465,20 @@ class User { /** * Get the username corresponding to a given user ID * @param $id Int User ID - * @return String|false The corresponding username + * @return String|bool The corresponding username */ public static function whoIs( $id ) { - $dbr = wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), __METHOD__ ); + return UserCache::singleton()->getProp( $id, 'name' ); } /** * Get the real name of a user given their user ID * * @param $id Int User ID - * @return String|false The corresponding user's real name + * @return String|bool The corresponding user's real name */ public static function whoIsReal( $id ) { - $dbr = wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ ); + return UserCache::singleton()->getProp( $id, 'real_name' ); } /** @@ -512,7 +530,7 @@ class User { * as 300.300.300.300 will return true because it looks like an IP * address, despite not being strictly valid. * - * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP + * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP * address because the usemod software would "cloak" anonymous IP * addresses like this, if we allowed accounts like this to be created * new users could get the old edits of these anonymous users. @@ -606,7 +624,7 @@ class User { // Certain names may be reserved for batch processes. foreach ( $reservedUsernames as $reserved ) { if ( substr( $reserved, 0, 4 ) == 'msg:' ) { - $reserved = wfMsgForContent( substr( $reserved, 4 ) ); + $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text(); } if ( $reserved == $name ) { return false; @@ -1023,7 +1041,7 @@ class User { } $dbr = wfGetDB( DB_MASTER ); - $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ ); + $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ ); wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) ); @@ -1278,22 +1296,22 @@ class User { } # User/IP blocking - $block = Block::newFromTarget( $this->getName(), $ip, !$bFromSlave ); + $block = Block::newFromTarget( $this, $ip, !$bFromSlave ); # Proxy blocking if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' ) - && !in_array( $ip, $wgProxyWhitelist ) ) + && !in_array( $ip, $wgProxyWhitelist ) ) { # Local list if ( self::isLocallyBlockedProxy( $ip ) ) { $block = new Block; - $block->setBlocker( wfMsg( 'proxyblocker' ) ); - $block->mReason = wfMsg( 'proxyblockreason' ); + $block->setBlocker( wfMessage( 'proxyblocker' )->text() ); + $block->mReason = wfMessage( 'proxyblockreason' )->text(); $block->setTarget( $ip ); } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) { $block = new Block; - $block->setBlocker( wfMsg( 'sorbs' ) ); - $block->mReason = wfMsg( 'sorbsreason' ); + $block->setBlocker( wfMessage( 'sorbs' )->text() ); + $block->mReason = wfMessage( 'sorbsreason' )->text(); $block->setTarget( $ip ); } } @@ -1372,11 +1390,11 @@ class User { $ipList = gethostbynamel( $host ); if( $ipList ) { - wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); + wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); $found = true; break; } else { - wfDebug( "Requested $host, not found in $base.\n" ); + wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" ); } } } @@ -1512,7 +1530,7 @@ class User { $count = $wgMemc->get( $key ); // Already pinged? if( $count ) { - if( $count > $max ) { + if( $count >= $max ) { wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" ); if( $wgRateLimitLog ) { wfSuppressWarnings(); @@ -1748,16 +1766,22 @@ class User { # Check memcached separately for anons, who have no # entire User object stored in there. if( !$this->mId ) { - global $wgMemc; - $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); - $newtalk = $wgMemc->get( $key ); - if( strval( $newtalk ) !== '' ) { - $this->mNewtalk = (bool)$newtalk; + global $wgDisableAnonTalk; + if( $wgDisableAnonTalk ) { + // Anon newtalk disabled by configuration. + $this->mNewtalk = false; } else { - // Since we are caching this, make sure it is up to date by getting it - // from the master - $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); - $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); + global $wgMemc; + $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); + $newtalk = $wgMemc->get( $key ); + if( strval( $newtalk ) !== '' ) { + $this->mNewtalk = (bool)$newtalk; + } else { + // Since we are caching this, make sure it is up to date by getting it + // from the master + $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); + $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); + } } } else { $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); @@ -1773,14 +1797,20 @@ class User { */ public function getNewMessageLinks() { $talks = array(); - if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) + if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) { return $talks; - - if( !$this->getNewtalk() ) + } elseif( !$this->getNewtalk() ) { return array(); - $up = $this->getUserPage(); - $utp = $up->getTalkPage(); - return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) ); + } + $utp = $this->getTalkPage(); + $dbr = wfGetDB( DB_SLAVE ); + // Get the "last viewed rev" timestamp from the oldest message notification + $timestamp = $dbr->selectField( 'user_newtalk', + 'MIN(user_last_timestamp)', + $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ), + __METHOD__ ); + $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null; + return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) ); } /** @@ -1807,12 +1837,17 @@ class User { * Add or update the new messages flag * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise * @param $id String|Int User's IP address for anonymous users, User ID otherwise + * @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null. * @return Bool True if successful, false otherwise */ - protected function updateNewtalk( $field, $id ) { + protected function updateNewtalk( $field, $id, $curRev = null ) { + // Get timestamp of the talk page revision prior to the current one + $prevRev = $curRev ? $curRev->getPrevious() : false; + $ts = $prevRev ? $prevRev->getTimestamp() : null; + // Mark the user as having new messages since this revision $dbw = wfGetDB( DB_MASTER ); $dbw->insert( 'user_newtalk', - array( $field => $id ), + array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ), __METHOD__, 'IGNORE' ); if ( $dbw->affectedRows() ) { @@ -1847,8 +1882,9 @@ class User { /** * Update the 'You have new messages!' status. * @param $val Bool Whether the user has new messages + * @param $curRev Revision new, as yet unseen revision of the user talk page. Ignored if null or !$val. */ - public function setNewtalk( $val ) { + public function setNewtalk( $val, $curRev = null ) { if( wfReadOnly() ) { return; } @@ -1866,7 +1902,7 @@ class User { global $wgMemc; if( $val ) { - $changed = $this->updateNewtalk( $field, $id ); + $changed = $this->updateNewtalk( $field, $id, $curRev ); } else { $changed = $this->deleteNewtalk( $field, $id ); } @@ -1921,10 +1957,19 @@ class User { $this->mTouched = self::newTouchedTimestamp(); $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'user', - array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), - array( 'user_id' => $this->mId ), - __METHOD__ ); + + // Prevent contention slams by checking user_touched first + $now = $dbw->timestamp( $this->mTouched ); + $needsPurge = $dbw->selectField( 'user', '1', + array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ) + ); + if ( $needsPurge ) { + $dbw->update( 'user', + array( 'user_touched' => $now ), + array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ), + __METHOD__ + ); + } $this->clearSharedCache(); } @@ -1971,7 +2016,7 @@ class User { if( $str !== null ) { if( !$wgAuth->allowPasswordChange() ) { - throw new PasswordError( wfMsg( 'password-change-forbidden' ) ); + throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() ); } if( !$this->isValidPassword( $str ) ) { @@ -1984,12 +2029,12 @@ class User { $message = $valid; $params = array( $wgMinimalPasswordLength ); } - throw new PasswordError( wfMsgExt( $message, array( 'parsemag' ), $params ) ); + throw new PasswordError( wfMessage( $message, $params )->text() ); } } if( !$wgAuth->setPassword( $this, $str ) ) { - throw new PasswordError( wfMsg( 'externaldberror' ) ); + throw new PasswordError( wfMessage( 'externaldberror' )->text() ); } $this->setInternalPassword( $str ); @@ -2036,7 +2081,6 @@ class User { * @param $token String|bool If specified, set the token to this value */ public function setToken( $token = false ) { - global $wgSecretKey, $wgProxyKey; $this->load(); if ( !$token ) { $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH ); @@ -2046,16 +2090,6 @@ class User { } /** - * Set the cookie password - * - * @param $str String New cookie password - */ - private function setCookiePassword( $str ) { - $this->load(); - $this->mCookiePassword = md5( $str ); - } - - /** * Set the password for a password reminder or new account email * * @param $str String New password to set @@ -2119,6 +2153,42 @@ class User { } /** + * Set the user's e-mail address and a confirmation mail if needed. + * + * @since 1.20 + * @param $str String New e-mail address + * @return Status + */ + public function setEmailWithConfirmation( $str ) { + global $wgEnableEmail, $wgEmailAuthentication; + + if ( !$wgEnableEmail ) { + return Status::newFatal( 'emaildisabled' ); + } + + $oldaddr = $this->getEmail(); + if ( $str === $oldaddr ) { + return Status::newGood( true ); + } + + $this->setEmail( $str ); + + if ( $str !== '' && $wgEmailAuthentication ) { + # Send a confirmation request to the new address if needed + $type = $oldaddr != '' ? 'changed' : 'set'; + $result = $this->sendConfirmationMail( $type ); + if ( $result->isGood() ) { + # Say the the caller that a confirmation mail has been sent + $result->value = 'eauth'; + } + } else { + $result = Status::newGood( true ); + } + + return $result; + } + + /** * Get the user's real name * @return String User's real name */ @@ -2239,9 +2309,11 @@ class User { $this->loadOptions(); // Explicitly NULL values should refer to defaults - global $wgDefaultUserOptions; - if( is_null( $val ) && isset( $wgDefaultUserOptions[$oname] ) ) { - $val = $wgDefaultUserOptions[$oname]; + if( is_null( $val ) ) { + $defaultOption = self::getDefaultOption( $oname ); + if( !is_null( $defaultOption ) ) { + $val = $defaultOption; + } } $this->mOptions[$oname] = $val; @@ -2251,7 +2323,10 @@ class User { * Reset all options to the site defaults */ public function resetOptions() { + $this->load(); + $this->mOptions = self::getDefaultOptions(); + $this->mOptionsLoaded = true; } /** @@ -2572,13 +2647,33 @@ class User { } /** + * Get a WatchedItem for this user and $title. + * + * @param $title Title + * @return WatchedItem + */ + public function getWatchedItem( $title ) { + $key = $title->getNamespace() . ':' . $title->getDBkey(); + + if ( isset( $this->mWatchedItems[$key] ) ) { + return $this->mWatchedItems[$key]; + } + + if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) { + $this->mWatchedItems = array(); + } + + $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title ); + return $this->mWatchedItems[$key]; + } + + /** * Check the watched status of an article. * @param $title Title of the article to look at * @return Bool */ public function isWatched( $title ) { - $wl = WatchedItem::fromUserTitle( $this, $title ); - return $wl->isWatched(); + return $this->getWatchedItem( $title )->isWatched(); } /** @@ -2586,8 +2681,7 @@ class User { * @param $title Title of the article to look at */ public function addWatch( $title ) { - $wl = WatchedItem::fromUserTitle( $this, $title ); - $wl->addWatch(); + $this->getWatchedItem( $title )->addWatch(); $this->invalidateCache(); } @@ -2596,8 +2690,7 @@ class User { * @param $title Title of the article to look at */ public function removeWatch( $title ) { - $wl = WatchedItem::fromUserTitle( $this, $title ); - $wl->removeWatch(); + $this->getWatchedItem( $title )->removeWatch(); $this->invalidateCache(); } @@ -2635,28 +2728,14 @@ class User { // The query to find out if it is watched is cached both in memcached and per-invocation, // and when it does have to be executed, it can be on a slave // If this is the user's newtalk page, we always update the timestamp - if( $title->getNamespace() == NS_USER_TALK && + $force = ''; + if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) { - $watched = true; - } else { - $watched = $this->isWatched( $title ); + $force = 'force'; } - // If the page is watched by the user (or may be watched), update the timestamp on any - // any matching rows - if ( $watched ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'watchlist', - array( /* SET */ - 'wl_notificationtimestamp' => null - ), array( /* WHERE */ - 'wl_title' => $title->getDBkey(), - 'wl_namespace' => $title->getNamespace(), - 'wl_user' => $this->getID() - ), __METHOD__ - ); - } + $this->getWatchedItem( $title )->resetNotificationTimestamp( $force ); } /** @@ -2903,6 +2982,7 @@ class User { 'user_token' => strval( $user->mToken ), 'user_registration' => $dbw->timestamp( $user->mRegistration ), 'user_editcount' => 0, + 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ), ); foreach ( $params as $name => $value ) { $fields["user_$name"] = $value; @@ -2921,6 +3001,9 @@ class User { */ public function addToDatabase() { $this->load(); + + $this->mTouched = self::newTouchedTimestamp(); + $dbw = wfGetDB( DB_MASTER ); $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); $dbw->insert( 'user', @@ -2936,6 +3019,7 @@ class User { 'user_token' => strval( $this->mToken ), 'user_registration' => $dbw->timestamp( $this->mRegistration ), 'user_editcount' => 0, + 'user_touched' => $dbw->timestamp( $this->mTouched ), ), __METHOD__ ); $this->mId = $dbw->insertId(); @@ -2994,7 +3078,7 @@ class User { */ public function getPageRenderingHash() { wfDeprecated( __METHOD__, '1.17' ); - + global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang; if( $this->mHash ){ return $this->mHash; @@ -3212,6 +3296,7 @@ class User { * * @param $salt String Optional salt value * @return String The new random token + * @deprecated since 1.20; Use MWCryptRand for secure purposes or wfRandomString for pesudo-randomness */ public static function generateToken( $salt = '' ) { return MWCryptRand::generateHex( 32 ); @@ -3273,15 +3358,15 @@ class User { $message = 'confirmemail_body_' . $type; } - return $this->sendMail( wfMsg( 'confirmemail_subject' ), - wfMsg( $message, + return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(), + wfMessage( $message, $this->getRequest()->getIP(), $this->getName(), $url, $wgLang->timeanddate( $expiration, false ), $invalidateURL, $wgLang->date( $expiration, false ), - $wgLang->time( $expiration, false ) ) ); + $wgLang->time( $expiration, false ) )->text() ); } /** @@ -3344,7 +3429,7 @@ class User { * @return String New token URL */ private function invalidationTokenUrl( $token ) { - return $this->getTokenUrl( 'Invalidateemail', $token ); + return $this->getTokenUrl( 'InvalidateEmail', $token ); } /** @@ -3372,7 +3457,7 @@ class User { * * @note Call saveSettings() after calling this function to commit the change. * - * @return true + * @return bool */ public function confirmEmail() { $this->setEmailAuthenticationTimestamp( wfTimestampNow() ); @@ -3385,7 +3470,7 @@ class User { * address if it was already confirmed. * * @note Call saveSettings() after calling this function to commit the change. - * @return true + * @return bool Returns true */ function invalidateEmail() { $this->load(); @@ -3933,10 +4018,10 @@ class User { $action = 'create2'; if ( $byEmail ) { if ( $reason === '' ) { - $reason = wfMsgForContent( 'newuserlog-byemail' ); + $reason = wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text(); } else { $reason = $wgContLang->commaList( array( - $reason, wfMsgForContent( 'newuserlog-byemail' ) ) ); + $reason, wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text() ) ); } } } @@ -3953,7 +4038,7 @@ class User { * Add an autocreate newuser log entry for this user * Used by things like CentralAuth and perhaps other authplugins. * - * @return true + * @return bool */ public function addNewUserLogEntryAutoCreate() { global $wgNewUserLog; @@ -3961,7 +4046,7 @@ class User { return true; // disabled } $log = new LogPage( 'newusers', false ); - $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) ); + $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ), $this ); return true; } @@ -3988,7 +4073,7 @@ class User { $res = $dbr->select( 'user_properties', - '*', + array( 'up_property', 'up_value' ), array( 'up_user' => $this->getId() ), __METHOD__ ); @@ -4011,13 +4096,9 @@ class User { protected function saveOptions() { global $wgAllowPrefChange; - $extuser = ExternalUser::newFromUser( $this ); - $this->loadOptions(); - $dbw = wfGetDB( DB_MASTER ); - - $insert_rows = array(); + // Not using getOptions(), to keep hidden preferences in database $saveOptions = $this->mOptions; // Allow hooks to abort, for instance to save to a global profile. @@ -4026,13 +4107,17 @@ class User { return; } + $extuser = ExternalUser::newFromUser( $this ); + $userId = $this->getId(); + $insert_rows = array(); foreach( $saveOptions as $key => $value ) { # Don't bother storing default values - if ( ( is_null( self::getDefaultOption( $key ) ) && - !( $value === false || is_null($value) ) ) || - $value != self::getDefaultOption( $key ) ) { + $defaultOption = self::getDefaultOption( $key ); + if ( ( is_null( $defaultOption ) && + !( $value === false || is_null( $value ) ) ) || + $value != $defaultOption ) { $insert_rows[] = array( - 'up_user' => $this->getId(), + 'up_user' => $userId, 'up_property' => $key, 'up_value' => $value, ); @@ -4049,7 +4134,8 @@ class User { } } - $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ ); $dbw->insert( 'user_properties', $insert_rows, __METHOD__ ); } @@ -4104,11 +4190,35 @@ class User { /* if ( $wgMinimalPasswordLength > 1 ) { $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}'; - $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag', - $wgMinimalPasswordLength ); + $ret['title'] = wfMessage( 'passwordtooshort' ) + ->numParams( $wgMinimalPasswordLength )->text(); } */ return $ret; } + + /** + * Return the list of user fields that should be selected to create + * a new user object. + * @return array + */ + public static function selectFields() { + return array( + 'user_id', + 'user_name', + 'user_real_name', + 'user_password', + 'user_newpassword', + 'user_newpass_time', + 'user_email', + 'user_touched', + 'user_token', + 'user_email_authenticated', + 'user_email_token', + 'user_email_token_expires', + 'user_registration', + 'user_editcount', + ); + } } diff --git a/includes/UserArray.php b/includes/UserArray.php index c5ba0b2b..3b8f5c10 100644 --- a/includes/UserArray.php +++ b/includes/UserArray.php @@ -1,4 +1,24 @@ <?php +/** + * Classes to walk into a list of User objects. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ abstract class UserArray implements Iterator { /** diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 5d98d9d2..01e7132d 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -109,16 +109,17 @@ class UserMailer { /** * Creates a single string from an associative array * - * @param $headers Associative Array: keys are header field names, + * @param $headers array Associative Array: keys are header field names, * values are ... values. * @param $endl String: The end of line character. Defaults to "\n" * @return String */ static function arrayToHeaderString( $headers, $endl = "\n" ) { + $strings = array(); foreach( $headers as $name => $value ) { - $string[] = "$name: $value"; + $strings[] = "$name: $value"; } - return implode( $endl, $string ); + return implode( $endl, $strings ); } /** @@ -345,6 +346,7 @@ class UserMailer { /** * Converts a string into quoted-printable format * @since 1.17 + * @return string */ public static function quotedPrintable( $string, $charset = '' ) { # Probably incomplete; see RFC 2045 @@ -434,9 +436,9 @@ class EmailNotification { $res = $dbw->select( array( 'watchlist' ), array( 'wl_user' ), array( - 'wl_title' => $title->getDBkey(), - 'wl_namespace' => $title->getNamespace(), 'wl_user != ' . intval( $editor->getID() ), + 'wl_namespace' => $title->getNamespace(), + 'wl_title' => $title->getDBkey(), 'wl_notificationtimestamp IS NULL', ), __METHOD__ ); @@ -446,17 +448,17 @@ class EmailNotification { if ( $watchers ) { // Update wl_notificationtimestamp for all watching users except // the editor - $dbw->begin(); + $dbw->begin( __METHOD__ ); $dbw->update( 'watchlist', array( /* SET */ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) ), array( /* WHERE */ - 'wl_title' => $title->getDBkey(), + 'wl_user' => $watchers, 'wl_namespace' => $title->getNamespace(), - 'wl_user' => $watchers + 'wl_title' => $title->getDBkey(), ), __METHOD__ ); - $dbw->commit(); + $dbw->commit( __METHOD__ ); } } @@ -620,32 +622,37 @@ class EmailNotification { $postTransformKeys = array(); if ( $this->oldid ) { - if ( $wgEnotifImpersonal ) { - // For impersonal mail, show a diff link to the last revision. - $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_lastdiff', - $this->title->getCanonicalUrl( 'diff=next&oldid=' . $this->oldid ) ); - } else { - $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_lastvisited', - $this->title->getCanonicalUrl( 'diff=0&oldid=' . $this->oldid ) ); + // Always show a link to the diff which triggered the mail. See bug 32210. + $keys['$NEWPAGE'] = wfMessage( 'enotif_lastdiff', + $this->title->getCanonicalUrl( 'diff=next&oldid=' . $this->oldid ) ) + ->inContentLanguage()->text(); + if ( !$wgEnotifImpersonal ) { + // For personal mail, also show a link to the diff of all changes + // since last visited. + $keys['$NEWPAGE'] .= " \n" . wfMessage( 'enotif_lastvisited', + $this->title->getCanonicalUrl( 'diff=0&oldid=' . $this->oldid ) ) + ->inContentLanguage()->text(); } $keys['$OLDID'] = $this->oldid; - $keys['$CHANGEDORCREATED'] = wfMsgForContent( 'changed' ); + $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text(); } else { - $keys['$NEWPAGE'] = wfMsgForContent( 'enotif_newpagetext' ); + $keys['$NEWPAGE'] = wfMessage( 'enotif_newpagetext' )->inContentLanguage()->text(); # clear $OLDID placeholder in the message template $keys['$OLDID'] = ''; - $keys['$CHANGEDORCREATED'] = wfMsgForContent( 'created' ); + $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text(); } $keys['$PAGETITLE'] = $this->title->getPrefixedText(); $keys['$PAGETITLE_URL'] = $this->title->getCanonicalUrl(); - $keys['$PAGEMINOREDIT'] = $this->minorEdit ? wfMsgForContent( 'minoredit' ) : ''; + $keys['$PAGEMINOREDIT'] = $this->minorEdit ? + wfMessage( 'minoredit' )->inContentLanguage()->text() : ''; $keys['$UNWATCHURL'] = $this->title->getCanonicalUrl( 'action=unwatch' ); if ( $this->editor->isAnon() ) { # real anon (user:xxx.xxx.xxx.xxx) - $keys['$PAGEEDITOR'] = wfMsgForContent( 'enotif_anon_editor', $this->editor->getName() ); - $keys['$PAGEEDITOR_EMAIL'] = wfMsgForContent( 'noemailtitle' ); + $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() ) + ->inContentLanguage()->text(); + $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text(); } else { $keys['$PAGEEDITOR'] = $wgEnotifUseRealName ? $this->editor->getRealName() : $this->editor->getName(); $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() ); @@ -659,12 +666,12 @@ class EmailNotification { # Now build message's subject and body - $subject = wfMsgExt( 'enotif_subject', 'content' ); + $subject = wfMessage( 'enotif_subject' )->inContentLanguage()->plain(); $subject = strtr( $subject, $keys ); $subject = MessageCache::singleton()->transform( $subject, false, null, $this->title ); $this->subject = strtr( $subject, $postTransformKeys ); - $body = wfMsgExt( 'enotif_body', 'content' ); + $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain(); $body = strtr( $body, $keys ); $body = MessageCache::singleton()->transform( $body, false, null, $this->title ); $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 ); @@ -754,6 +761,7 @@ class EmailNotification { /** * Same as sendPersonalised but does impersonal mail suitable for bulk * mailing. Takes an array of MailAddress objects. + * @return Status */ function sendImpersonal( $addresses ) { global $wgContLang; @@ -765,7 +773,7 @@ class EmailNotification { array( '$WATCHINGUSERNAME', '$PAGEEDITDATE', '$PAGEEDITTIME' ), - array( wfMsgForContent( 'enotif_impersonal_salutation' ), + array( wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(), $wgContLang->date( $this->timestamp, false, false ), $wgContLang->time( $this->timestamp, false, false ) ), $this->body ); diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php index dfce8adf..26ac3dcd 100644 --- a/includes/UserRightsProxy.php +++ b/includes/UserRightsProxy.php @@ -1,4 +1,24 @@ <?php +/** + * Representation of an user on a other locally-hosted wiki. + * + * 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 + */ /** * Cut-down copy of User interface for local-interwiki-database @@ -163,6 +183,7 @@ class UserRightsProxy { /** * Replaces User::getUserGroups() + * @return array */ function getGroups() { $res = $this->db->select( 'user_groups', diff --git a/includes/ViewCountUpdate.php b/includes/ViewCountUpdate.php index a30b0f79..28ba3414 100644 --- a/includes/ViewCountUpdate.php +++ b/includes/ViewCountUpdate.php @@ -48,8 +48,7 @@ class ViewCountUpdate implements DeferrableUpdate { $dbw = wfGetDB( DB_MASTER ); if ( $wgHitcounterUpdateFreq <= 1 || $dbw->getType() == 'sqlite' ) { - $pageTable = $dbw->tableName( 'page' ); - $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = {$this->id}" ); + $dbw->update( 'page', array( 'page_counter = page_counter + 1' ), array( 'page_id' => $this->id ), __METHOD__ ); return; } @@ -71,10 +70,7 @@ class ViewCountUpdate implements DeferrableUpdate { $dbw = wfGetDB( DB_MASTER ); - $hitcounterTable = $dbw->tableName( 'hitcounter' ); - $res = $dbw->query( "SELECT COUNT(*) as n FROM $hitcounterTable" ); - $row = $dbw->fetchObject( $res ); - $rown = intval( $row->n ); + $rown = $dbw->selectField( 'hitcounter', 'COUNT(*)', array(), __METHOD__ ); if ( $rown < $wgHitcounterUpdateFreq ) { return; @@ -87,6 +83,7 @@ class ViewCountUpdate implements DeferrableUpdate { $dbType = $dbw->getType(); $tabletype = $dbType == 'mysql' ? "ENGINE=HEAP " : ''; + $hitcounterTable = $dbw->tableName( 'hitcounter' ); $acchitsTable = $dbw->tableName( 'acchits' ); $pageTable = $dbw->tableName( 'page' ); diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php index 031b2b48..932af169 100644 --- a/includes/WatchedItem.php +++ b/includes/WatchedItem.php @@ -1,14 +1,34 @@ <?php /** + * Accessor and mutator for watchlist entries. + * + * 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 Watchlist */ /** + * Representation of a pair of user and title for watchlist entries. + * * @ingroup Watchlist */ class WatchedItem { var $mTitle, $mUser, $id, $ns, $ti; + private $loaded = false, $watched, $timestamp; /** * Create a WatchedItem object with the given user and title @@ -32,18 +52,83 @@ class WatchedItem { } /** - * Is mTitle being watched by mUser? - * @return bool + * Return an array of conditions to select or update the appropriate database + * row. + * + * @return array */ - public function isWatched() { + private function dbCond() { + return array( 'wl_user' => $this->id, 'wl_namespace' => $this->ns, 'wl_title' => $this->ti ); + } + + /** + * Load the object from the database + */ + private function load() { + if ( $this->loaded ) { + return; + } + $this->loaded = true; + # Pages and their talk pages are considered equivalent for watching; # remember that talk namespaces are numbered as page namespace+1. $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'watchlist', 1, array( 'wl_user' => $this->id, 'wl_namespace' => $this->ns, - 'wl_title' => $this->ti ), __METHOD__ ); - $iswatched = ($dbr->numRows( $res ) > 0) ? 1 : 0; - return $iswatched; + $row = $dbr->selectRow( 'watchlist', 'wl_notificationtimestamp', + $this->dbCond(), __METHOD__ ); + + if ( $row === false ) { + $this->watched = false; + } else { + $this->watched = true; + $this->timestamp = $row->wl_notificationtimestamp; + } + } + + /** + * Is mTitle being watched by mUser? + * @return bool + */ + public function isWatched() { + $this->load(); + return $this->watched; + } + + /** + * Get the notification timestamp of this entry. + * + * @return false|null|string: false if the page is not watched, the value of + * the wl_notificationtimestamp field otherwise + */ + public function getNotificationTimestamp() { + $this->load(); + if ( $this->watched ) { + return $this->timestamp; + } else { + return false; + } + } + + /** + * Reset the notification timestamp of this entry + * + * @param $force Whether to force the write query to be executed even if the + * page is not watched or the notification timestamp is already NULL. + */ + public function resetNotificationTimestamp( $force = '' ) { + if ( $force != 'force' ) { + $this->load(); + if ( !$this->watched || $this->timestamp === null ) { + return; + } + } + + // If the page is watched by the user (or may be watched), update the timestamp on any + // any matching rows + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => null ), + $this->dbCond(), __METHOD__ ); + $this->timestamp = null; } /** @@ -75,6 +160,8 @@ class WatchedItem { 'wl_notificationtimestamp' => null ), __METHOD__, 'IGNORE' ); + $this->watched = true; + wfProfileOut( __METHOD__ ); return true; } @@ -115,6 +202,8 @@ class WatchedItem { $success = true; } + $this->watched = false; + wfProfileOut( __METHOD__ ); return $success; } @@ -139,7 +228,7 @@ class WatchedItem { * * @return bool */ - private static function doDuplicateEntries( $ot, $nt ) { + private static function doDuplicateEntries( $ot, $nt ) { $oldnamespace = $ot->getNamespace(); $newnamespace = $nt->getNamespace(); $oldtitle = $ot->getDBkey(); diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 39f9cb8e..2cc6338b 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -77,6 +77,7 @@ class WebRequest { * @return Array: Any query arguments found in path matches. */ static public function getPathInfo( $want = 'all' ) { + global $wgUsePathInfo; // PATH_INFO is mangled due to http://bugs.php.net/bug.php?id=31892 // And also by Apache 2.x, double slashes are converted to single slashes. // So we will use REQUEST_URI if possible. @@ -87,7 +88,9 @@ class WebRequest { if ( !preg_match( '!^https?://!', $url ) ) { $url = 'http://unused' . $url; } + wfSuppressWarnings(); $a = parse_url( $url ); + wfRestoreWarnings(); if( $a ) { $path = isset( $a['path'] ) ? $a['path'] : ''; @@ -134,15 +137,17 @@ class WebRequest { $matches = $router->parse( $path ); } - } elseif ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) { - // Mangled PATH_INFO - // http://bugs.php.net/bug.php?id=31892 - // Also reported when ini_get('cgi.fix_pathinfo')==false - $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); - - } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) { - // Regular old PATH_INFO yay - $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 ); + } elseif ( $wgUsePathInfo ) { + if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) { + // Mangled PATH_INFO + // http://bugs.php.net/bug.php?id=31892 + // Also reported when ini_get('cgi.fix_pathinfo')==false + $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); + + } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) { + // Regular old PATH_INFO yay + $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 ); + } } return $matches; @@ -206,18 +211,14 @@ class WebRequest { * available variant URLs. */ public function interpolateTitle() { - global $wgUsePathInfo; - // bug 16019: title interpolation on API queries is useless and sometimes harmful if ( defined( 'MW_API' ) ) { return; } - if ( $wgUsePathInfo ) { - $matches = self::getPathInfo( 'title' ); - foreach( $matches as $key => $val) { - $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val; - } + $matches = self::getPathInfo( 'title' ); + foreach( $matches as $key => $val) { + $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val; } } @@ -298,8 +299,8 @@ class WebRequest { /** * Recursively normalizes UTF-8 strings in the given array. * - * @param $data string or array - * @return cleaned-up version of the given + * @param $data string|array + * @return array|string cleaned-up version of the given * @private */ function normalizeUnicode( $data ) { @@ -379,6 +380,23 @@ class WebRequest { return $ret; } + + /** + * Unset an arbitrary value from our get/post data. + * + * @param $key String: key name to use + * @return Mixed: old value if one was present, null otherwise + */ + public function unsetVal( $key ) { + if ( !isset( $this->data[$key] ) ) { + $ret = null; + } else { + $ret = $this->data[$key]; + unset( $this->data[$key] ); + } + return $ret; + } + /** * Fetch an array from the input or return $default if it's not set. * If source was scalar, will return an array with a single element. @@ -480,17 +498,16 @@ class WebRequest { public function getCheck( $name ) { # Checkboxes and buttons are only present when clicked # Presence connotes truth, abscense false - $val = $this->getVal( $name, null ); - return isset( $val ); + return $this->getVal( $name, null ) !== null; } /** * Fetch a text string from the given array or return $default if it's not * set. Carriage returns are stripped from the text, and with some language * modules there is an input transliteration applied. This should generally - * be used for form <textarea> and <input> fields. Used for user-supplied - * freeform text input (for which input transformations may be required - e.g. - * Esperanto x-coding). + * be used for form "<textarea>" and "<input>" fields. Used for + * user-supplied freeform text input (for which input transformations may + * be required - e.g. Esperanto x-coding). * * @param $name String * @param $default String: optional @@ -518,7 +535,7 @@ class WebRequest { $retVal = array(); foreach ( $names as $name ) { - $value = $this->getVal( $name ); + $value = $this->getGPCVal( $this->data, $name, null ); if ( !is_null( $value ) ) { $retVal[$name] = $value; } @@ -547,6 +564,15 @@ class WebRequest { } /** + * Get the HTTP method used for this request. + * + * @return String + */ + public function getMethod() { + return isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } + + /** * Returns true if the present request was reached by a POST operation, * false otherwise (GET, HEAD, or command-line). * @@ -556,7 +582,7 @@ class WebRequest { * @return Boolean */ public function wasPosted() { - return isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] == 'POST'; + return $this->getMethod() == 'POST'; } /** @@ -655,6 +681,7 @@ class WebRequest { /** * HTML-safe version of appendQuery(). + * @deprecated: Deprecated in 1.20, warnings in 1.21, remove in 1.22. * * @param $query String: query string fragment; do not include initial '?' * @return String @@ -838,7 +865,7 @@ class WebRequest { * Get a request header, or false if it isn't set * @param $name String: case-insensitive header name * - * @return string|false + * @return string|bool False on failure */ public function getHeader( $name ) { $this->initHeaders(); @@ -964,9 +991,11 @@ HTML; /** * Parse the Accept-Language header sent by the client into an array - * @return array array( languageCode => q-value ) sorted by q-value in descending order + * @return array array( languageCode => q-value ) sorted by q-value in descending order then + * appearing time in the header in ascending order. * May contain the "language" '*', which applies to languages other than those explicitly listed. * This is aligned with rfc2616 section 14.4 + * Preference for earlier languages appears in rfc3282 as an extension to HTTP/1.1. */ public function getAcceptLang() { // Modified version of code found at http://www.thefutureoftheweb.com/blog/use-accept-language-header @@ -987,19 +1016,25 @@ HTML; return array(); } - // Create a list like "en" => 0.8 - $langs = array_combine( $lang_parse[1], $lang_parse[4] ); + $langcodes = $lang_parse[1]; + $qvalues = $lang_parse[4]; + $indices = range( 0, count( $lang_parse[1] ) - 1 ); + // Set default q factor to 1 - foreach ( $langs as $lang => $val ) { - if ( $val === '' ) { - $langs[$lang] = 1; - } elseif ( $val == 0 ) { - unset($langs[$lang]); + foreach ( $indices as $index ) { + if ( $qvalues[$index] === '' ) { + $qvalues[$index] = 1; + } elseif ( $qvalues[$index] == 0 ) { + unset( $langcodes[$index], $qvalues[$index], $indices[$index] ); } } - // Sort list - arsort( $langs, SORT_NUMERIC ); + // Sort list. First by $qvalues, then by order. Reorder $langcodes the same way + array_multisort( $qvalues, SORT_DESC, SORT_NUMERIC, $indices, $langcodes ); + + // Create a list like "en" => 0.8 + $langs = array_combine( $langcodes, $qvalues ); + return $langs; } @@ -1251,6 +1286,10 @@ class FauxRequest extends WebRequest { } } + public function getMethod() { + return $this->wasPosted ? 'POST' : 'GET'; + } + /** * @return bool */ @@ -1336,6 +1375,7 @@ class FauxRequest extends WebRequest { * (cookies, session and headers). * * @ingroup HTTP + * @since 1.19 */ class DerivativeRequest extends FauxRequest { private $base; @@ -1366,7 +1406,7 @@ class DerivativeRequest extends FauxRequest { } public function setSessionData( $key, $data ) { - return $this->base->setSessionData( $key, $data ); + $this->base->setSessionData( $key, $data ); } public function getAcceptLang() { diff --git a/includes/WebStart.php b/includes/WebStart.php index 17f8216b..01c5eea8 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -81,7 +81,7 @@ define( 'MEDIAWIKI', true ); # Full path to working directory. # Makes it possible to for example to have effective exclude path in apc. # Also doesn't break installations using symlinked includes, like -# dirname( __FILE__ ) would do. +# __DIR__ would do. $IP = getenv( 'MW_INSTALL_PATH' ); if ( $IP === false ) { $IP = realpath( '.' ); diff --git a/includes/Wiki.php b/includes/Wiki.php index 6ead57c4..a4a89032 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -62,7 +62,6 @@ class MediaWiki { } $this->context = $context; - $this->context->setTitle( $this->parseTitle() ); } /** @@ -134,6 +133,34 @@ class MediaWiki { } /** + * Returns the name of the action that will be executed. + * + * @return string: action + */ + public function getAction() { + static $action = null; + + if ( $action === null ) { + $action = Action::getActionName( $this->context ); + } + + return $action; + } + + /** + * Create an Article object of the appropriate class for the given page. + * + * @deprecated in 1.18; use Article::newFromTitle() instead + * @param $title Title + * @param $context IContextSource + * @return Article object + */ + public static function articleFromTitle( $title, IContextSource $context ) { + wfDeprecated( __METHOD__, '1.18' ); + return Article::newFromTitle( $title, $context ); + } + + /** * Performs the request. * - bad titles * - read restriction @@ -269,11 +296,10 @@ class MediaWiki { $pageView = true; /** * $wgArticle is deprecated, do not use it. - * This will be removed entirely in 1.20. * @deprecated since 1.18 */ global $wgArticle; - $wgArticle = $article; + $wgArticle = new DeprecatedGlobal( 'wgArticle', $article, '1.18' ); $this->performAction( $article ); } elseif ( is_string( $article ) ) { @@ -293,34 +319,6 @@ class MediaWiki { } /** - * Create an Article object of the appropriate class for the given page. - * - * @deprecated in 1.18; use Article::newFromTitle() instead - * @param $title Title - * @param $context IContextSource - * @return Article object - */ - public static function articleFromTitle( $title, IContextSource $context ) { - wfDeprecated( __METHOD__, '1.18' ); - return Article::newFromTitle( $title, $context ); - } - - /** - * Returns the name of the action that will be executed. - * - * @return string: action - */ - public function getAction() { - static $action = null; - - if ( $action === null ) { - $action = Action::getActionName( $this->context ); - } - - return $action; - } - - /** * Initialize the main Article object for "standard" actions (view, etc) * Create an Article object for the page, following redirects if needed. * @@ -394,75 +392,13 @@ class MediaWiki { } /** - * Cleaning up request by doing deferred updates, DB transaction, and the output - */ - public function finalCleanup() { - wfProfileIn( __METHOD__ ); - // Now commit any transactions, so that unreported errors after - // output() don't roll back the whole DB transaction - $factory = wfGetLBFactory(); - $factory->commitMasterChanges(); - // Output everything! - $this->context->getOutput()->output(); - // Do any deferred jobs - DeferredUpdates::doUpdates( 'commit' ); - $this->doJobs(); - wfProfileOut( __METHOD__ ); - } - - /** - * Do a job from the job queue - */ - private function doJobs() { - global $wgJobRunRate; - - if ( $wgJobRunRate <= 0 || wfReadOnly() ) { - return; - } - if ( $wgJobRunRate < 1 ) { - $max = mt_getrandmax(); - if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) { - return; - } - $n = 1; - } else { - $n = intval( $wgJobRunRate ); - } - - while ( $n-- && false != ( $job = Job::pop() ) ) { - $output = $job->toString() . "\n"; - $t = - microtime( true ); - $success = $job->run(); - $t += microtime( true ); - $t = round( $t * 1000 ); - if ( !$success ) { - $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n"; - } else { - $output .= "Success, Time: $t ms\n"; - } - wfDebugLog( 'jobqueue', $output ); - } - } - - /** - * Ends this task peacefully - */ - public function restInPeace() { - MessageCache::logMessages(); - wfLogProfilingData(); - // Commit and close up! - $factory = wfGetLBFactory(); - $factory->commitMasterChanges(); - $factory->shutdown(); - wfDebug( "Request ended normally\n" ); - } - - /** * Perform one of the "standard" actions * * @param $page Page */ private function performAction( Page $page ) { + global $wgUseSquid, $wgSquidMaxage; + wfProfileIn( __METHOD__ ); $request = $this->context->getRequest(); @@ -481,6 +417,13 @@ class MediaWiki { $action = Action::factory( $act, $page ); if ( $action instanceof Action ) { + # Let Squid cache things if we can purge them. + if ( $wgUseSquid && + in_array( $request->getFullRequestURL(), $title->getSquidURLs() ) + ) { + $output->setSquidMaxage( $wgSquidMaxage ); + } + $action->show(); wfProfileOut( __METHOD__ ); return; @@ -591,8 +534,72 @@ class MediaWiki { } $this->performRequest(); - $this->finalCleanup(); + + // Now commit any transactions, so that unreported errors after + // output() don't roll back the whole DB transaction + wfGetLBFactory()->commitMasterChanges(); + + // Output everything! + $this->context->getOutput()->output(); wfProfileOut( __METHOD__ ); } + + /** + * Ends this task peacefully + */ + public function restInPeace() { + // Do any deferred jobs + DeferredUpdates::doUpdates( 'commit' ); + + // Execute a job from the queue + $this->doJobs(); + + // Log message usage, if $wgAdaptiveMessageCache is set to true + MessageCache::logMessages(); + + // Log profiling data, e.g. in the database or UDP + wfLogProfilingData(); + + // Commit and close up! + $factory = wfGetLBFactory(); + $factory->commitMasterChanges(); + $factory->shutdown(); + + wfDebug( "Request ended normally\n" ); + } + + /** + * Do a job from the job queue + */ + private function doJobs() { + global $wgJobRunRate; + + if ( $wgJobRunRate <= 0 || wfReadOnly() ) { + return; + } + if ( $wgJobRunRate < 1 ) { + $max = mt_getrandmax(); + if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) { + return; + } + $n = 1; + } else { + $n = intval( $wgJobRunRate ); + } + + while ( $n-- && false != ( $job = Job::pop() ) ) { + $output = $job->toString() . "\n"; + $t = - microtime( true ); + $success = $job->run(); + $t += microtime( true ); + $t = round( $t * 1000 ); + if ( !$success ) { + $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n"; + } else { + $output .= "Success, Time: $t ms\n"; + } + wfDebugLog( 'jobqueue', $output ); + } + } } diff --git a/includes/WikiCategoryPage.php b/includes/WikiCategoryPage.php index 01938cd9..d3820016 100644 --- a/includes/WikiCategoryPage.php +++ b/includes/WikiCategoryPage.php @@ -1,5 +1,26 @@ <?php /** + * Special handling for category pages. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** * Special handling for category pages */ class WikiCategoryPage extends WikiPage { diff --git a/includes/WikiError.php b/includes/WikiError.php index 7c167f61..45ee20c9 100644 --- a/includes/WikiError.php +++ b/includes/WikiError.php @@ -91,22 +91,22 @@ class WikiErrorMsg extends WikiError { wfDeprecated( __METHOD__, '1.17' ); $args = func_get_args(); array_shift( $args ); - $this->mMessage = wfMsgReal( $message, $args, true ); + $this->mMessage = wfMessage( $message )->rawParams( $args )->text(); $this->mMsgKey = $message; $this->mMsgArgs = $args; } - + function getMessageKey() { return $this->mMsgKey; } - + function getMessageArgs() { return $this->mMsgArgs; } } /** - * Error class designed to handle errors involved with + * Error class designed to handle errors involved with * XML parsing * @ingroup Exception */ @@ -134,12 +134,12 @@ class WikiXmlError extends WikiError { /** @return string */ function getMessage() { // '$1 at line $2, col $3 (byte $4): $5', - return wfMsgHtml( 'xml-error-string', + return wfMessage( 'xml-error-string', $this->mMessage, $this->mLine, $this->mColumn, $this->mByte . $this->mContext, - xml_error_string( $this->mXmlError ) ); + xml_error_string( $this->mXmlError ) )->escaped(); } function _extractContext( $context, $offset ) { diff --git a/includes/WikiFilePage.php b/includes/WikiFilePage.php index 8aeaa243..9fb1522d 100644 --- a/includes/WikiFilePage.php +++ b/includes/WikiFilePage.php @@ -1,5 +1,26 @@ <?php /** + * Special handling for file pages. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** * Special handling for file pages * * @ingroup Media @@ -40,12 +61,9 @@ class WikiFilePage extends WikiPage { } $this->mFileLoaded = true; - $this->mFile = false; + $this->mFile = wfFindFile( $this->mTitle ); if ( !$this->mFile ) { - $this->mFile = wfFindFile( $this->mTitle ); - if ( !$this->mFile ) { - $this->mFile = wfLocalFile( $this->mTitle ); // always a File - } + $this->mFile = wfLocalFile( $this->mTitle ); // always a File } $this->mRepo = $this->mFile->getRepo(); return true; @@ -148,6 +166,7 @@ class WikiFilePage extends WikiPage { /** * Override handling of action=purge + * @return bool */ public function doPurge() { $this->loadFile(); diff --git a/includes/WikiMap.php b/includes/WikiMap.php index 6c7f23b5..4a5e2bcf 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -1,4 +1,24 @@ <?php +/** + * Tools for dealing with other locally-hosted wikis. + * + * 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 + */ /** * Helper tools for dealing with other locally-hosted wikis @@ -34,7 +54,7 @@ class WikiMap { * * @todo We can give more info than just the wiki id! * @param $wikiID String: wiki'd id (generally database name) - * @return Wiki's name or $wiki_id if the wiki was not found + * @return string|int Wiki's name or $wiki_id if the wiki was not found */ public static function getWikiName( $wikiID ) { $wiki = WikiMap::getWiki( $wikiID ); @@ -89,7 +109,7 @@ class WikiMap { $wiki = WikiMap::getWiki( $wikiID ); if ( $wiki ) { - return $wiki->getUrl( $page ); + return $wiki->getFullUrl( $page ); } return false; @@ -106,6 +126,13 @@ class WikiReference { private $mServer; ///< server URL, may be protocol-relative, e.g. '//www.mediawiki.org' private $mPath; ///< path, '/wiki/$1' + /** + * @param $major string + * @param $minor string + * @param $canonicalServer string + * @param $path string + * @param $server null|string + */ public function __construct( $major, $minor, $canonicalServer, $path, $server = null ) { $this->mMajor = $major; $this->mMinor = $minor; @@ -167,7 +194,16 @@ class WikiReference { } /** + * Get a canonical server URL + * @return string + */ + public function getCanonicalServer() { + return $this->mCanonicalServer; + } + + /** * Alias for getCanonicalUrl(), for backwards compatibility. + * @param $page string * @return String */ public function getUrl( $page ) { diff --git a/includes/WikiPage.php b/includes/WikiPage.php index acc9831a..bc8766de 100644 --- a/includes/WikiPage.php +++ b/includes/WikiPage.php @@ -1,5 +1,26 @@ <?php /** + * Base representation for a MediaWiki page. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage) */ abstract class Page {} @@ -12,30 +33,8 @@ abstract class Page {} * * @internal documentation reviewed 15 Mar 2010 */ -class WikiPage extends Page { - // doDeleteArticleReal() return values. Values less than zero indicate fatal errors, - // values greater than zero indicate that there were problems not resulting in page - // not being deleted - - /** - * Delete operation aborted by hook - */ - const DELETE_HOOK_ABORTED = -1; - - /** - * Deletion successful - */ - const DELETE_SUCCESS = 0; - - /** - * Page not found - */ - const DELETE_NO_PAGE = 1; - - /** - * No revisions found to delete - */ - const DELETE_NO_REVISIONS = 2; +class WikiPage extends Page implements IDBAccessObject { + // Constants for $mDataLoadedFrom and related /** * @var Title @@ -52,6 +51,11 @@ class WikiPage extends Page { /**@}}*/ /** + * @var int; one of the READ_* constants + */ + protected $mDataLoadedFrom = self::READ_NONE; + + /** * @var Title */ protected $mRedirectTarget = null; @@ -88,6 +92,7 @@ class WikiPage extends Page { * Create a WikiPage object of the appropriate class for the given title. * * @param $title Title + * @throws MWException * @return WikiPage object of the appropriate type */ public static function factory( Title $title ) { @@ -117,15 +122,58 @@ class WikiPage extends Page { * Constructor from a page id * * @param $id Int article ID to load + * @param $from string|int one of the following values: + * - "fromdb" or WikiPage::READ_NORMAL to select from a slave database + * - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database * + * @return WikiPage|null + */ + public static function newFromID( $id, $from = 'fromdb' ) { + $from = self::convertSelectType( $from ); + $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE ); + $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ ); + if ( !$row ) { + return null; + } + return self::newFromRow( $row, $from ); + } + + /** + * Constructor from a database row + * + * @since 1.20 + * @param $row object: database row containing at least fields returned + * by selectFields(). + * @param $from string|int: source of $data: + * - "fromdb" or WikiPage::READ_NORMAL: from a slave DB + * - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB + * - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE * @return WikiPage */ - public static function newFromID( $id ) { - $t = Title::newFromID( $id ); - if ( $t ) { - return self::factory( $t ); + public static function newFromRow( $row, $from = 'fromdb' ) { + $page = self::factory( Title::newFromRow( $row ) ); + $page->loadFromRow( $row, $from ); + return $page; + } + + /** + * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants. + * + * @param $type object|string|int + * @return mixed + */ + private static function convertSelectType( $type ) { + switch ( $type ) { + case 'fromdb': + return self::READ_NORMAL; + case 'fromdbmaster': + return self::READ_LATEST; + case 'forupdate': + return self::READ_LOCKING; + default: + // It may already be an integer or whatever else + return $type; } - return null; } /** @@ -152,10 +200,20 @@ class WikiPage extends Page { /** * Clear the object + * @return void */ public function clear() { $this->mDataLoaded = false; + $this->mDataLoadedFrom = self::READ_NONE; + + $this->clearCacheFields(); + } + /** + * Clear the object cache fields + * @return void + */ + protected function clearCacheFields() { $this->mCounter = null; $this->mRedirectTarget = null; # Title object if set $this->mLastRevision = null; # Latest revision @@ -192,14 +250,15 @@ class WikiPage extends Page { * Fetch a page record with the given conditions * @param $dbr DatabaseBase object * @param $conditions Array + * @param $options Array * @return mixed Database result resource, or false on failure */ - protected function pageData( $dbr, $conditions ) { + protected function pageData( $dbr, $conditions, $options = array() ) { $fields = self::selectFields(); wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); - $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__ ); + $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); @@ -212,12 +271,13 @@ class WikiPage extends Page { * * @param $dbr DatabaseBase object * @param $title Title object + * @param $options Array * @return mixed Database result resource, or false on failure */ - public function pageDataFromTitle( $dbr, $title ) { + public function pageDataFromTitle( $dbr, $title, $options = array() ) { return $this->pageData( $dbr, array( 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() ) ); + 'page_title' => $title->getDBkey() ), $options ); } /** @@ -225,37 +285,69 @@ class WikiPage extends Page { * * @param $dbr DatabaseBase * @param $id Integer + * @param $options Array * @return mixed Database result resource, or false on failure */ - public function pageDataFromId( $dbr, $id ) { - return $this->pageData( $dbr, array( 'page_id' => $id ) ); + public function pageDataFromId( $dbr, $id, $options = array() ) { + return $this->pageData( $dbr, array( 'page_id' => $id ), $options ); } /** * Set the general counter, title etc data loaded from * some source. * - * @param $data Object|String One of the following: - * A DB query result object or... - * "fromdb" to get from a slave DB or... - * "fromdbmaster" to get from the master DB + * @param $from object|string|int One of the following: + * - A DB query result object + * - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB + * - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB + * - "forupdate" or WikiPage::READ_LOCKING to get from the master DB using SELECT FOR UPDATE + * * @return void */ - public function loadPageData( $data = 'fromdb' ) { - if ( $data === 'fromdbmaster' ) { + public function loadPageData( $from = 'fromdb' ) { + $from = self::convertSelectType( $from ); + if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) { + // We already have the data from the correct location, no need to load it twice. + return; + } + + if ( $from === self::READ_LOCKING ) { + $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); + } elseif ( $from === self::READ_LATEST ) { $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); - } elseif ( $data === 'fromdb' ) { // slave + } elseif ( $from === self::READ_NORMAL ) { $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); # Use a "last rev inserted" timestamp key to dimish the issue of slave lag. # Note that DB also stores the master position in the session and checks it. $touched = $this->getCachedLastEditTime(); if ( $touched ) { // key set if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { + $from = self::READ_LATEST; $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); } } + } else { + // No idea from where the caller got this data, assume slave database. + $data = $from; + $from = self::READ_NORMAL; } + $this->loadFromRow( $data, $from ); + } + + /** + * Load the object from a database row + * + * @since 1.20 + * @param $data object: database row containing at least fields returned + * by selectFields() + * @param $from string|int One of the following: + * - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB + * - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB + * - "forupdate" or WikiPage::READ_LOCKING if the data comes from from + * the master DB using SELECT FOR UPDATE + */ + public function loadFromRow( $data, $from ) { $lc = LinkCache::singleton(); if ( $data ) { @@ -270,13 +362,22 @@ class WikiPage extends Page { $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); $this->mIsRedirect = intval( $data->page_is_redirect ); $this->mLatest = intval( $data->page_latest ); + // Bug 37225: $latest may no longer match the cached latest Revision object. + // Double-check the ID of any cached latest Revision object for consistency. + if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { + $this->mLastRevision = null; + $this->mTimestamp = ''; + } } else { $lc->addBadLinkObj( $this->mTitle ); $this->mTitle->loadFromRow( false ); + + $this->clearCacheFields(); } $this->mDataLoaded = true; + $this->mDataLoadedFrom = self::convertSelectType( $from ); } /** @@ -368,6 +469,45 @@ class WikiPage extends Page { } /** + * Get the Revision object of the oldest revision + * @return Revision|null + */ + public function getOldestRevision() { + wfProfileIn( __METHOD__ ); + + // Try using the slave database first, then try the master + $continue = 2; + $db = wfGetDB( DB_SLAVE ); + $revSelectFields = Revision::selectFields(); + + while ( $continue ) { + $row = $db->selectRow( + array( 'page', 'revision' ), + $revSelectFields, + array( + 'page_namespace' => $this->mTitle->getNamespace(), + 'page_title' => $this->mTitle->getDBkey(), + 'rev_page = page_id' + ), + __METHOD__, + array( + 'ORDER BY' => 'rev_timestamp ASC' + ) + ); + + if ( $row ) { + $continue = 0; + } else { + $db = wfGetDB( DB_MASTER ); + $continue--; + } + } + + wfProfileOut( __METHOD__ ); + return $row ? Revision::newFromRow( $row ) : null; + } + + /** * Loads everything except the text * This isn't necessary for all uses, so it's only done if needed. */ @@ -381,7 +521,14 @@ class WikiPage extends Page { return; // page doesn't exist or is missing page_latest info } - $revision = Revision::newFromPageId( $this->getId(), $latest ); + // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the + // latest changes committed. This is true even within REPEATABLE-READ transactions, where + // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to + // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row + // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. + // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. + $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; + $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); if ( $revision ) { // sanity $this->setLastEdit( $revision ); } @@ -414,7 +561,7 @@ class WikiPage extends Page { * Revision::FOR_PUBLIC to be displayed to all users * Revision::FOR_THIS_USER to be displayed to $wgUser * Revision::RAW get the text regardless of permissions - * @return String|false The text of the current revision + * @return String|bool The text of the current revision. False on failure */ public function getText( $audience = Revision::FOR_PUBLIC ) { $this->loadLastEdit(); @@ -427,7 +574,7 @@ class WikiPage extends Page { /** * Get the text of the current revision. No side-effects... * - * @return String|false The text of the current revision + * @return String|bool The text of the current revision. False on failure */ public function getRawText() { $this->loadLastEdit(); @@ -445,6 +592,7 @@ class WikiPage extends Page { if ( !$this->mTimestamp ) { $this->loadLastEdit(); } + return wfTimestamp( TS_MW, $this->mTimestamp ); } @@ -474,6 +622,24 @@ class WikiPage extends Page { } /** + * Get the User object of the user who created the page + * @param $audience Integer: one of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * @return User|null + */ + public function getCreator( $audience = Revision::FOR_PUBLIC ) { + $revision = $this->getOldestRevision(); + if ( $revision ) { + $userName = $revision->getUserText( $audience ); + return User::newFromName( $userName, false ); + } else { + return null; + } + } + + /** * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users * Revision::FOR_THIS_USER to be displayed to $wgUser @@ -546,7 +712,7 @@ class WikiPage extends Page { * 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 $editInfo Object or false: object returned by prepareTextForEdit(), + * @param $editInfo Object|bool (false): object returned by prepareTextForEdit(), * if false, the current database state will be used * @return Boolean */ @@ -726,10 +892,10 @@ class WikiPage extends Page { $tables = array( 'revision', 'user' ); $fields = array( - 'rev_user as user_id', - 'rev_user_text AS user_name', + 'user_id' => 'rev_user', + 'user_name' => 'rev_user_text', $realNameField, - 'MAX(rev_timestamp) AS timestamp', + 'timestamp' => 'MAX(rev_timestamp)', ); $conds = array( 'rev_page' => $this->getId() ); @@ -888,6 +1054,7 @@ class WikiPage extends Page { /** * Perform the actions of a page purging + * @return bool */ public function doPurge() { global $wgUseSquid; @@ -903,7 +1070,7 @@ class WikiPage extends Page { if ( $wgUseSquid ) { // Commit the transaction before the purge is sent $dbw = wfGetDB( DB_MASTER ); - $dbw->commit(); + $dbw->commit( __METHOD__ ); // Send purge $update = SquidUpdate::newSimplePurge( $this->mTitle ); @@ -1002,7 +1169,7 @@ class WikiPage extends Page { $conditions, __METHOD__ ); - $result = $dbw->affectedRows() != 0; + $result = $dbw->affectedRows() > 0; if ( $result ) { $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); $this->setLastEdit( $revision ); @@ -1023,7 +1190,7 @@ class WikiPage extends Page { * @param $dbw DatabaseBase * @param $redirectTitle Title object pointing to the redirect target, * or NULL if this is not a redirect - * @param $lastRevIsRedirect If given, will optimize adding and + * @param $lastRevIsRedirect null|bool If given, will optimize adding and * removing rows in redirect table. * @return bool true on success, false on failure * @private @@ -1059,7 +1226,7 @@ class WikiPage extends Page { * If the given revision is newer than the currently set page_latest, * update the page record. Otherwise, do nothing. * - * @param $dbw Database object + * @param $dbw DatabaseBase object * @param $revision Revision object * @return mixed */ @@ -1124,7 +1291,7 @@ class WikiPage extends Page { } /** - * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...) + * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...) * @param $text String: new text of the section * @param $sectionTitle String: new section's subject, only if $section is 'new' * @param $edittime String: revision timestamp or null to use the current revision @@ -1160,7 +1327,8 @@ class WikiPage extends Page { if ( $section == 'new' ) { # Inserting a new section - $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : ''; + $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' ) + ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) { $text = strlen( trim( $oldtext ) ) > 0 ? "{$oldtext}\n\n{$subject}{$text}" @@ -1223,9 +1391,10 @@ class WikiPage extends Page { * 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 bool|int $baseRevId int the revision ID this edit was based off, if any * @param $user User the user doing the edit * + * @throws MWException * @return Status object. Possible errors: * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status * edit-gone-missing: In update mode, but the article didn't exist @@ -1242,7 +1411,7 @@ class WikiPage extends Page { * 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; + global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; # Low-level sanity check if ( $this->mTitle->getText() === '' ) { @@ -1254,7 +1423,9 @@ class WikiPage extends Page { $user = is_null( $user ) ? $wgUser : $user; $status = Status::newGood( array() ); - # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already + // Load the data from the master database if needed. + // The caller may already loaded it from the master or even loaded it using + // SELECT FOR UPDATE, so do not override that using clear(). $this->loadPageData( 'fromdbmaster' ); $flags = $this->checkFlags( $flags ); @@ -1306,11 +1477,10 @@ class WikiPage extends Page { wfProfileOut( __METHOD__ ); return $status; - } - - # Make sure the revision is either completely inserted or not inserted at all - if ( !$wgDBtransactions ) { - $userAbort = ignore_user_abort( true ); + } elseif ( $oldtext === false ) { + # Sanity check for bug 37225 + wfProfileOut( __METHOD__ ); + throw new MWException( "Could not find text for current revision {$oldid}." ); } $revision = new Revision( array( @@ -1323,11 +1493,14 @@ class WikiPage extends Page { 'user_text' => $user->getName(), 'timestamp' => $now ) ); + # Bug 37225: use accessor to get the text as Revision may trim it. + # After trimming, the text may be a duplicate of the current text. + $text = $revision->getText(); // sanity; EditPage should trim already $changed = ( strcmp( $text, $oldtext ) != 0 ); if ( $changed ) { - $dbw->begin(); + $dbw->begin( __METHOD__ ); $revisionId = $revision->insertOn( $dbw ); # Update page @@ -1340,54 +1513,40 @@ class WikiPage extends Page { $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); if ( !$ok ) { - /* Belated edit conflict! Run away!! */ + # 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__ ); - } + $dbw->rollback( __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 && !count( - $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); - # Add RC row to the DB - $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, - $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, - $revisionId, $patrolled - ); - - # Log auto-patrolled edits - if ( $patrolled ) { - PatrolLog::record( $rc, true ); - } + wfProfileOut( __METHOD__ ); + return $status; + } + + wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); + # Update recentchanges + if ( !( $flags & EDIT_SUPPRESS_RC ) ) { + # Mark as patrolled if the user can do so + $patrolled = $wgUseRCPatrol && !count( + $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); + # Add RC row to the DB + $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, + $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, + $revisionId, $patrolled + ); + + # Log auto-patrolled edits + if ( $patrolled ) { + PatrolLog::record( $rc, true, $user ); } - $user->incEditCount(); - $dbw->commit(); } + $user->incEditCount(); + $dbw->commit( __METHOD__ ); } else { // Bug 32948: revision ID must be set to page {{REVISIONID}} and // related variables correctly $revision->setId( $this->getLatest() ); } - 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; - } - # Update links tables, site stats, etc. $this->doEditUpdates( $revision, $user, array( 'changed' => $changed, 'oldcountable' => $oldcountable ) ); @@ -1403,14 +1562,14 @@ class WikiPage extends Page { # Create new article $status->value['new'] = true; - $dbw->begin(); + $dbw->begin( __METHOD__ ); # 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(); + $dbw->rollback( __METHOD__ ); $status->fatal( 'edit-already-exists' ); wfProfileOut( __METHOD__ ); @@ -1429,6 +1588,9 @@ class WikiPage extends Page { ) ); $revisionId = $revision->insertOn( $dbw ); + # Bug 37225: use accessor to get the text as Revision may trim it + $text = $revision->getText(); // sanity; EditPage should trim already + # Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); @@ -1436,8 +1598,6 @@ class WikiPage extends Page { # Update recentchanges if ( !( $flags & EDIT_SUPPRESS_RC ) ) { - global $wgUseRCPatrol, $wgUseNPPatrol; - # Mark as patrolled if the user can do so $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); @@ -1447,11 +1607,11 @@ class WikiPage extends Page { # Log auto-patrolled edits if ( $patrolled ) { - PatrolLog::record( $rc, true ); + PatrolLog::record( $rc, true, $user ); } } $user->incEditCount(); - $dbw->commit(); + $dbw->commit( __METHOD__ ); # Update links, etc. $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); @@ -1480,24 +1640,41 @@ class WikiPage extends Page { /** * Get parser options suitable for rendering the primary article wikitext - * @param User|string $user User object or 'canonical' + * + * @param IContextSource|User|string $context One of the following: + * - IContextSource: Use the User and the Language of the provided + * context + * - User: Use the provided User object and $wgLang for the language, + * so use an IContextSource object if possible. + * - 'canonical': Canonical options (anonymous user with default + * preferences and content language). * @return ParserOptions */ - public function makeParserOptions( $user ) { + public function makeParserOptions( $context ) { global $wgContLang; - if ( $user instanceof User ) { // settings per user (even anons) - $options = ParserOptions::newFromUser( $user ); + + if ( $context instanceof IContextSource ) { + $options = ParserOptions::newFromContext( $context ); + } elseif ( $context instanceof User ) { // settings per user (even anons) + $options = ParserOptions::newFromUser( $context ); } else { // canonical settings $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); } + + if ( $this->getTitle()->isConversionTable() ) { + $options->disableContentConversion(); + } + $options->enableLimitReport(); // show inclusion/loop reports $options->setTidy( true ); // fix bad HTML + return $options; } /** * Prepare text which is about to be saved. * Returns a stdclass with source, pst and output members + * @return bool|object */ public function prepareTextForEdit( $text, $revid = null, User $user = null ) { global $wgParser, $wgContLang, $wgUser; @@ -1568,9 +1745,9 @@ class WikiPage extends Page { $parserCache->save( $editInfo->output, $this, $editInfo->popts ); } - # Update the links tables - $u = new LinksUpdate( $this->mTitle, $editInfo->output ); - $u->doUpdate(); + # Update the links tables and other secondary data + $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle ); + DataUpdate::runUpdates( $updates ); wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); @@ -1630,9 +1807,9 @@ class WikiPage extends Page { wfDebug( __METHOD__ . ": invalid username\n" ); } elseif ( User::isIP( $shortTitle ) ) { // An anonymous user - $other->setNewtalk( true ); + $other->setNewtalk( true, $revision ); } elseif ( $other->isLoggedIn() ) { - $other->setNewtalk( true ); + $other->setNewtalk( true, $revision ); } else { wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); } @@ -1689,7 +1866,7 @@ class WikiPage extends Page { * @param &$cascade Integer. Set to false if cascading protection isn't allowed. * @param $expiry Array: per restriction type expiration * @param $user User The user updating the restrictions - * @return bool true on success + * @return Status */ public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) { global $wgContLang; @@ -1772,12 +1949,15 @@ class WikiPage extends Page { if ( $restrictions != '' ) { $protectDescription .= $wgContLang->getDirMark() . "[$action=$restrictions] ("; if ( $encodedExpiry[$action] != 'infinity' ) { - $protectDescription .= wfMsgForContent( 'protect-expiring', + $protectDescription .= wfMessage( + 'protect-expiring', $wgContLang->timeanddate( $expiry[$action], false, false ) , $wgContLang->date( $expiry[$action], false, false ) , - $wgContLang->time( $expiry[$action], false, false ) ); + $wgContLang->time( $expiry[$action], false, false ) + )->inContentLanguage()->text(); } else { - $protectDescription .= wfMsgForContent( 'protect-expiry-indefinite' ); + $protectDescription .= wfMessage( 'protect-expiry-indefinite' ) + ->inContentLanguage()->text(); } $protectDescription .= ') '; @@ -1818,7 +1998,12 @@ class WikiPage extends Page { } # Prepare a null revision to be added to the history - $editComment = $wgContLang->ucfirst( wfMsgForContent( $revCommentMsg, $this->mTitle->getPrefixedText() ) ); + $editComment = $wgContLang->ucfirst( + wfMessage( + $revCommentMsg, + $this->mTitle->getPrefixedText() + )->inContentLanguage()->text() + ); if ( $reason ) { $editComment .= ": $reason"; } @@ -1826,7 +2011,9 @@ class WikiPage extends Page { $editComment .= " ($protectDescription)"; } if ( $cascade ) { - $editComment .= ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']'; + // FIXME: Should use 'brackets' message. + $editComment .= ' [' . wfMessage( 'protect-summary-cascade' ) + ->inContentLanguage()->text() . ']'; } # Insert a null revision @@ -1893,6 +2080,7 @@ class WikiPage extends Page { * Take an array of page restrictions and flatten it to a string * suitable for insertion into the page_restrictions field. * @param $limit Array + * @throws MWException * @return String */ protected static function flattenRestrictions( $limit ) { @@ -1913,15 +2101,15 @@ class WikiPage extends Page { } /** - * Same as doDeleteArticleReal(), but returns more detailed success/failure status + * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for + * backwards compatibility, if you care about error reporting you should use + * doDeleteArticleReal() instead. + * * Deletes the article with database consistency, writes logs, purges caches * * @param $reason string delete reason for deletion log - * @param $suppress bitfield - * Revision::DELETED_TEXT - * Revision::DELETED_COMMENT - * Revision::DELETED_USER - * Revision::DELETED_RESTRICTED + * @param $suppress boolean suppress all revisions and log the deletion in + * the suppression log instead of the deletion log * @param $id int article ID * @param $commit boolean defaults to true, triggers transaction end * @param &$error Array of errors to append to @@ -1931,43 +2119,56 @@ class WikiPage extends Page { public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null ) { - return $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ) - == WikiPage::DELETE_SUCCESS; + $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ); + return $status->isGood(); } /** * Back-end article deletion * Deletes the article with database consistency, writes logs, purges caches * + * @since 1.19 + * * @param $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 $suppress boolean suppress all revisions and log the deletion in + * the suppression log instead of the deletion log * @param $commit boolean defaults to true, triggers transaction end * @param &$error Array of errors to append to * @param $user User The deleting user - * @return int: One of WikiPage::DELETE_* constants + * @return Status: Status object; if successful, $status->value is the log_id of the + * deletion log entry. If the page couldn't be deleted because it wasn't + * found, $status is a non-fatal 'cannotdelete' error */ public function doDeleteArticleReal( $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null ) { global $wgUser; - $user = is_null( $user ) ? $wgUser : $user; wfDebug( __METHOD__ . "\n" ); - if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error ) ) ) { - return WikiPage::DELETE_HOOK_ABORTED; + $status = Status::newGood(); + + if ( $this->mTitle->getDBkey() === '' ) { + $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); + return $status; } - $dbw = wfGetDB( DB_MASTER ); - $t = $this->mTitle->getDBkey(); - $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); - if ( $t === '' || $id == 0 ) { - return WikiPage::DELETE_NO_PAGE; + $user = is_null( $user ) ? $wgUser : $user; + if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) { + if ( $status->isOK() ) { + // Hook aborted but didn't set a fatal status + $status->fatal( 'delete-hook-aborted' ); + } + return $status; + } + + if ( $id == 0 ) { + $this->loadPageData( 'forupdate' ); + $id = $this->getID(); + if ( $id == 0 ) { + $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); + return $status; + } } // Bitfields to further suppress the content @@ -1982,7 +2183,8 @@ class WikiPage extends Page { $bitfield = 'rev_deleted'; } - $dbw->begin(); + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin( __METHOD__ ); // 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 @@ -2019,11 +2221,12 @@ class WikiPage extends Page { # 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 + $ok = ( $dbw->affectedRows() > 0 ); // getArticleID() uses slave, could be laggy if ( !$ok ) { - $dbw->rollback(); - return WikiPage::DELETE_NO_REVISIONS; + $dbw->rollback( __METHOD__ ); + $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); + return $status; } $this->doDeleteUpdates( $id ); @@ -2039,79 +2242,54 @@ class WikiPage extends Page { $logEntry->publish( $logid ); if ( $commit ) { - $dbw->commit(); + $dbw->commit( __METHOD__ ); } wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) ); - return WikiPage::DELETE_SUCCESS; + $status->value = $logid; + return $status; } /** * Do some database updates after deletion * - * @param $id Int: page_id value of the page being deleted + * @param $id Int: page_id value of the page being deleted (B/C, currently unused) */ public function doDeleteUpdates( $id ) { + # update site status DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); - $dbw = wfGetDB( DB_MASTER ); - - # Delete restrictions for it - $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); - - # 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__ ); - - # Delete outgoing links - $dbw->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ ); - $dbw->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ ); - $dbw->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ ); - $dbw->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ ); - $dbw->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ ); - $dbw->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ ); - $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ ); - $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ ); - $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ ); - } - - # 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__ ); - } + # remove secondary indexes, etc + $updates = $this->getDeletionUpdates( ); + DataUpdate::runUpdates( $updates ); # Clear caches - self::onArticleDelete( $this->mTitle ); + WikiPage::onArticleDelete( $this->mTitle ); + + # Reset this object + $this->clear(); # Clear the cached article id so the interface doesn't act like we exist $this->mTitle->resetArticleID( 0 ); } + public function getDeletionUpdates() { + $updates = array( + new LinksDeletionUpdate( $this ), + ); + + //@todo: make a hook to add update objects + //NOTE: deletion updates will be determined by the ContentHandler in the future + return $updates; + } + /** * Roll back the most recent consecutive set of edits to a page * from the same user; fails if there are no eligible edits to * roll back to, e.g. user is the sole contributor. This function * performs permissions checks on $user, then calls commitRollback() * to do the dirty work - * + * * @todo: seperate the business/permission stuff out from backend code * * @param $fromP String: Name of the user whose edits to rollback. @@ -2169,6 +2347,7 @@ class WikiPage extends Page { * * @param $resultDetails Array: contains result-specific array of additional values * @param $guser User The user performing the rollback + * @return array */ public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { global $wgUseRCPatrol, $wgContLang; @@ -2234,7 +2413,7 @@ class WikiPage extends Page { array( /* WHERE */ 'rc_cur_id' => $current->getPage(), 'rc_user_text' => $current->getUserText(), - "rc_timestamp > '{$s->rev_timestamp}'", + 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), ), __METHOD__ ); } @@ -2243,9 +2422,9 @@ class WikiPage extends Page { $target = Revision::newFromId( $s->rev_id ); if ( empty( $summary ) ) { if ( $from == '' ) { // no public user name - $summary = wfMsgForContent( 'revertpage-nouser' ); + $summary = wfMessage( 'revertpage-nouser' ); } else { - $summary = wfMsgForContent( 'revertpage' ); + $summary = wfMessage( 'revertpage' ); } } @@ -2255,7 +2434,14 @@ class WikiPage extends Page { $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) ); - $summary = wfMsgReplaceArgs( $summary, $args ); + if( $summary instanceof Message ) { + $summary = $summary->params( $args )->inContentLanguage()->text(); + } else { + $summary = wfMsgReplaceArgs( $summary, $args ); + } + + # Truncate for whole multibyte characters. + $summary = $wgContLang->truncate( $summary, 255 ); # Save $flags = EDIT_UPDATE; @@ -2432,11 +2618,12 @@ class WikiPage extends Page { if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { $truncatedtext = $wgContLang->truncate( str_replace( "\n", ' ', $newtext ), - max( 0, 250 - - strlen( wfMsgForContent( 'autoredircomment' ) ) + max( 0, 255 + - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) - strlen( $rt->getFullText() ) ) ); - return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext ); + return wfMessage( 'autoredircomment', $rt->getFullText() ) + ->rawParams( $truncatedtext )->inContentLanguage()->text(); } # New page autosummaries @@ -2445,22 +2632,24 @@ class WikiPage extends Page { $truncatedtext = $wgContLang->truncate( str_replace( "\n", ' ', $newtext ), - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) ); + max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) ); - return wfMsgForContent( 'autosumm-new', $truncatedtext ); + return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) + ->inContentLanguage()->text(); } # Blanking autosummaries if ( $oldtext != '' && $newtext == '' ) { - return wfMsgForContent( 'autosumm-blank' ); + return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); } 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' ) ) ) ); + max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) ); - return wfMsgForContent( 'autosumm-replace', $truncatedtext ); + return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) + ->inContentLanguage()->text(); } # If we reach this point, there's no applicable autosummary for our case, so our @@ -2535,12 +2724,16 @@ class WikiPage extends Page { 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' ); + $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text(); } else { if ( $onlyAuthor ) { - $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor ); + $reason = wfMessage( + 'excontentauthor', + '$1', + $onlyAuthor + )->inContentLanguage()->text(); } else { - $reason = wfMsgForContent( 'excontent', '$1' ); + $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text(); } } @@ -2672,6 +2865,7 @@ class WikiPage extends Page { if ( count( $templates_diff ) > 0 ) { # Whee, link updates time. + # Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output. $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); $u->doUpdate(); } @@ -2779,7 +2973,7 @@ class WikiPage extends Page { public function quickEdit( $text, $comment = '', $minor = 0 ) { wfDeprecated( __METHOD__, '1.18' ); global $wgUser; - return $this->doQuickEdit( $text, $wgUser, $comment, $minor ); + $this->doQuickEdit( $text, $wgUser, $comment, $minor ); } /** @@ -2793,6 +2987,8 @@ class WikiPage extends Page { /** * @deprecated since 1.18 + * @param $oldid int + * @return bool */ public function useParserCache( $oldid ) { wfDeprecated( __METHOD__, '1.18' ); @@ -2829,7 +3025,7 @@ class PoolWorkArticleView extends PoolCounterWork { private $text; /** - * @var ParserOutput|false + * @var ParserOutput|bool */ private $parserOutput = false; @@ -2839,7 +3035,7 @@ class PoolWorkArticleView extends PoolCounterWork { private $isDirty = false; /** - * @var Status|false + * @var Status|bool */ private $error = false; @@ -2883,7 +3079,7 @@ class PoolWorkArticleView extends PoolCounterWork { /** * Get a Status object in case of error or false otherwise * - * @return Status|false + * @return Status|bool */ public function getError() { return $this->error; @@ -2973,6 +3169,7 @@ class PoolWorkArticleView extends PoolCounterWork { /** * @param $status Status + * @return bool */ function error( $status ) { $this->error = $status; diff --git a/includes/Xml.php b/includes/Xml.php index 7e5b3cdb..120312dd 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -1,9 +1,28 @@ <?php +/** + * Methods to generate XML. + * + * 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 + */ /** * Module of static functions for generating XML */ - class Xml { /** * Format an XML element with given attributes and, optionally, text content. @@ -40,6 +59,7 @@ class Xml { * The values are passed to Sanitizer::encodeAttribute. * Return null if no attributes given. * @param $attribs Array of attributes for an XML element + * @return null|string */ public static function expandAttributes( $attribs ) { $out = ''; @@ -146,7 +166,7 @@ class Xml { if( is_null( $selected ) ) $selected = ''; if( !is_null( $allmonths ) ) - $options[] = self::option( wfMsg( 'monthsall' ), $allmonths, $selected === $allmonths ); + $options[] = self::option( wfMessage( 'monthsall' )->text(), $allmonths, $selected === $allmonths ); for( $i = 1; $i < 13; $i++ ) $options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i ); return self::openElement( 'select', array( 'id' => $id, 'name' => 'month', 'class' => 'mw-month-selector' ) ) @@ -178,9 +198,9 @@ class Xml { } else { $encYear = ''; } - return Xml::label( wfMsg( 'year' ), 'year' ) . ' '. + return Xml::label( wfMessage( 'year' )->text(), 'year' ) . ' '. Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . ' '. - Xml::label( wfMsg( 'month' ), 'month' ) . ' '. + Xml::label( wfMessage( 'month' )->text(), 'month' ) . ' '. Xml::monthSelector( $encMonth, -1 ); } @@ -189,41 +209,29 @@ class Xml { * * @param string $selected The language code of the selected language * @param boolean $customisedOnly If true only languages which have some content are listed - * @param string $language The ISO code of the language to display the select list in (optional) + * @param string $inLanguage The ISO code of the language to display the select list in (optional) + * @param array $overrideAttrs Override the attributes of the select tag (since 1.20) + * @param Message|null $msg Label message key (since 1.20) * @return array containing 2 items: label HTML and select list HTML */ - public static function languageSelector( $selected, $customisedOnly = true, $language = null ) { + public static function languageSelector( $selected, $customisedOnly = true, $inLanguage = null, $overrideAttrs = array(), Message $msg = null ) { global $wgLanguageCode; - // If a specific language was requested and CLDR is installed, use it - if ( $language && is_callable( array( 'LanguageNames', 'getNames' ) ) ) { - if ( $customisedOnly ) { - $listType = LanguageNames::LIST_MW_SUPPORTED; // Only pull names that have localisation in MediaWiki - } else { - $listType = LanguageNames::LIST_MW; // Pull all languages that are in Names.php - } - // Retrieve the list of languages in the requested language (via CLDR) - $languages = LanguageNames::getNames( - $language, // Code of the requested language - LanguageNames::FALLBACK_NORMAL, // Use fallback chain - $listType - ); - } else { - $languages = Language::getLanguageNames( $customisedOnly ); - } - - // Make sure the site language is in the list; a custom language code might not have a - // defined name... + $include = $customisedOnly ? 'mwfile' : 'mw'; + $languages = Language::fetchLanguageNames( $inLanguage, $include ); + + // Make sure the site language is in the list; + // a custom language code might not have a defined name... if( !array_key_exists( $wgLanguageCode, $languages ) ) { $languages[$wgLanguageCode] = $wgLanguageCode; } - + ksort( $languages ); /** * If a bogus value is set, default to the content language. * Otherwise, no default is selected and the user ends up - * with an Afrikaans interface since it's first in the list. + * with Afrikaans since it's first in the list. */ $selected = isset( $languages[$selected] ) ? $selected : $wgLanguageCode; $options = "\n"; @@ -231,12 +239,15 @@ class Xml { $options .= Xml::option( "$code - $name", $code, ($code == $selected) ) . "\n"; } + $attrs = array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ); + $attrs = array_merge( $attrs, $overrideAttrs ); + + if( $msg === null ) { + $msg = wfMessage( 'yourlanguage' ); + } return array( - Xml::label( wfMsg('yourlanguage'), 'wpUserLanguage' ), - Xml::tags( 'select', - array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ), - $options - ) + Xml::label( $msg->text(), $attrs['id'] ), + Xml::tags( 'select', $attrs, $options ) ); } @@ -254,8 +265,8 @@ class Xml { /** * Shortcut to make a specific element with a class attribute - * @param $text content of the element, will be escaped - * @param $class class name of the span element + * @param $text string content of the element, will be escaped + * @param $class string class name of the span element * @param $tag string element name * @param $attribs array other attributes * @return string @@ -529,8 +540,8 @@ class Xml { /** * Shortcut for creating fieldsets. * - * @param $legend Legend of the fieldset. If evaluates to false, legend is not added. - * @param $content Pre-escaped content for the fieldset. If false, only open fieldset is returned. + * @param $legend string|bool Legend of the fieldset. If evaluates to false, legend is not added. + * @param $content string Pre-escaped content for the fieldset. If false, only open fieldset is returned. * @param $attribs array Any attributes to fieldset-element. * * @return string @@ -761,7 +772,7 @@ class Xml { foreach( $fields as $labelmsg => $input ) { $id = "mw-$labelmsg"; $form .= Xml::openElement( 'tr', array( 'id' => $id ) ); - $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMsgExt( $labelmsg, array('parseinline') ) ); + $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMessage( $labelmsg )->parse() ); $form .= Xml::openElement( 'td', array( 'class' => 'mw-input' ) ) . $input . Xml::closeElement( 'td' ); $form .= Xml::closeElement( 'tr' ); } @@ -769,7 +780,7 @@ class Xml { if( $submitLabel ) { $form .= Xml::openElement( 'tr' ); $form .= Xml::tags( 'td', array(), '' ); - $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMsg( $submitLabel ) ) . Xml::closeElement( 'td' ); + $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMessage( $submitLabel )->text() ) . Xml::closeElement( 'td' ); $form .= Xml::closeElement( 'tr' ); } diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php index be286f8e..b95dd6a5 100644 --- a/includes/XmlTypeCheck.php +++ b/includes/XmlTypeCheck.php @@ -1,4 +1,24 @@ <?php +/** + * XML syntax and type checker. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ class XmlTypeCheck { /** diff --git a/includes/ZhClient.php b/includes/ZhClient.php index d3d79165..4299841b 100644 --- a/includes/ZhClient.php +++ b/includes/ZhClient.php @@ -1,4 +1,24 @@ <?php +/** + * Client for querying zhdaemon. + * + * 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 + */ /** * Client for querying zhdaemon diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php index 58bc98c9..247b1939 100644 --- a/includes/ZhConversion.php +++ b/includes/ZhConversion.php @@ -2570,7 +2570,6 @@ $zh2Hant = array( '龚' => '龔', '龛' => '龕', '龟' => '龜', -'' => '棡', '𠮶' => '嗰', '𡒄' => '壈', '𦈖' => '䌈', @@ -3273,8 +3272,8 @@ $zh2Hant = array( '于伟国' => '于偉國', '于偉國' => '于偉國', '于光新' => '于光新', -'于光远' => '于光遠', '于光遠' => '于光遠', +'于光远' => '于光遠', '于克-蘭多縣' => '于克-蘭多縣', '于克-兰多县' => '于克-蘭多縣', '于克勒' => '于克勒', @@ -3444,8 +3443,8 @@ $zh2Hant = array( '于风政' => '于風政', '于風政' => '于風政', '于飞' => '于飛', -'于飛島' => '于飛島', '于飞岛' => '于飛島', +'于飛島' => '于飛島', '于余曲折' => '于餘曲折', '于鬯' => '于鬯', '于魁智' => '于魁智', @@ -6283,8 +6282,8 @@ $zh2Hant = array( '有只用' => '有只用', '有够赞' => '有夠讚', '有征伐' => '有征伐', -'有征戰' => '有征戰', '有征战' => '有征戰', +'有征戰' => '有征戰', '有征服' => '有征服', '有征讨' => '有征討', '有征討' => '有征討', @@ -6610,6 +6609,8 @@ $zh2Hant = array( '浮松' => '浮鬆', '海上布雷' => '海上佈雷', '海干' => '海乾', +'海淀山后' => '海淀山後', +'海淀山後' => '海淀山後', '海湾布雷' => '海灣佈雷', '涂善妮' => '涂善妮', '涂坤' => '涂坤', @@ -7060,6 +7061,7 @@ $zh2Hant = array( '皇庄' => '皇莊', '皓发' => '皓髮', '皮制服' => '皮制服', +'皮肤' => '皮膚', '皮里春秋' => '皮裡春秋', '皮里阳秋' => '皮裡陽秋', '皮制' => '皮製', @@ -8408,6 +8410,7 @@ $zh2Hant = array( '跌扑' => '跌扑', '跌荡' => '跌蕩', '路签' => '路籤', +'路面' => '路面', '跳梁小丑' => '跳樑小丑', '跳荡' => '跳蕩', '跳表' => '跳錶', @@ -10387,7 +10390,6 @@ $zh2Hans = array( '棖' => '枨', '棗' => '枣', '棟' => '栋', -'棡' => '', '棧' => '栈', '棲' => '栖', '棶' => '梾', @@ -15595,8 +15597,8 @@ $zh2TW = array( '卡塔尔' => '卡達', '打印機' => '印表機', '打印机' => '印表機', -'厄立特里亞' => '厄利垂亞', '厄立特里亚' => '厄利垂亞', +'厄立特里亞' => '厄利垂亞', '厄瓜多尔' => '厄瓜多', '厄瓜多爾' => '厄瓜多', '斯威士兰' => '史瓦濟蘭', @@ -15800,6 +15802,7 @@ $zh2TW = array( '彩线' => '綵線', '彩船' => '綵船', '彩衣' => '綵衣', +'綫' => '線', '缉凶' => '緝凶', '緝兇' => '緝凶', '緝凶' => '緝凶', @@ -15935,6 +15938,30 @@ $zh2TW = array( ); $zh2HK = array( +'505線' => '505綫', +'505线' => '505綫', +'507線' => '507綫', +'507线' => '507綫', +'610線' => '610綫', +'610线' => '610綫', +'614P線' => '614P綫', +'614P线' => '614P綫', +'614线' => '614綫', +'614線' => '614綫', +'615P線' => '615P綫', +'615P线' => '615P綫', +'615线' => '615綫', +'615線' => '615綫', +'705线' => '705綫', +'705線' => '705綫', +'706线' => '706綫', +'706線' => '706綫', +'751P線' => '751P綫', +'751P线' => '751P綫', +'751線' => '751綫', +'751线' => '751綫', +'761P线' => '761P綫', +'761P線' => '761P綫', '“' => '「', '”' => '」', '‘' => '『', @@ -16171,6 +16198,8 @@ $zh2HK = array( '動著者' => '動著者', '動著述' => '動著述', '動著錄' => '動著錄', +'北环线' => '北環綫', +'北環線' => '北環綫', '医院里' => '医院裏', '波札那' => '博茨瓦納', '珍妮弗·卡普里亚蒂' => '卡佩雅蒂', @@ -16418,6 +16447,8 @@ $zh2HK = array( '寫著者' => '寫著者', '寫著述' => '寫著述', '寫著錄' => '寫著錄', +'将军澳线' => '將軍澳綫', +'將軍澳線' => '將軍澳綫', '专辑里' => '專輯裏', '專輯裡' => '專輯裏', '尋著' => '尋着', @@ -16948,6 +16979,10 @@ $zh2HK = array( '本著錄' => '本著錄', '村子里' => '村子裏', '村子裡' => '村子裏', +'东涌线' => '東涌綫', +'東涌線' => '東涌綫', +'東鐵線' => '東鐵綫', +'东铁线' => '東鐵綫', '枕著' => '枕着', '枕著作' => '枕著作', '枕著名' => '枕著名', @@ -16981,6 +17016,8 @@ $zh2HK = array( '樂著錄' => '樂著錄', '寶獅' => '標致', '標誌著' => '標誌着', +'機場快線' => '機場快綫', +'机场快线' => '機場快綫', '機器人' => '機械人', '机器人' => '機械人', '历史里' => '歷史裏', @@ -17013,8 +17050,12 @@ $zh2HK = array( '沉著者' => '沉著者', '沉著述' => '沉著述', '沉著錄' => '沉著錄', +'沙中线' => '沙中綫', +'沙中線' => '沙中綫', '沙地阿拉伯' => '沙特阿拉伯', '沙烏地阿拉伯' => '沙特阿拉伯', +'沙田至中環線' => '沙田至中環綫', +'沙田至中环线' => '沙田至中環綫', '马拉特·萨芬' => '沙芬', '沿著' => '沿着', '沿著作' => '沿著作', @@ -17072,6 +17113,8 @@ $zh2HK = array( '涼著錄' => '涼著錄', '深淵裡' => '深淵裏', '深渊里' => '深渊裏', +'港岛线' => '港島綫', +'港島線' => '港島綫', '渴著' => '渴着', '渴著作' => '渴著作', '渴著名' => '渴著名', @@ -17112,6 +17155,14 @@ $zh2HK = array( '潤著者' => '潤著者', '潤著述' => '潤著述', '潤著錄' => '潤著錄', +'無線劇集' => '無綫劇集', +'无线剧集' => '無綫劇集', +'無線收費' => '無綫收費', +'无线收费' => '無綫收費', +'无线节目' => '無綫節目', +'無線節目' => '無綫節目', +'无线电视' => '無綫電視', +'無線電視' => '無綫電視', '菸' => '煙', '照著' => '照着', '照著作' => '照著作', @@ -17545,6 +17596,8 @@ $zh2HK = array( '苦著錄' => '苦著錄', '苦里' => '苦裏', '苦裡' => '苦裏', +'荃湾线' => '荃灣綫', +'荃灣線' => '荃灣綫', '莫三比克' => '莫桑比克', '賴索托' => '萊索托', '馬自達' => '萬事得', @@ -17628,6 +17681,8 @@ $zh2HK = array( '裹著者' => '裹著者', '裹著述' => '裹著述', '裹著錄' => '裹著錄', +'西铁线' => '西鐵綫', +'西鐵線' => '西鐵綫', '見著' => '見着', '見著作' => '見著作', '見著名' => '見著名', @@ -17636,6 +17691,8 @@ $zh2HK = array( '見著者' => '見著者', '見著述' => '見著述', '見著錄' => '見著錄', +'觀塘線' => '觀塘綫', +'观塘线' => '觀塘綫', '記著' => '記着', '記著作' => '記著作', '記著名' => '記著名', @@ -17819,6 +17876,8 @@ $zh2HK = array( '辦著錄' => '辦著錄', '近角聪信' => '近角聰信', '近角聰信' => '近角聰信', +'迪士尼线' => '迪士尼綫', +'迪士尼線' => '迪士尼綫', '迫著' => '迫着', '追著' => '追着', '追著作' => '追著作', @@ -18078,6 +18137,8 @@ $zh2HK = array( '馬爾地夫' => '馬爾代夫', '馬利共和國' => '馬里共和國', '土豆' => '馬鈴薯', +'馬鞍山線' => '馬鞍山綫', +'马鞍山线' => '馬鞍山綫', '駕著' => '駕着', '駕著作' => '駕著作', '駕著名' => '駕著名', @@ -18192,7 +18253,6 @@ $zh2CN = array( '攜帶型' => '便携式', '資訊理論' => '信息论', '母音' => '元音', -'游標' => '光标', '光碟' => '光盘', '光碟機' => '光驱', '柯林頓' => '克林顿', @@ -18450,8 +18510,8 @@ $zh2SG = array( '方便面' => '快速面', '零钱' => '散钱', '散紙' => '散钱', -'榴蓮' => '榴梿', '榴莲' => '榴梿', +'榴蓮' => '榴梿', '笨豬跳' => '绑紧跳', '蹦极跳' => '绑紧跳', '笑星' => '谐星', diff --git a/includes/ZipDirectoryReader.php b/includes/ZipDirectoryReader.php index 37934aea..0e84583f 100644 --- a/includes/ZipDirectoryReader.php +++ b/includes/ZipDirectoryReader.php @@ -1,4 +1,24 @@ <?php +/** + * ZIP file directories reader, for the purposes of upload verification. + * + * 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 + */ /** * A class for reading ZIP file directories, for the purposes of upload @@ -297,7 +317,7 @@ class ZipDirectoryReader { * Find the location of the central directory, as would be seen by a * ZIP64-compliant reader. * - * @return List containing offset, size and end position. + * @return array List containing offset, size and end position. */ function findZip64CentralDirectory() { // The spec is ambiguous about the exact rules of precedence between the @@ -426,6 +446,7 @@ class ZipDirectoryReader { /** * Interpret ZIP64 "extra field" data and return an associative array. + * @return array|bool */ function unpackZip64Extra( $extraField ) { $extraHeaderInfo = array( @@ -473,8 +494,8 @@ class ZipDirectoryReader { * Get the file contents from a given offset. If there are not enough bytes * in the file to satisfy the request, an exception will be thrown. * - * @param $start The byte offset of the start of the block. - * @param $length The number of bytes to return. If omitted, the remainder + * @param $start int The byte offset of the start of the block. + * @param $length int The number of bytes to return. If omitted, the remainder * of the file will be returned. * * @return string @@ -520,6 +541,7 @@ class ZipDirectoryReader { * If there are not enough bytes in the file to satsify the request, the * return value will be truncated. If a request is made for a segment beyond * the end of the file, an empty string will be returned. + * @return string */ function getSegment( $segIndex ) { if ( !isset( $this->buffer[$segIndex] ) ) { @@ -542,6 +564,7 @@ class ZipDirectoryReader { /** * Get the size of a structure in bytes. See unpack() for the format of $struct. + * @return int */ function getStructSize( $struct ) { $size = 0; @@ -560,9 +583,9 @@ class ZipDirectoryReader { * Unpack a binary structure. This is like the built-in unpack() function * except nicer. * - * @param $string The binary data input + * @param $string string The binary data input * - * @param $struct An associative array giving structure members and their + * @param $struct array An associative array giving structure members and their * types. In the key is the field name. The value may be either an * integer, in which case the field is a little-endian unsigned integer * encoded in the given number of bytes, or an array, in which case the @@ -571,9 +594,9 @@ class ZipDirectoryReader { * - "string": The second array element gives the length of string. * Not null terminated. * - * @param $offset The offset into the string at which to start unpacking. + * @param $offset int The offset into the string at which to start unpacking. * - * @return Unpacked associative array. Note that large integers in the input + * @return array Unpacked associative array. Note that large integers in the input * may be represented as floating point numbers in the return value, so * the use of weak comparison is advised. */ @@ -628,7 +651,8 @@ class ZipDirectoryReader { * boolean. * * @param $value integer - * @param $bitIndex The index of the bit, where 0 is the LSB. + * @param $bitIndex int The index of the bit, where 0 is the LSB. + * @return bool */ function testBit( $value, $bitIndex ) { return (bool)( ( $value >> $bitIndex ) & 1 ); @@ -672,10 +696,10 @@ class ZipDirectoryReader { * Internal exception class. Will be caught by private code. */ class ZipDirectoryReaderError extends Exception { - var $code; + var $errorCode; function __construct( $code ) { - $this->code = $code; + $this->errorCode = $code; parent::__construct( "ZipDirectoryReader error: $code" ); } @@ -683,6 +707,6 @@ class ZipDirectoryReaderError extends Exception { * @return mixed */ function getErrorCode() { - return $this->code; + return $this->errorCode; } } diff --git a/includes/actions/CachedAction.php b/includes/actions/CachedAction.php new file mode 100644 index 00000000..d21f9aeb --- /dev/null +++ b/includes/actions/CachedAction.php @@ -0,0 +1,182 @@ +<?php + +/** + * Abstract action class with scaffolding for caching HTML and other values + * in a single blob. + * + * Before using any of the caching functionality, call startCache. + * After the last call to either getCachedValue or addCachedHTML, call saveCache. + * + * To get a cached value or compute it, use getCachedValue like this: + * $this->getCachedValue( $callback ); + * + * To add HTML that should be cached, use addCachedHTML like this: + * $this->addCachedHTML( $callback ); + * + * The callback function is only called when needed, so do all your expensive + * computations here. This function should returns the HTML to be cached. + * It should not add anything to the PageOutput object! + * + * 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 Action + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @since 1.20 + */ +abstract class CachedAction extends FormlessAction implements ICacheHelper { + + /** + * CacheHelper object to which we forward the non-SpecialPage specific caching work. + * Initialized in startCache. + * + * @since 1.20 + * @var CacheHelper + */ + protected $cacheHelper; + + /** + * If the cache is enabled or not. + * + * @since 1.20 + * @var boolean + */ + protected $cacheEnabled = true; + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + public function setCacheEnabled( $cacheEnabled ) { + $this->cacheHelper->setCacheEnabled( $cacheEnabled ); + } + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { + $this->cacheHelper = new CacheHelper(); + + $this->cacheHelper->setCacheEnabled( $this->cacheEnabled ); + $this->cacheHelper->setOnInitializedHandler( array( $this, 'onCacheInitialized' ) ); + + $keyArgs = $this->getCacheKey(); + + if ( array_key_exists( 'action', $keyArgs ) && $keyArgs['action'] === 'purge' ) { + unset( $keyArgs['action'] ); + } + + $this->cacheHelper->setCacheKey( $keyArgs ); + + if ( $this->getRequest()->getText( 'action' ) === 'purge' ) { + $this->cacheHelper->rebuildOnDemand(); + } + + $this->cacheHelper->startCache( $cacheExpiry, $cacheEnabled ); + } + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + public function getCachedValue( $computeFunction, $args = array(), $key = null ) { + return $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ); + } + + /** + * Add some HTML to be cached. + * This is done by providing a callback function that should + * return the HTML to be added. It will only be called if the + * item is not in the cache yet or when the cache has been invalidated. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array $args + * @param string|null $key + */ + public function addCachedHTML( $computeFunction, $args = array(), $key = null ) { + $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ) ); + } + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + public function saveCache() { + $this->cacheHelper->saveCache(); + } + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + public function setExpiry( $cacheExpiry ) { + $this->cacheHelper->setExpiry( $cacheExpiry ); + } + + /** + * Returns the variables used to constructed the cache key in an array. + * + * @since 1.20 + * + * @return array + */ + protected function getCacheKey() { + return array( + get_class( $this->page ), + $this->getName(), + $this->getLanguage()->getCode() + ); + } + + /** + * Gets called after the cache got initialized. + * + * @since 1.20 + * + * @param boolean $hasCached + */ + public function onCacheInitialized( $hasCached ) { + if ( $hasCached ) { + $this->getOutput()->setSubtitle( $this->cacheHelper->getCachedNotice( $this->getContext() ) ); + } + } + +} diff --git a/includes/actions/CreditsAction.php b/includes/actions/CreditsAction.php index cd083c30..f7152297 100644 --- a/includes/actions/CreditsAction.php +++ b/includes/actions/CreditsAction.php @@ -30,7 +30,7 @@ class CreditsAction extends FormlessAction { } protected function getDescription() { - return wfMsgHtml( 'creditspage' ); + return $this->msg( 'creditspage' )->escaped(); } /** diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 457f67ff..dcd6fe55 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -3,6 +3,22 @@ * Page history * * Split off from Article.php and Skin.php, 2003-12-22 + * + * 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 */ @@ -69,10 +85,9 @@ class HistoryAction extends FormlessAction { /** * Print the history page for an article. - * @return nothing */ function onView() { - global $wgScript, $wgUseFileCache, $wgSquidMaxage; + global $wgScript, $wgUseFileCache; $out = $this->getOutput(); $request = $this->getRequest(); @@ -86,10 +101,6 @@ class HistoryAction extends FormlessAction { wfProfileIn( __METHOD__ ); - if ( $request->getFullRequestURL() == $this->getTitle()->getInternalURL( 'action=history' ) ) { - $out->setSquidMaxage( $wgSquidMaxage ); - } - $this->preCacheMessages(); # Fill in the file cache if not set already @@ -107,8 +118,9 @@ class HistoryAction extends FormlessAction { // Handle atom/RSS feeds. $feedType = $request->getVal( 'feed' ); if ( $feedType ) { + $this->feed( $feedType ); wfProfileOut( __METHOD__ ); - return $this->feed( $feedType ); + return; } // Fail nicely if article doesn't exist. @@ -192,6 +204,11 @@ class HistoryAction extends FormlessAction { * @return ResultWrapper */ function fetchRevisions( $limit, $offset, $direction ) { + // Fail if article doesn't exist. + if( !$this->getTitle()->exists() ) { + return new FakeResultWrapper( array() ); + } + $dbr = wfGetDB( DB_SLAVE ); if ( $direction == HistoryPage::DIR_PREV ) { @@ -231,8 +248,8 @@ class HistoryAction extends FormlessAction { $feed = new $wgFeedClasses[$type]( $this->getTitle()->getPrefixedText() . ' - ' . - wfMsgForContent( 'history-feed-title' ), - wfMsgForContent( 'history-feed-description' ), + $this->msg( 'history-feed-title' )->inContentLanguage()->text(), + $this->msg( 'history-feed-description' )->inContentLanguage()->text(), $this->getTitle()->getFullUrl( 'action=history' ) ); @@ -258,8 +275,8 @@ class HistoryAction extends FormlessAction { function feedEmpty() { return new FeedItem( - wfMsgForContent( 'nohistory' ), - $this->getOutput()->parse( wfMsgForContent( 'history-feed-empty' ) ), + $this->msg( 'nohistory' )->inContentLanguage()->text(), + $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(), $this->getTitle()->getFullUrl(), wfTimestamp( TS_MW ), '', @@ -287,15 +304,14 @@ class HistoryAction extends FormlessAction { ); if ( $rev->getComment() == '' ) { global $wgContLang; - $title = wfMsgForContent( 'history-feed-item-nocomment', + $title = $this->msg( 'history-feed-item-nocomment', $rev->getUserText(), $wgContLang->timeanddate( $rev->getTimestamp() ), $wgContLang->date( $rev->getTimestamp() ), - $wgContLang->time( $rev->getTimestamp() ) - ); + $wgContLang->time( $rev->getTimestamp() ) )->inContentLanguage()->text(); } else { $title = $rev->getUserText() . - wfMsgForContent( 'colon-separator' ) . + $this->msg( 'colon-separator' )->inContentLanguage()->text() . FeedItem::stripComment( $rev->getComment() ); } return new FeedItem( @@ -316,6 +332,10 @@ class HistoryPager extends ReverseChronologicalPager { public $lastRow = false, $counter, $historyPage, $buttons, $conds; protected $oldIdChecked; protected $preventClickjacking = false; + /** + * @var array + */ + protected $parentLens; function __construct( $historyPage, $year = '', $month = '', $tagFilter = '', $conds = array() ) { parent::__construct( $historyPage->getContext() ); @@ -384,7 +404,11 @@ class HistoryPager extends ReverseChronologicalPager { # Do a link batch query $this->mResult->seek( 0 ); $batch = new LinkBatch(); + $revIds = array(); foreach ( $this->mResult as $row ) { + if( $row->rev_parent_id ) { + $revIds[] = $row->rev_parent_id; + } if( !is_null( $row->user_name ) ) { $batch->add( NS_USER, $row->user_name ); $batch->add( NS_USER_TALK, $row->user_name ); @@ -393,6 +417,7 @@ class HistoryPager extends ReverseChronologicalPager { $batch->add( NS_USER_TALK, $row->rev_user_text ); } } + $this->parentLens = Revision::getParentLengths( $this->mDb, $revIds ); $batch->execute(); $this->mResult->seek( 0 ); } @@ -523,7 +548,7 @@ class HistoryPager extends ReverseChronologicalPager { $histLinks = Html::rawElement( 'span', array( 'class' => 'mw-history-histlinks' ), - '(' . $curlink . $this->historyPage->message['pipe-separator'] . $lastlink . ') ' + $this->msg( 'parentheses' )->rawParams( $curlink . $this->historyPage->message['pipe-separator'] . $lastlink )->escaped() ); $s = $histLinks . $diffButtons; @@ -574,26 +599,29 @@ class HistoryPager extends ReverseChronologicalPager { } # Size is always public data - $prevSize = $prevRev ? $prevRev->getSize() : 0; + $prevSize = isset( $this->parentLens[$row->rev_parent_id] ) + ? $this->parentLens[$row->rev_parent_id] + : 0; $sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() ); - $s .= ' . . ' . $sDiff . ' . . '; + $fSize = Linker::formatRevisionSize($rev->getSize()); + $s .= ' <span class="mw-changeslist-separator">. .</span> ' . "$fSize $sDiff"; - $s .= Linker::revComment( $rev, false, true ); + # Text following the character difference is added just before running hooks + $s2 = Linker::revComment( $rev, false, true ); if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) { - $s .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>'; + $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>'; + $classes[] = 'mw-history-line-updated'; } $tools = array(); # Rollback and undo links - if ( $prevRev && - !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) ) ) - { - if ( $latest && !count( $this->getTitle()->getUserPermissionsErrors( 'rollback', $this->getUser() ) ) ) { + if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) { + if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) { $this->preventClickjacking(); $tools[] = '<span class="mw-rollback-link">' . - Linker::buildRollbackLink( $rev ) . '</span>'; + Linker::buildRollbackLink( $rev, $this->getContext() ) . '</span>'; } if ( !$rev->isDeleted( Revision::DELETED_TEXT ) @@ -618,13 +646,20 @@ class HistoryPager extends ReverseChronologicalPager { } if ( $tools ) { - $s .= ' (' . $lang->pipeList( $tools ) . ')'; + $s2 .= ' '. $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped(); } # Tags list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'history' ); $classes = array_merge( $classes, $newClasses ); - $s .= " $tagSummary"; + if ( $tagSummary !== '' ) { + $s2 .= " $tagSummary"; + } + + # Include separator between character difference and following text + if ( $s2 !== '' ) { + $s .= ' <span class="mw-changeslist-separator">. .</span> ' . $s2; + } wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s, &$classes ) ); @@ -649,7 +684,7 @@ class HistoryPager extends ReverseChronologicalPager { $link = Linker::linkKnown( $this->getTitle(), $date, - array(), + array( 'class' => 'mw-changeslist-date' ), array( 'oldid' => $rev->getId() ) ); } else { @@ -784,6 +819,7 @@ class HistoryPager extends ReverseChronologicalPager { /** * Get the "prevent clickjacking" flag + * @return bool */ function getPreventClickjacking() { return $this->preventClickjacking; diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index 70edabce..ae550391 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -1,7 +1,6 @@ <?php /** - * Display informations about a page. - * Very inefficient for the moment. + * Displays information about a page. * * Copyright © 2011 Alexandre Emsenhuber * @@ -24,124 +23,592 @@ */ class InfoAction extends FormlessAction { - + /** + * Returns the name of the action this object responds to. + * + * @return string lowercase + */ public function getName() { return 'info'; } - protected function getDescription() { - return ''; + /** + * Whether this action can still be executed by a blocked user. + * + * @return bool + */ + public function requiresUnblock() { + return false; } + /** + * Whether this action requires the wiki not to be locked. + * + * @return bool + */ public function requiresWrite() { return false; } - public function requiresUnblock() { - return false; + /** + * Shows page information on GET request. + * + * @return string Page information that will be added to the output + */ + public function onView() { + $content = ''; + + // Validate revision + $oldid = $this->page->getOldID(); + if ( $oldid ) { + $revision = $this->page->getRevisionFetched(); + + // Revision is missing + if ( $revision === null ) { + return $this->msg( 'missing-revision', $oldid )->parse(); + } + + // Revision is not current + if ( !$revision->isCurrent() ) { + return $this->msg( 'pageinfo-not-current' )->plain(); + } + } + + // Page header + if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) { + $content .= $this->msg( 'pageinfo-header' )->parse(); + } + + // Hide "This page is a member of # hidden categories" explanation + $content .= Html::element( 'style', array(), + '.mw-hiddenCategoriesExplanation { display: none; }' ); + + // Hide "Templates used on this page" explanation + $content .= Html::element( 'style', array(), + '.mw-templatesUsedExplanation { display: none; }' ); + + // Get page information + $pageInfo = $this->pageInfo(); + + // Allow extensions to add additional information + wfRunHooks( 'InfoAction', array( $this->getContext(), &$pageInfo ) ); + + // Render page information + foreach ( $pageInfo as $header => $infoTable ) { + $content .= $this->makeHeader( $this->msg( "pageinfo-${header}" )->escaped() ); + $table = ''; + foreach ( $infoTable as $infoRow ) { + $name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0]; + $value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1]; + $table = $this->addRow( $table, $name, $value ); + } + $content = $this->addTable( $content, $table ); + } + + // Page footer + if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) { + $content .= $this->msg( 'pageinfo-footer' )->parse(); + } + + // Page credits + /*if ( $this->page->exists() ) { + $content .= Html::rawElement( 'div', array( 'id' => 'mw-credits' ), $this->getContributors() ); + }*/ + + return $content; } - protected function getPageTitle() { - return $this->msg( 'pageinfo-title', $this->getTitle()->getSubjectPage()->getPrefixedText() )->text(); + /** + * Creates a header that can be added to the output. + * + * @param $header The header text. + * @return string The HTML. + */ + protected function makeHeader( $header ) { + global $wgParser; + $spanAttribs = array( 'class' => 'mw-headline', 'id' => $wgParser->guessSectionNameFromWikiText( $header ) ); + return Html::rawElement( 'h2', array(), Html::element( 'span', $spanAttribs, $header ) ); } - public function onView() { - global $wgDisableCounters; - - $title = $this->getTitle()->getSubjectPage(); - - $pageInfo = self::pageCountInfo( $title ); - $talkInfo = self::pageCountInfo( $title->getTalkPage() ); - - return Html::rawElement( 'table', array( 'class' => 'wikitable mw-page-info' ), - Html::rawElement( 'tr', array(), - Html::element( 'th', array(), '' ) . - Html::element( 'th', array(), $this->msg( 'pageinfo-subjectpage' )->text() ) . - Html::element( 'th', array(), $this->msg( 'pageinfo-talkpage' )->text() ) - ) . - Html::rawElement( 'tr', array(), - Html::element( 'th', array( 'colspan' => 3 ), $this->msg( 'pageinfo-header-edits' )->text() ) - ) . - Html::rawElement( 'tr', array(), - Html::element( 'td', array(), $this->msg( 'pageinfo-edits' )->text() ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( $pageInfo['edits'] ) ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( $talkInfo['edits'] ) ) - ) . - Html::rawElement( 'tr', array(), - Html::element( 'td', array(), $this->msg( 'pageinfo-authors' )->text() ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( $pageInfo['authors'] ) ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( $talkInfo['authors'] ) ) - ) . - ( !$this->getUser()->isAllowed( 'unwatchedpages' ) ? '' : - Html::rawElement( 'tr', array(), - Html::element( 'th', array( 'colspan' => 3 ), $this->msg( 'pageinfo-header-watchlist' )->text() ) - ) . - Html::rawElement( 'tr', array(), - Html::element( 'td', array(), $this->msg( 'pageinfo-watchers' )->text() ) . - Html::element( 'td', array( 'colspan' => 2 ), $this->getLanguage()->formatNum( $pageInfo['watchers'] ) ) - ) - ). - ( $wgDisableCounters ? '' : - Html::rawElement( 'tr', array(), - Html::element( 'th', array( 'colspan' => 3 ), $this->msg( 'pageinfo-header-views' )->text() ) - ) . - Html::rawElement( 'tr', array(), - Html::element( 'td', array(), $this->msg( 'pageinfo-views' )->text() ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( $pageInfo['views'] ) ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( $talkInfo['views'] ) ) - ) . - Html::rawElement( 'tr', array(), - Html::element( 'td', array(), $this->msg( 'pageinfo-viewsperedit' )->text() ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( sprintf( '%.2f', $pageInfo['edits'] ? $pageInfo['views'] / $pageInfo['edits'] : 0 ) ) ) . - Html::element( 'td', array(), $this->getLanguage()->formatNum( sprintf( '%.2f', $talkInfo['edits'] ? $talkInfo['views'] / $talkInfo['edits'] : 0 ) ) ) - ) - ) + /** + * Adds a row to a table that will be added to the content. + * + * @param $table string The table that will be added to the content + * @param $name string The name of the row + * @param $value string The value of the row + * @return string The table with the row added + */ + protected function addRow( $table, $name, $value ) { + return $table . Html::rawElement( 'tr', array(), + Html::rawElement( 'td', array( 'style' => 'vertical-align: top;' ), $name ) . + Html::rawElement( 'td', array(), $value ) ); } /** - * Return the total number of edits and number of unique editors - * on a given page. If page does not exist, returns false. + * Adds a table to the content that will be added to the output. * - * @param $title Title object - * @return mixed array or boolean false + * @param $content string The content that will be added to the output + * @param $table string The table + * @return string The content with the table added + */ + protected function addTable( $content, $table ) { + return $content . Html::rawElement( 'table', array( 'class' => 'wikitable mw-page-info' ), + $table ); + } + + /** + * Returns page information in an easily-manipulated format. Array keys are used so extensions + * may add additional information in arbitrary positions. Array values are arrays with one + * element to be rendered as a header, arrays with two elements to be rendered as a table row. */ - public static function pageCountInfo( $title ) { - $id = $title->getArticleId(); + protected function pageInfo() { + global $wgContLang, $wgRCMaxAge; + + $user = $this->getUser(); + $lang = $this->getLanguage(); + $title = $this->getTitle(); + $id = $title->getArticleID(); + + // Get page information that would be too "expensive" to retrieve by normal means + $pageCounts = self::pageCounts( $title, $user ); + + // Get page properties $dbr = wfGetDB( DB_SLAVE ); + $result = $dbr->select( + 'page_props', + array( 'pp_propname', 'pp_value' ), + array( 'pp_page' => $id ), + __METHOD__ + ); - $watchers = (int)$dbr->selectField( - 'watchlist', - 'COUNT(*)', - array( - 'wl_title' => $title->getDBkey(), - 'wl_namespace' => $title->getNamespace() + $pageProperties = array(); + foreach ( $result as $row ) { + $pageProperties[$row->pp_propname] = $row->pp_value; + } + + // Basic information + $pageInfo = array(); + $pageInfo['header-basic'] = array(); + + // Display title + $displayTitle = $title->getPrefixedText(); + if ( !empty( $pageProperties['displaytitle'] ) ) { + $displayTitle = $pageProperties['displaytitle']; + } + + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-display-title' ), $displayTitle + ); + + // Default sort key + $sortKey = $title->getCategorySortKey(); + if ( !empty( $pageProperties['defaultsort'] ) ) { + $sortKey = $pageProperties['defaultsort']; + } + + $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-default-sort' ), $sortKey ); + + // Page length (in bytes) + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() ) + ); + + // Page ID (number not localised, as it's a database ID) + $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-article-id' ), $id ); + + // Search engine status + $pOutput = new ParserOutput(); + if ( isset( $pageProperties['noindex'] ) ) { + $pOutput->setIndexPolicy( 'noindex' ); + } + + // Use robot policy logic + $policy = $this->page->getRobotPolicy( 'view', $pOutput ); + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-robot-policy' ), $this->msg( "pageinfo-robot-${policy['index']}" ) + ); + + if ( isset( $pageCounts['views'] ) ) { + // Number of views + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-views' ), $lang->formatNum( $pageCounts['views'] ) + ); + } + + if ( isset( $pageCounts['watchers'] ) ) { + // Number of page watchers + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-watchers' ), $lang->formatNum( $pageCounts['watchers'] ) + ); + } + + // Redirects to this page + $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ); + $pageInfo['header-basic'][] = array( + Linker::link( + $whatLinksHere, + $this->msg( 'pageinfo-redirects-name' )->escaped(), + array(), + array( 'hidelinks' => 1, 'hidetrans' => 1 ) ), - __METHOD__ + $this->msg( 'pageinfo-redirects-value' ) + ->numParams( count( $title->getRedirectsHere() ) ) ); - $edits = (int)$dbr->selectField( + // Subpages of this page, if subpages are enabled for the current NS + if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) { + $prefixIndex = SpecialPage::getTitleFor( 'Prefixindex', $title->getPrefixedText() . '/' ); + $pageInfo['header-basic'][] = array( + Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ), + $this->msg( 'pageinfo-subpages-value' ) + ->numParams( + $pageCounts['subpages']['total'], + $pageCounts['subpages']['redirects'], + $pageCounts['subpages']['nonredirects'] ) + ); + } + + // Page protection + $pageInfo['header-restrictions'] = array(); + + // Page protection + foreach ( $title->getRestrictionTypes() as $restrictionType ) { + $protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) ); + + if ( $protectionLevel == '' ) { + // Allow all users + $message = $this->msg( 'protect-default' )->escaped(); + } else { + // Administrators only + $message = $this->msg( "protect-level-$protectionLevel" ); + if ( $message->isDisabled() ) { + // Require "$1" permission + $message = $this->msg( "protect-fallback", $protectionLevel )->parse(); + } else { + $message = $message->escaped(); + } + } + + $pageInfo['header-restrictions'][] = array( + $this->msg( "restriction-$restrictionType" ), $message + ); + } + + if ( !$this->page->exists() ) { + return $pageInfo; + } + + // Edit history + $pageInfo['header-edits'] = array(); + + $firstRev = $this->page->getOldestRevision(); + + // Page creator + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-firstuser' ), + Linker::revUserTools( $firstRev ) + ); + + // Date of page creation + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-firsttime' ), + Linker::linkKnown( + $title, + $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ), + array(), + array( 'oldid' => $firstRev->getId() ) + ) + ); + + // Latest editor + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-lastuser' ), + Linker::revUserTools( $this->page->getRevision() ) + ); + + // Date of latest edit + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-lasttime' ), + Linker::linkKnown( + $title, + $lang->userTimeAndDate( $this->page->getTimestamp(), $user ), + array(), + array( 'oldid' => $this->page->getLatest() ) + ) + ); + + // Total number of edits + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-edits' ), $lang->formatNum( $pageCounts['edits'] ) + ); + + // Total number of distinct authors + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-authors' ), $lang->formatNum( $pageCounts['authors'] ) + ); + + // Recent number of edits (within past 30 days) + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $wgRCMaxAge ) ), + $lang->formatNum( $pageCounts['recent_edits'] ) + ); + + // Recent number of distinct authors + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-recent-authors' ), $lang->formatNum( $pageCounts['recent_authors'] ) + ); + + // Array of MagicWord objects + $magicWords = MagicWord::getDoubleUnderscoreArray(); + + // Array of magic word IDs + $wordIDs = $magicWords->names; + + // Array of IDs => localized magic words + $localizedWords = $wgContLang->getMagicWords(); + + $listItems = array(); + foreach ( $pageProperties as $property => $value ) { + if ( in_array( $property, $wordIDs ) ) { + $listItems[] = Html::element( 'li', array(), $localizedWords[$property][1] ); + } + } + + $localizedList = Html::rawElement( 'ul', array(), implode( '', $listItems ) ); + $hiddenCategories = $this->page->getHiddenCategories(); + $transcludedTemplates = $title->getTemplateLinksFrom(); + + if ( count( $listItems ) > 0 + || count( $hiddenCategories ) > 0 + || count( $transcludedTemplates ) > 0 ) { + // Page properties + $pageInfo['header-properties'] = array(); + + // Magic words + if ( count( $listItems ) > 0 ) { + $pageInfo['header-properties'][] = array( + $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ), + $localizedList + ); + } + + // Hidden categories + if ( count( $hiddenCategories ) > 0 ) { + $pageInfo['header-properties'][] = array( + $this->msg( 'pageinfo-hidden-categories' ) + ->numParams( count( $hiddenCategories ) ), + Linker::formatHiddenCategories( $hiddenCategories ) + ); + } + + // Transcluded templates + if ( count( $transcludedTemplates ) > 0 ) { + $pageInfo['header-properties'][] = array( + $this->msg( 'pageinfo-templates' ) + ->numParams( count( $transcludedTemplates ) ), + Linker::formatTemplates( $transcludedTemplates ) + ); + } + } + + return $pageInfo; + } + + /** + * Returns page counts that would be too "expensive" to retrieve by normal means. + * + * @param $title Title object + * @param $user User object + * @return array + */ + protected static function pageCounts( $title, $user ) { + global $wgRCMaxAge, $wgDisableCounters; + + wfProfileIn( __METHOD__ ); + $id = $title->getArticleID(); + + $dbr = wfGetDB( DB_SLAVE ); + $result = array(); + + if ( !$wgDisableCounters ) { + // Number of views + $views = (int) $dbr->selectField( + 'page', + 'page_counter', + array( 'page_id' => $id ), + __METHOD__ + ); + $result['views'] = $views; + } + + if ( $user->isAllowed( 'unwatchedpages' ) ) { + // Number of page watchers + $watchers = (int) $dbr->selectField( + 'watchlist', + 'COUNT(*)', + array( + 'wl_namespace' => $title->getNamespace(), + 'wl_title' => $title->getDBkey(), + ), + __METHOD__ + ); + $result['watchers'] = $watchers; + } + + // Total number of edits + $edits = (int) $dbr->selectField( 'revision', 'COUNT(rev_page)', array( 'rev_page' => $id ), __METHOD__ ); + $result['edits'] = $edits; - $authors = (int)$dbr->selectField( + // Total number of distinct authors + $authors = (int) $dbr->selectField( 'revision', 'COUNT(DISTINCT rev_user_text)', array( 'rev_page' => $id ), __METHOD__ ); + $result['authors'] = $authors; + + // "Recent" threshold defined by $wgRCMaxAge + $threshold = $dbr->timestamp( time() - $wgRCMaxAge ); + + // Recent number of edits + $edits = (int) $dbr->selectField( + 'revision', + 'COUNT(rev_page)', + array( + 'rev_page' => $id , + "rev_timestamp >= $threshold" + ), + __METHOD__ + ); + $result['recent_edits'] = $edits; - $views = (int)$dbr->selectField( - 'page', - 'page_counter', - array( 'page_id' => $id ), + // Recent number of distinct authors + $authors = (int) $dbr->selectField( + 'revision', + 'COUNT(DISTINCT rev_user_text)', + array( + 'rev_page' => $id, + "rev_timestamp >= $threshold" + ), __METHOD__ ); + $result['recent_authors'] = $authors; + + // Subpages (if enabled) + if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) { + $conds = array( 'page_namespace' => $title->getNamespace() ); + $conds[] = 'page_title ' . $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() ); - return array( 'watchers' => $watchers, 'edits' => $edits, - 'authors' => $authors, 'views' => $views ); + // Subpages of this page (redirects) + $conds['page_is_redirect'] = 1; + $result['subpages']['redirects'] = (int) $dbr->selectField( + 'page', + 'COUNT(page_id)', + $conds, + __METHOD__ ); + + // Subpages of this page (non-redirects) + $conds['page_is_redirect'] = 0; + $result['subpages']['nonredirects'] = (int) $dbr->selectField( + 'page', + 'COUNT(page_id)', + $conds, + __METHOD__ + ); + + // Subpages of this page (total) + $result['subpages']['total'] = $result['subpages']['redirects'] + + $result['subpages']['nonredirects']; + } + + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Returns the name that goes in the <h1> page title. + * + * @return string + */ + protected function getPageTitle() { + return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text(); + } + + /** + * Get a list of contributors of $article + * @return string: html + */ + protected function getContributors() { + global $wgHiddenPrefs; + + $contributors = $this->page->getContributors(); + $real_names = array(); + $user_names = array(); + $anon_ips = array(); + + # Sift for real versus user names + foreach ( $contributors as $user ) { + $page = $user->isAnon() + ? SpecialPage::getTitleFor( 'Contributions', $user->getName() ) + : $user->getUserPage(); + + if ( $user->getID() == 0 ) { + $anon_ips[] = Linker::link( $page, htmlspecialchars( $user->getName() ) ); + } elseif ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { + $real_names[] = Linker::link( $page, htmlspecialchars( $user->getRealName() ) ); + } else { + $user_names[] = Linker::link( $page, htmlspecialchars( $user->getName() ) ); + } + } + + $lang = $this->getLanguage(); + + $real = $lang->listToText( $real_names ); + + # "ThisSite user(s) A, B and C" + if ( count( $user_names ) ) { + $user = $this->msg( 'siteusers' )->rawParams( $lang->listToText( $user_names ) )->params( + count( $user_names ) )->escaped(); + } else { + $user = false; + } + + if ( count( $anon_ips ) ) { + $anon = $this->msg( 'anonusers' )->rawParams( $lang->listToText( $anon_ips ) )->params( + count( $anon_ips ) )->escaped(); + } else { + $anon = false; + } + + # This is the big list, all mooshed together. We sift for blank strings + $fulllist = array(); + foreach ( array( $real, $user, $anon ) as $s ) { + if ( $s !== '' ) { + array_push( $fulllist, $s ); + } + } + + $count = count( $fulllist ); + # "Based on work by ..." + return $count + ? $this->msg( 'othercontribs' )->rawParams( + $lang->listToText( $fulllist ) )->params( $count )->escaped() + : ''; + } + + /** + * Returns the description that goes below the <h1> tag. + * + * @return string + */ + protected function getDescription() { + return ''; } } diff --git a/includes/actions/PurgeAction.php b/includes/actions/PurgeAction.php index 21a6d904..cd58889d 100644 --- a/includes/actions/PurgeAction.php +++ b/includes/actions/PurgeAction.php @@ -79,15 +79,15 @@ class PurgeAction extends FormAction { } protected function alterForm( HTMLForm $form ) { - $form->setSubmitText( wfMsg( 'confirm_purge_button' ) ); + $form->setSubmitTextMsg( 'confirm_purge_button' ); } protected function preText() { - return wfMessage( 'confirm-purge-top' )->parse(); + return $this->msg( 'confirm-purge-top' )->parse(); } protected function postText() { - return wfMessage( 'confirm-purge-bottom' )->parse(); + return $this->msg( 'confirm-purge-bottom' )->parse(); } public function onSuccess() { diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index e4c6b3e0..174ca3f8 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -7,7 +7,20 @@ * * Based on HistoryPage and SpecialExport * - * License: GPL (http://www.gnu.org/copyleft/gpl.html) + * 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 * * @author Gabriel Wicke <wicke@wikidev.net> * @file @@ -120,10 +133,13 @@ class RawAction extends FormlessAction { // If it's a MediaWiki message we can just hit the message cache if ( $request->getBool( 'usemsgcache' ) && $title->getNamespace() == NS_MEDIAWIKI ) { - $key = $title->getDBkey(); - $msg = wfMessage( $key )->inContentLanguage(); - # If the message doesn't exist, return a blank - $text = !$msg->exists() ? '' : $msg->plain(); + // The first "true" is to use the database, the second is to use the content langue + // and the last one is to specify the message key already contains the language in it ("/de", etc.) + $text = MessageCache::singleton()->get( $title->getDBkey(), true, true, true ); + // If the message doesn't exist, return a blank + if ( $text === false ) { + $text = ''; + } } else { // Get it from the DB $rev = Revision::newFromTitle( $title, $this->getOldId() ); diff --git a/includes/actions/RevertAction.php b/includes/actions/RevertAction.php index f9497f4b..77434384 100644 --- a/includes/actions/RevertAction.php +++ b/includes/actions/RevertAction.php @@ -75,8 +75,8 @@ class RevertFileAction extends FormAction { } protected function alterForm( HTMLForm $form ) { - $form->setWrapperLegend( wfMsgHtml( 'filerevert-legend' ) ); - $form->setSubmitText( wfMsg( 'filerevert-submit' ) ); + $form->setWrapperLegendMsg( 'filerevert-legend' ); + $form->setSubmitTextMsg( 'filerevert-submit' ); $form->addHiddenField( 'oldimage', $this->getRequest()->getText( 'oldimage' ) ); } @@ -85,22 +85,28 @@ class RevertFileAction extends FormAction { $timestamp = $this->oldFile->getTimestamp(); + $user = $this->getUser(); + $lang = $this->getLanguage(); + $userDate = $lang->userDate( $timestamp, $user ); + $userTime = $lang->userTime( $timestamp, $user ); + $siteDate = $wgContLang->date( $timestamp, false, false ); + $siteTime = $wgContLang->time( $timestamp, false, false ); + return array( 'intro' => array( 'type' => 'info', 'vertical-label' => true, 'raw' => true, - 'default' => wfMsgExt( 'filerevert-intro', 'parse', $this->getTitle()->getText(), - $this->getLanguage()->date( $timestamp, true ), $this->getLanguage()->time( $timestamp, true ), + 'default' => $this->msg( 'filerevert-intro', + $this->getTitle()->getText(), $userDate, $userTime, wfExpandUrl( $this->page->getFile()->getArchiveUrl( $this->getRequest()->getText( 'oldimage' ) ), - PROTO_CURRENT - ) ) + PROTO_CURRENT ) )->parseAsBlock() ), 'comment' => array( 'type' => 'text', 'label-message' => 'filerevert-comment', - 'default' => wfMsgForContent( 'filerevert-defaultcomment', - $wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ), + 'default' => $this->msg( 'filerevert-defaultcomment', $siteDate, $siteTime + )->inContentLanguage()->text() ) ); } @@ -114,17 +120,21 @@ class RevertFileAction extends FormAction { public function onSuccess() { $timestamp = $this->oldFile->getTimestamp(); - $this->getOutput()->addHTML( wfMsgExt( 'filerevert-success', 'parse', $this->getTitle()->getText(), - $this->getLanguage()->date( $timestamp, true ), - $this->getLanguage()->time( $timestamp, true ), + $user = $this->getUser(); + $lang = $this->getLanguage(); + $userDate = $lang->userDate( $timestamp, $user ); + $userTime = $lang->userTime( $timestamp, $user ); + + $this->getOutput()->addWikiMsg( 'filerevert-success', $this->getTitle()->getText(), + $userDate, $userTime, wfExpandUrl( $this->page->getFile()->getArchiveUrl( $this->getRequest()->getText( 'oldimage' ) ), PROTO_CURRENT - ) ) ); + ) ); $this->getOutput()->returnToMain( false, $this->getTitle() ); } protected function getPageTitle() { - return wfMsg( 'filerevert', $this->getTitle()->getText() ); + return $this->msg( 'filerevert', $this->getTitle()->getText() ); } protected function getDescription() { diff --git a/includes/actions/RevisiondeleteAction.php b/includes/actions/RevisiondeleteAction.php index f07e493d..14da2fcf 100644 --- a/includes/actions/RevisiondeleteAction.php +++ b/includes/actions/RevisiondeleteAction.php @@ -44,6 +44,6 @@ class RevisiondeleteAction extends FormlessAction { public function show() { $special = SpecialPageFactory::getPage( 'Revisiondelete' ); $special->setContext( $this->getContext() ); - $special->execute( '' ); + $special->run( '' ); } } diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index ebb34c78..0d9a9027 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -63,7 +63,7 @@ class RollbackAction extends FormlessAction { $current = $details['current']; if ( $current->getComment() != '' ) { - $this->getOutput()->addHTML( wfMessage( 'editcomment' )->rawParams( + $this->getOutput()->addHTML( $this->msg( 'editcomment' )->rawParams( Linker::formatComment( $current->getComment() ) )->parse() ); } } @@ -97,7 +97,7 @@ class RollbackAction extends FormlessAction { $this->getOutput()->setRobotPolicy( 'noindex,nofollow' ); if ( $current->getUserText() === '' ) { - $old = wfMsg( 'rev-deleted-user' ); + $old = $this->msg( 'rev-deleted-user' )->escaped(); } else { $old = Linker::userLink( $current->getUser(), $current->getUserText() ) . Linker::userToolLinks( $current->getUser(), $current->getUserText() ); @@ -105,7 +105,7 @@ class RollbackAction extends FormlessAction { $new = Linker::userLink( $target->getUser(), $target->getUserText() ) . Linker::userToolLinks( $target->getUser(), $target->getUserText() ); - $this->getOutput()->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); + $this->getOutput()->addHTML( $this->msg( 'rollback-success' )->rawParams( $old, $new )->parseAsBlock() ); $this->getOutput()->returnToMain( false, $this->getTitle() ); if ( !$request->getBool( 'hidediff', false ) && !$this->getUser()->getBoolOption( 'norollbackdiff', false ) ) { diff --git a/includes/actions/ViewAction.php b/includes/actions/ViewAction.php index 4e37381b..d57585ee 100644 --- a/includes/actions/ViewAction.php +++ b/includes/actions/ViewAction.php @@ -34,9 +34,6 @@ class ViewAction extends FormlessAction { } public function show(){ - global $wgSquidMaxage; - - $this->getOutput()->setSquidMaxage( $wgSquidMaxage ); $this->page->view(); } diff --git a/includes/actions/WatchAction.php b/includes/actions/WatchAction.php index 63d9b151..e2636452 100644 --- a/includes/actions/WatchAction.php +++ b/includes/actions/WatchAction.php @@ -31,7 +31,7 @@ class WatchAction extends FormAction { } protected function getDescription() { - return wfMsgHtml( 'addwatch' ); + return $this->msg( 'addwatch' )->escaped(); } /** @@ -136,11 +136,11 @@ class WatchAction extends FormAction { } protected function alterForm( HTMLForm $form ) { - $form->setSubmitText( wfMsg( 'confirm-watch-button' ) ); + $form->setSubmitTextMsg( 'confirm-watch-button' ); } protected function preText() { - return wfMessage( 'confirm-watch-top' )->parse(); + return $this->msg( 'confirm-watch-top' )->parse(); } public function onSuccess() { @@ -155,7 +155,7 @@ class UnwatchAction extends WatchAction { } protected function getDescription() { - return wfMsg( 'removewatch' ); + return $this->msg( 'removewatch' )->escaped(); } public function onSubmit( $data ) { @@ -166,11 +166,11 @@ class UnwatchAction extends WatchAction { } protected function alterForm( HTMLForm $form ) { - $form->setSubmitText( wfMsg( 'confirm-unwatch-button' ) ); + $form->setSubmitTextMsg( 'confirm-unwatch-button' ); } protected function preText() { - return wfMessage( 'confirm-unwatch-top' )->parse(); + return $this->msg( 'confirm-unwatch-top' )->parse(); } public function onSuccess() { diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index a586f688..875a3814 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -4,7 +4,7 @@ * * Created on Sep 5, 2006 * - * Copyright © 2006, 2010 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006, 2010 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -51,9 +51,16 @@ abstract class ApiBase extends ContextSource { const PARAM_MIN = 5; // Lowest value allowed for a parameter. Only applies if TYPE='integer' const PARAM_ALLOW_DUPLICATES = 6; // Boolean, do we allow the same value to be set more than once when ISMULTI=true const PARAM_DEPRECATED = 7; // Boolean, is the parameter deprecated (will show a warning) + /// @since 1.17 const PARAM_REQUIRED = 8; // Boolean, is the parameter required? + /// @since 1.17 const PARAM_RANGE_ENFORCE = 9; // Boolean, if MIN/MAX are set, enforce (die) these? Only applies if TYPE='integer' Use with extreme caution + const PROP_ROOT = 'ROOT'; // Name of property group that is on the root element of the result, i.e. not part of a list + const PROP_LIST = 'LIST'; // Boolean, is the result multiple items? Defaults to true for query modules, to false for other modules + const PROP_TYPE = 0; // Type of the property, uses same format as PARAM_TYPE + const PROP_NULLABLE = 1; // Boolean, can the property be not included in the result? Defaults to false + const LIMIT_BIG1 = 500; // Fast query, std user limit const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit const LIMIT_SML1 = 50; // Slow query, std user limit @@ -127,7 +134,7 @@ abstract class ApiBase extends ContextSource { /** * Get the name of the module as shown in the profiler log * - * @param $db DatabaseBase + * @param $db DatabaseBase|bool * * @return string */ @@ -280,12 +287,12 @@ abstract class ApiBase extends ContextSource { if ( is_numeric( $k ) ) { $msg .= " $v\n"; } else { - $v .= ":"; if ( is_array( $v ) ) { $msgExample = implode( "\n", array_map( array( $this, 'indentExampleText' ), $v ) ); } else { $msgExample = " $v"; } + $msgExample .= ":"; $msg .= wordwrap( $msgExample, 100, "\n" ) . "\n $k\n"; } } @@ -365,27 +372,38 @@ abstract class ApiBase extends ContextSource { $desc = implode( $paramPrefix, $desc ); } + //handle shorthand if ( !is_array( $paramSettings ) ) { $paramSettings = array( self::PARAM_DFLT => $paramSettings, ); } - $deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] ) ? - $paramSettings[self::PARAM_DEPRECATED] : false; - if ( $deprecated ) { + //handle missing type + if ( !isset( $paramSettings[ApiBase::PARAM_TYPE] ) ) { + $dflt = isset( $paramSettings[ApiBase::PARAM_DFLT] ) ? $paramSettings[ApiBase::PARAM_DFLT] : null; + if ( is_bool( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'boolean'; + } elseif ( is_string( $dflt ) || is_null( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'string'; + } elseif ( is_int( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'integer'; + } + } + + if ( isset( $paramSettings[self::PARAM_DEPRECATED] ) && $paramSettings[self::PARAM_DEPRECATED] ) { $desc = "DEPRECATED! $desc"; } - $required = isset( $paramSettings[self::PARAM_REQUIRED] ) ? - $paramSettings[self::PARAM_REQUIRED] : false; - if ( $required ) { + if ( isset( $paramSettings[self::PARAM_REQUIRED] ) && $paramSettings[self::PARAM_REQUIRED] ) { $desc .= $paramPrefix . "This parameter is required"; } $type = isset( $paramSettings[self::PARAM_TYPE] ) ? $paramSettings[self::PARAM_TYPE] : null; if ( isset( $type ) ) { - if ( isset( $paramSettings[self::PARAM_ISMULTI] ) && $paramSettings[self::PARAM_ISMULTI] ) { + $hintPipeSeparated = true; + $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) ? $paramSettings[self::PARAM_ISMULTI] : false; + if ( $multi ) { $prompt = 'Values (separate with \'|\'): '; } else { $prompt = 'One value: '; @@ -393,7 +411,7 @@ abstract class ApiBase extends ContextSource { if ( is_array( $type ) ) { $choices = array(); - $nothingPrompt = false; + $nothingPrompt = ''; foreach ( $type as $t ) { if ( $t === '' ) { $nothingPrompt = 'Can be empty, or '; @@ -404,6 +422,7 @@ abstract class ApiBase extends ContextSource { $desc .= $paramPrefix . $nothingPrompt . $prompt; $choicesstring = implode( ', ', $choices ); $desc .= wordwrap( $choicesstring, 100, $descWordwrap ); + $hintPipeSeparated = false; } else { switch ( $type ) { case 'namespace': @@ -411,6 +430,7 @@ abstract class ApiBase extends ContextSource { $desc .= $paramPrefix . $prompt; $desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ), 100, $descWordwrap ); + $hintPipeSeparated = false; break; case 'limit': $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]}"; @@ -420,37 +440,39 @@ abstract class ApiBase extends ContextSource { $desc .= ' allowed'; break; case 'integer': + $s = $multi ? 's' : ''; $hasMin = isset( $paramSettings[self::PARAM_MIN] ); $hasMax = isset( $paramSettings[self::PARAM_MAX] ); if ( $hasMin || $hasMax ) { if ( !$hasMax ) { - $intRangeStr = "The value must be no less than {$paramSettings[self::PARAM_MIN]}"; + $intRangeStr = "The value$s must be no less than {$paramSettings[self::PARAM_MIN]}"; } elseif ( !$hasMin ) { - $intRangeStr = "The value must be no more than {$paramSettings[self::PARAM_MAX]}"; + $intRangeStr = "The value$s must be no more than {$paramSettings[self::PARAM_MAX]}"; } else { - $intRangeStr = "The value must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; + $intRangeStr = "The value$s must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; } $desc .= $paramPrefix . $intRangeStr; } break; } + } - if ( isset( $paramSettings[self::PARAM_ISMULTI] ) ) { - $isArray = is_array( $paramSettings[self::PARAM_TYPE] ); + if ( $multi ) { + if ( $hintPipeSeparated ) { + $desc .= $paramPrefix . "Separate values with '|'"; + } - if ( !$isArray - || $isArray && count( $paramSettings[self::PARAM_TYPE] ) > self::LIMIT_SML1 ) { - $desc .= $paramPrefix . "Maximum number of values " . - self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)"; - } + $isArray = is_array( $type ); + if ( !$isArray + || $isArray && count( $type ) > self::LIMIT_SML1 ) { + $desc .= $paramPrefix . "Maximum number of values " . + self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)"; } } } - $default = is_array( $paramSettings ) - ? ( isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null ) - : $paramSettings; + $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null; if ( !is_null( $default ) && $default !== false ) { $desc .= $paramPrefix . "Default: $default"; } @@ -512,7 +534,7 @@ abstract class ApiBase extends ContextSource { /** * Returns usage examples for this module. Return false if no examples are available. - * @return false|string|array + * @return bool|string|array */ protected function getExamples() { return false; @@ -523,7 +545,7 @@ abstract class ApiBase extends ContextSource { * value) or (parameter name) => (array with PARAM_* constants as keys) * Don't call this function directly: use getFinalParams() to allow * hooks to modify parameters as needed. - * @return array or false + * @return array|bool */ protected function getAllowedParams() { return false; @@ -533,7 +555,7 @@ abstract class ApiBase extends ContextSource { * Returns an array of parameter descriptions. * Don't call this functon directly: use getFinalParamDescription() to * allow hooks to modify descriptions as needed. - * @return array or false + * @return array|bool False on no parameter descriptions */ protected function getParamDescription() { return false; @@ -543,7 +565,7 @@ abstract class ApiBase extends ContextSource { * Get final list of parameters, after hooks have had a chance to * tweak it as needed. * - * @return array or false + * @return array|Bool False on no parameters */ public function getFinalParams() { $params = $this->getAllowedParams(); @@ -555,7 +577,7 @@ abstract class ApiBase extends ContextSource { * Get final parameter descriptions, after hooks have had a chance to tweak it as * needed. * - * @return array + * @return array|bool False on no parameter descriptions */ public function getFinalParamDescription() { $desc = $this->getParamDescription(); @@ -564,11 +586,56 @@ abstract class ApiBase extends ContextSource { } /** - * Get final module description, after hooks have had a chance to tweak it as + * Returns possible properties in the result, grouped by the value of the prop parameter + * that shows them. + * + * Properties that are shown always are in a group with empty string as a key. + * Properties that can be shown by several values of prop are included multiple times. + * If some properties are part of a list and some are on the root object (see ApiQueryQueryPage), + * those on the root object are under the key PROP_ROOT. + * The array can also contain a boolean under the key PROP_LIST, + * indicating whether the result is a list. + * + * Don't call this functon directly: use getFinalResultProperties() to + * allow hooks to modify descriptions as needed. + * + * @return array|bool False on no properties + */ + protected function getResultProperties() { + return false; + } + + /** + * Get final possible result properties, after hooks have had a chance to tweak it as * needed. * * @return array */ + public function getFinalResultProperties() { + $properties = $this->getResultProperties(); + wfRunHooks( 'APIGetResultProperties', array( $this, &$properties ) ); + return $properties; + } + + /** + * Add token properties to the array used by getResultProperties, + * based on a token functions mapping. + */ + protected static function addTokenProperties( &$props, $tokenFunctions ) { + foreach ( array_keys( $tokenFunctions ) as $token ) { + $props[''][$token . 'token'] = array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ); + } + } + + /** + * Get final module description, after hooks have had a chance to tweak it as + * needed. + * + * @return array|bool False on no parameters + */ public function getFinalDescription() { $desc = $this->getDescription(); wfRunHooks( 'APIGetDescription', array( &$this, &$desc ) ); @@ -630,14 +697,15 @@ abstract class ApiBase extends ContextSource { public function requireOnlyOneParameter( $params ) { $required = func_get_args(); array_shift( $required ); + $p = $this->getModulePrefix(); $intersection = array_intersect( array_keys( array_filter( $params, array( $this, "parameterNotEmpty" ) ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( 'The parameters ' . implode( ', ', $intersection ) . ' can not be used together', 'invalidparammix' ); + $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" ); } elseif ( count( $intersection ) == 0 ) { - $this->dieUsage( 'One of the parameters ' . implode( ', ', $required ) . ' is required', 'missingparam' ); + $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" ); } } @@ -665,12 +733,13 @@ abstract class ApiBase extends ContextSource { public function requireMaxOneParameter( $params ) { $required = func_get_args(); array_shift( $required ); + $p = $this->getModulePrefix(); $intersection = array_intersect( array_keys( array_filter( $params, array( $this, "parameterNotEmpty" ) ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( 'The parameters ' . implode( ', ', $intersection ) . ' can not be used together', 'invalidparammix' ); + $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" ); } } @@ -690,6 +759,53 @@ abstract class ApiBase extends ContextSource { } /** + * @param $params array + * @param $load bool|string Whether load the object's state from the database: + * - false: don't load (if the pageid is given, it will still be loaded) + * - 'fromdb': load from a slave database + * - 'fromdbmaster': load from the master database + * @return WikiPage + */ + public function getTitleOrPageId( $params, $load = false ) { + $this->requireOnlyOneParameter( $params, 'title', 'pageid' ); + + $pageObj = null; + if ( isset( $params['title'] ) ) { + $titleObj = Title::newFromText( $params['title'] ); + if ( !$titleObj ) { + $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); + } + $pageObj = WikiPage::factory( $titleObj ); + if ( $load !== false ) { + $pageObj->loadPageData( $load ); + } + } elseif ( isset( $params['pageid'] ) ) { + if ( $load === false ) { + $load = 'fromdb'; + } + $pageObj = WikiPage::newFromID( $params['pageid'], $load ); + if ( !$pageObj ) { + $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) ); + } + } + + return $pageObj; + } + + /** + * @return array + */ + public function getTitleOrPageIdErrorMessage() { + return array_merge( + $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), + array( + array( 'invalidtitle', 'title' ), + array( 'nosuchpageid', 'pageid' ), + ) + ); + } + + /** * Callback function used in requireOnlyOneParameter to check whether reequired parameters are set * * @param $x object Parameter to check is not null/false @@ -719,7 +835,7 @@ abstract class ApiBase extends ContextSource { */ protected function getWatchlistValue ( $watchlist, $titleObj, $userOption = null ) { - $userWatching = $titleObj->userIsWatching(); + $userWatching = $this->getUser()->isWatched( $titleObj ); switch ( $watchlist ) { case 'watch': @@ -773,7 +889,7 @@ abstract class ApiBase extends ContextSource { * Using the settings determine the value for the given parameter * * @param $paramName String: parameter name - * @param $paramSettings Mixed: default value or an array of settings + * @param $paramSettings array|mixed default value or an array of settings * using PARAM_* constants. * @param $parseLimit Boolean: parse limit? * @return mixed Parameter value @@ -809,8 +925,8 @@ abstract class ApiBase extends ContextSource { if ( $type == 'boolean' ) { if ( isset( $default ) && $default !== false ) { - // Having a default value of anything other than 'false' is pointless - ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'" ); + // Having a default value of anything other than 'false' is not allowed + ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'. Boolean parameters must default to false." ); } $value = $this->getRequest()->getCheck( $encParamName ); @@ -1078,7 +1194,8 @@ abstract class ApiBase extends ContextSource { * @param $errorCode string Brief, arbitrary, stable string to allow easy * automated identification of the error, e.g., 'unknown_action' * @param $httpRespCode int HTTP response code - * @param $extradata array Data to add to the <error> element; array in ApiResult format + * @param $extradata array Data to add to the "<error>" element; array in ApiResult format + * @throws UsageException */ public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) { Profiler::instance()->close(); @@ -1155,6 +1272,8 @@ abstract class ApiBase extends ContextSource { 'nouserspecified' => array( 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ), 'noname' => array( 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ), 'summaryrequired' => array( 'code' => 'summaryrequired', 'info' => 'Summary required' ), + 'import-rootpage-invalid' => array( 'code' => 'import-rootpage-invalid', 'info' => 'Root page is an invalid title' ), + 'import-rootpage-nosubpage' => array( 'code' => 'import-rootpage-nosubpage', 'info' => 'Namespace "$1" of the root page does not allow subpages' ), // API-specific messages 'readrequired' => array( 'code' => 'readapidenied', 'info' => "You need read permission to use this module" ), @@ -1186,7 +1305,6 @@ abstract class ApiBase extends ContextSource { 'toofewexpiries' => array( 'code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed" ), 'cantimport' => array( 'code' => 'cantimport', 'info' => "You don't have permission to import pages" ), 'cantimport-upload' => array( 'code' => 'cantimport-upload', 'info' => "You don't have permission to import uploaded pages" ), - 'nouploadmodule' => array( 'code' => 'nomodule', 'info' => 'No upload module set' ), 'importnofile' => array( 'code' => 'nofile', 'info' => "You didn't upload a file" ), 'importuploaderrorsize' => array( 'code' => 'filetoobig', 'info' => 'The file you uploaded is bigger than the maximum upload size' ), 'importuploaderrorpartial' => array( 'code' => 'partialupload', 'info' => 'The file was only partially uploaded' ), @@ -1202,12 +1320,14 @@ abstract class ApiBase extends ContextSource { 'specialpage-cantexecute' => array( 'code' => 'specialpage-cantexecute', 'info' => "You don't have permission to view the results of this special page" ), 'invalidoldimage' => array( 'code' => 'invalidoldimage', 'info' => 'The oldimage parameter has invalid format' ), 'nodeleteablefile' => array( 'code' => 'nodeleteablefile', 'info' => 'No such old version of the file' ), + 'fileexists-forbidden' => array( 'code' => 'fileexists-forbidden', 'info' => 'A file with name "$1" already exists, and cannot be overwritten.' ), + 'fileexists-shared-forbidden' => array( 'code' => 'fileexists-shared-forbidden', 'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.' ), + 'filerevert-badversion' => array( 'code' => 'filerevert-badversion', 'info' => 'There is no previous local version of this file with the provided timestamp.' ), // ApiEditPage messages 'noimageredirect-anon' => array( 'code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects" ), 'noimageredirect-logged' => array( 'code' => 'noimageredirect', 'info' => "You don't have permission to create image redirects" ), 'spamdetected' => array( 'code' => 'spamdetected', 'info' => "Your edit was refused because it contained a spam fragment: \"\$1\"" ), - 'filtered' => array( 'code' => 'filtered', 'info' => "The filter callback function refused your edit" ), 'contenttoobig' => array( 'code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes" ), 'noedit-anon' => array( 'code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages" ), 'noedit' => array( 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ), @@ -1227,10 +1347,11 @@ abstract class ApiBase extends ContextSource { 'edit-already-exists' => array( 'code' => 'edit-already-exists', 'info' => "It seems the page you tried to create already exist" ), // uploadMsgs - 'invalid-session-key' => array( 'code' => 'invalid-session-key', 'info' => 'Not a valid session key' ), + 'invalid-file-key' => array( 'code' => 'invalid-file-key', 'info' => 'Not a valid file key' ), 'nouploadmodule' => array( 'code' => 'nouploadmodule', 'info' => 'No upload module set' ), 'uploaddisabled' => array( 'code' => 'uploaddisabled', 'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true' ), 'copyuploaddisabled' => array( 'code' => 'copyuploaddisabled', 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' ), + 'copyuploadbaddomain' => array( 'code' => 'copyuploadbaddomain', 'info' => 'Uploads by URL are not allowed from this domain.' ), 'filename-tooshort' => array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), 'filename-toolong' => array( 'code' => 'filename-toolong', 'info' => 'The filename is too long' ), @@ -1280,10 +1401,9 @@ abstract class ApiBase extends ContextSource { } if ( isset( self::$messageMap[$key] ) ) { - return array( 'code' => - wfMsgReplaceArgs( self::$messageMap[$key]['code'], $error ), - 'info' => - wfMsgReplaceArgs( self::$messageMap[$key]['info'], $error ) + return array( + 'code' => wfMsgReplaceArgs( self::$messageMap[$key]['code'], $error ), + 'info' => wfMsgReplaceArgs( self::$messageMap[$key]['info'], $error ) ); } @@ -1332,7 +1452,9 @@ abstract class ApiBase extends ContextSource { } /** - * Returns whether this module requires a Token to execute + * Returns whether this module requires a token to execute + * It is used to show possible errors in action=paraminfo + * see bug 25248 * @return bool */ public function needsToken() { @@ -1340,8 +1462,12 @@ abstract class ApiBase extends ContextSource { } /** - * Returns the token salt if there is one, '' if the module doesn't require a salt, else false if the module doesn't need a token - * @return bool|string + * Returns the token salt if there is one, + * '' if the module doesn't require a salt, + * else false if the module doesn't need a token + * You have also to override needsToken() + * Value is passed to User::getEditToken + * @return bool|string|array */ public function getTokenSalt() { return false; @@ -1373,7 +1499,7 @@ abstract class ApiBase extends ContextSource { } /** - * @return false|string|array Returns a false if the module has no help url, else returns a (array of) string + * @return bool|string|array Returns a false if the module has no help url, else returns a (array of) string */ public function getHelpUrls() { return false; diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 351ac6b7..c879b35d 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -4,7 +4,7 @@ * * Created on Sep 4, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -47,7 +47,7 @@ class ApiBlock extends ApiBase { $params = $this->extractRequestParams(); if ( $params['gettoken'] ) { - $res['blocktoken'] = $user->getEditToken( '', $this->getMain()->getRequest() ); + $res['blocktoken'] = $user->getEditToken(); $this->getResult()->addValue( null, $this->getModuleName(), $res ); return; } @@ -72,9 +72,9 @@ class ApiBlock extends ApiBase { $data = array( 'Target' => $params['user'], 'Reason' => array( - is_null( $params['reason'] ) ? '' : $params['reason'], + $params['reason'], 'other', - is_null( $params['reason'] ) ? '' : $params['reason'] + $params['reason'] ), 'Expiry' => $params['expiry'] == 'never' ? 'infinite' : $params['expiry'], 'HardBlock' => !$params['anononly'], @@ -100,12 +100,14 @@ class ApiBlock extends ApiBase { $block = Block::newFromTarget( $target ); if( $block instanceof Block ){ - $res['expiry'] = $block->mExpiry == wfGetDB( DB_SLAVE )->getInfinity() + $res['expiry'] = $block->mExpiry == $this->getDB()->getInfinity() ? 'infinite' : wfTimestamp( TS_ISO_8601, $block->mExpiry ); + $res['id'] = $block->getId(); } else { # should be unreachable $res['expiry'] = ''; + $res['id'] = ''; } $res['reason'] = $params['reason']; @@ -149,9 +151,12 @@ class ApiBlock extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'token' => null, - 'gettoken' => false, + 'gettoken' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), 'expiry' => 'never', - 'reason' => null, + 'reason' => '', 'anononly' => false, 'nocreate' => false, 'autoblock' => false, @@ -166,10 +171,10 @@ class ApiBlock extends ApiBase { public function getParamDescription() { return array( 'user' => 'Username, IP address or IP range you want to block', - 'token' => 'A block token previously obtained through the gettoken parameter or prop=info', + 'token' => 'A block token previously obtained through prop=info', 'gettoken' => 'If set, a block token will be returned, and no other action will be taken', 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.', - 'reason' => 'Reason for block (optional)', + 'reason' => 'Reason for block', 'anononly' => 'Block anonymous users only (i.e. disable anonymous edits for this IP)', 'nocreate' => 'Prevent account creation', 'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from', @@ -181,6 +186,44 @@ class ApiBlock extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'blocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'user' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'userID' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'expiry' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'id' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'reason' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'anononly' => 'boolean', + 'nocreate' => 'boolean', + 'autoblock' => 'boolean', + 'noemail' => 'boolean', + 'hidename' => 'boolean', + 'allowusertalk' => 'boolean', + 'watchuser' => 'boolean' + ) + ); + } + public function getDescription() { return 'Block a user'; } diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 4bb94c4a..ed72b29b 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -32,8 +32,8 @@ class ApiComparePages extends ApiBase { public function execute() { $params = $this->extractRequestParams(); - $rev1 = $this->revisionOrTitle( $params['fromrev'], $params['fromtitle'] ); - $rev2 = $this->revisionOrTitle( $params['torev'], $params['totitle'] ); + $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] ); + $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] ); $de = new DifferenceEngine( $this->getContext(), $rev1, @@ -46,10 +46,16 @@ class ApiComparePages extends ApiBase { if ( isset( $params['fromtitle'] ) ) { $vals['fromtitle'] = $params['fromtitle']; } + if ( isset( $params['fromid'] ) ) { + $vals['fromid'] = $params['fromid']; + } $vals['fromrevid'] = $rev1; if ( isset( $params['totitle'] ) ) { $vals['totitle'] = $params['totitle']; } + if ( isset( $params['toid'] ) ) { + $vals['toid'] = $params['toid']; + } $vals['torevid'] = $rev2; $difftext = $de->getDiffBody(); @@ -67,9 +73,10 @@ class ApiComparePages extends ApiBase { /** * @param $revision int * @param $titleText string + * @param $titleId int * @return int */ - private function revisionOrTitle( $revision, $titleText ) { + private function revisionOrTitleOrId( $revision, $titleText, $titleId ) { if( $revision ){ return $revision; } elseif( $titleText ) { @@ -78,17 +85,29 @@ class ApiComparePages extends ApiBase { $this->dieUsageMsg( array( 'invalidtitle', $titleText ) ); } return $title->getLatestRevID(); + } elseif ( $titleId ) { + $title = Title::newFromID( $titleId ); + if( !$title ) { + $this->dieUsageMsg( array( 'nosuchpageid', $titleId ) ); + } + return $title->getLatestRevID(); } - $this->dieUsage( 'inputneeded', 'A title or a revision number is needed for both the from and the to parameters' ); + $this->dieUsage( 'inputneeded', 'A title, a page ID, or a revision number is needed for both the from and the to parameters' ); } public function getAllowedParams() { return array( 'fromtitle' => null, + 'fromid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), 'fromrev' => array( ApiBase::PARAM_TYPE => 'integer' ), 'totitle' => null, + 'toid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), 'torev' => array( ApiBase::PARAM_TYPE => 'integer' ), @@ -98,15 +117,36 @@ class ApiComparePages extends ApiBase { public function getParamDescription() { return array( 'fromtitle' => 'First title to compare', + 'fromid' => 'First page ID to compare', 'fromrev' => 'First revision to compare', 'totitle' => 'Second title to compare', + 'toid' => 'Second page ID to compare', 'torev' => 'Second revision to compare', ); } + + public function getResultProperties() { + return array( + '' => array( + 'fromtitle' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'fromrevid' => 'integer', + 'totitle' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'torevid' => 'integer', + '*' => 'string' + ) + ); + } + public function getDescription() { return array( 'Get the difference between 2 pages', - 'You must pass a revision number or a page title for each part (1 and 2)' + 'You must pass a revision number or a page title or a page ID id for each part (1 and 2)' ); } @@ -114,6 +154,7 @@ class ApiComparePages extends ApiBase { return array_merge( parent::getPossibleErrors(), array( array( 'code' => 'inputneeded', 'info' => 'A title or a revision is needed' ), array( 'invalidtitle', 'title' ), + array( 'nosuchpageid', 'pageid' ), array( 'code' => 'baddiff', 'info' => 'The diff cannot be retrieved. Maybe one or both revisions do not exist or you do not have permission to view them.' ), ) ); } diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index cfaf6cc1..2d36f19a 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -4,7 +4,7 @@ * * Created on Jun 30, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -46,35 +46,24 @@ class ApiDelete extends ApiBase { public function execute() { $params = $this->extractRequestParams(); - $this->requireOnlyOneParameter( $params, 'title', 'pageid' ); - - if ( isset( $params['title'] ) ) { - $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj ) { - $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); - } - } elseif ( isset( $params['pageid'] ) ) { - $titleObj = Title::newFromID( $params['pageid'] ); - if ( !$titleObj ) { - $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) ); - } - } - if ( !$titleObj->exists() ) { + $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); + if ( !$pageObj->exists() ) { $this->dieUsageMsg( 'notanarticle' ); } - $reason = ( isset( $params['reason'] ) ? $params['reason'] : null ); - $pageObj = WikiPage::factory( $titleObj ); + $titleObj = $pageObj->getTitle(); + $reason = $params['reason']; $user = $this->getUser(); if ( $titleObj->getNamespace() == NS_FILE ) { - $retval = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false ); + $status = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false ); } else { - $retval = self::delete( $pageObj, $user, $params['token'], $reason ); + $status = self::delete( $pageObj, $user, $params['token'], $reason ); } - if ( count( $retval ) ) { - $this->dieUsageMsg( reset( $retval ) ); // We don't care about multiple errors, just report one of them + if ( !$status->isGood() ) { + $errors = $status->getErrorsArray(); + $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them } // Deprecated parameters @@ -87,7 +76,11 @@ class ApiDelete extends ApiBase { } $this->setWatch( $watch, $titleObj, 'watchdeletion' ); - $r = array( 'title' => $titleObj->getPrefixedText(), 'reason' => $reason ); + $r = array( + 'title' => $titleObj->getPrefixedText(), + 'reason' => $reason, + 'logid' => $status->value + ); $this->getResult()->addValue( null, $this->getModuleName(), $r ); } @@ -109,7 +102,7 @@ class ApiDelete extends ApiBase { * @param $user User doing the action * @param $token String: delete token (same as edit token) * @param $reason String: reason for the deletion. Autogenerated if NULL - * @return Title::getUserPermissionsErrors()-like array + * @return Status */ public static function delete( Page $page, User $user, $token, &$reason = null ) { $title = $page->getTitle(); @@ -131,11 +124,7 @@ class ApiDelete extends ApiBase { $error = ''; // Luckily, Article.php provides a reusable delete function that does the hard work for us - if ( $page->doDeleteArticle( $reason, false, 0, true, $error ) ) { - return array(); - } else { - return array( array( 'cannotdelete', $title->getPrefixedText() ) ); - } + return $page->doDeleteArticleReal( $reason, false, 0, true, $error ); } /** @@ -145,7 +134,7 @@ class ApiDelete extends ApiBase { * @param $oldimage * @param $reason * @param $suppress bool - * @return \type|array|Title + * @return Status */ public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) { $title = $page->getTitle(); @@ -167,19 +156,12 @@ class ApiDelete extends ApiBase { if ( !$oldfile->exists() || !$oldfile->isLocal() || $oldfile->getRedirected() ) { return array( array( 'nodeleteablefile' ) ); } - } else { - $oldfile = false; } if ( is_null( $reason ) ) { // Log and RC don't like null reasons $reason = ''; } - $status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress ); - if ( !$status->isGood() ) { - return array( array( 'cannotdelete', $title->getPrefixedText() ) ); - } - - return array(); + return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress ); } public function mustBePosted() { @@ -196,7 +178,10 @@ class ApiDelete extends ApiBase { 'pageid' => array( ApiBase::PARAM_TYPE => 'integer' ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'reason' => null, 'watch' => array( ApiBase::PARAM_DFLT => false, @@ -233,16 +218,24 @@ class ApiDelete extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'title' => 'string', + 'reason' => 'string', + 'logid' => 'integer' + ) + ); + } + public function getDescription() { return 'Delete a page'; } public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), + $this->getTitleOrPageIdErrorMessage(), array( - array( 'invalidtitle', 'title' ), - array( 'nosuchpageid', 'pageid' ), array( 'notanarticle' ), array( 'hookaborted', 'error' ), array( 'delete-toobig', 'limit' ), diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php index 55754896..13975aec 100644 --- a/includes/api/ApiDisabled.php +++ b/includes/api/ApiDisabled.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 9ed6d08d..0963fe7c 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -4,7 +4,7 @@ * * Created on August 16, 2007 * - * Copyright © 2007 Iker Labarga <Firstname><Lastname>@gmail.com + * Copyright © 2007 Iker Labarga "<Firstname><Lastname>@gmail.com" * * 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 @@ -48,8 +48,9 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( 'missingtext' ); } - $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj || $titleObj->isExternal() ) { + $pageObj = $this->getTitleOrPageId( $params ); + $titleObj = $pageObj->getTitle(); + if ( $titleObj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } @@ -59,7 +60,11 @@ class ApiEditPage extends ApiBase { if ( $titleObj->isRedirect() ) { $oldTitle = $titleObj; - $titles = Title::newFromRedirectArray( Revision::newFromTitle( $oldTitle )->getText( Revision::FOR_THIS_USER ) ); + $titles = Title::newFromRedirectArray( + Revision::newFromTitle( + $oldTitle, false, Revision::READ_LATEST + )->getText( Revision::FOR_THIS_USER ) + ); // array_shift( $titles ); $redirValues = array(); @@ -161,7 +166,7 @@ class ApiEditPage extends ApiBase { // If no summary was given and we only undid one rev, // use an autosummary if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] ) { - $params['summary'] = wfMsgForContent( 'undo-summary', $params['undo'], $undoRev->getUserText() ); + $params['summary'] = wfMessage( 'undo-summary', $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text(); } } @@ -181,7 +186,7 @@ class ApiEditPage extends ApiBase { if ( !is_null( $params['summary'] ) ) { $requestArray['wpSummary'] = $params['summary']; } - + if ( !is_null( $params['sectiontitle'] ) ) { $requestArray['wpSectionTitle'] = $params['sectiontitle']; } @@ -282,9 +287,6 @@ class ApiEditPage extends ApiBase { case EditPage::AS_SPAM_ERROR: $this->dieUsageMsg( array( 'spamdetected', $result['spam'] ) ); - case EditPage::AS_FILTERING: - $this->dieUsageMsg( 'filtered' ); - case EditPage::AS_BLOCKED_PAGE_FOR_USER: $this->dieUsageMsg( 'blockedtext' ); @@ -342,16 +344,11 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( 'summaryrequired' ); case EditPage::AS_END: + default: // $status came from WikiPage::doEdit() $errors = $status->getErrorsArray(); $this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map break; - default: - if ( is_string( $status->value ) && strlen( $status->value ) ) { - $this->dieUsage( "An unknown return value was returned by Editpage. The code returned was \"{$status->value}\"" , $status->value ); - } else { - $this->dieUsageMsg( array( 'unknownerror', $status->value ) ); - } } $apiResult->addValue( null, $this->getModuleName(), $r ); } @@ -371,45 +368,48 @@ class ApiEditPage extends ApiBase { public function getPossibleErrors() { global $wgMaxArticleSize; - return array_merge( parent::getPossibleErrors(), array( - array( 'missingtext' ), - array( 'invalidtitle', 'title' ), - array( 'createonly-exists' ), - array( 'nocreate-missing' ), - array( 'nosuchrevid', 'undo' ), - array( 'nosuchrevid', 'undoafter' ), - array( 'revwrongpage', 'id', 'text' ), - array( 'undo-failure' ), - array( 'hashcheckfailed' ), - array( 'hookaborted' ), - array( 'noimageredirect-anon' ), - array( 'noimageredirect-logged' ), - array( 'spamdetected', 'spam' ), - array( 'summaryrequired' ), - array( 'filtered' ), - array( 'blockedtext' ), - array( 'contenttoobig', $wgMaxArticleSize ), - array( 'noedit-anon' ), - array( 'noedit' ), - array( 'actionthrottledtext' ), - array( 'wasdeleted' ), - array( 'nocreate-loggedin' ), - array( 'blankpage' ), - array( 'editconflict' ), - array( 'emptynewsection' ), - array( 'unknownerror', 'retval' ), - array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ), - array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ), - array( 'customcssprotected' ), - array( 'customjsprotected' ), - ) ); + return array_merge( parent::getPossibleErrors(), + $this->getTitleOrPageIdErrorMessage(), + array( + array( 'missingtext' ), + array( 'createonly-exists' ), + array( 'nocreate-missing' ), + array( 'nosuchrevid', 'undo' ), + array( 'nosuchrevid', 'undoafter' ), + array( 'revwrongpage', 'id', 'text' ), + array( 'undo-failure' ), + array( 'hashcheckfailed' ), + array( 'hookaborted' ), + array( 'noimageredirect-anon' ), + array( 'noimageredirect-logged' ), + array( 'spamdetected', 'spam' ), + array( 'summaryrequired' ), + array( 'blockedtext' ), + array( 'contenttoobig', $wgMaxArticleSize ), + array( 'noedit-anon' ), + array( 'noedit' ), + array( 'actionthrottledtext' ), + array( 'wasdeleted' ), + array( 'nocreate-loggedin' ), + array( 'blankpage' ), + array( 'editconflict' ), + array( 'emptynewsection' ), + array( 'unknownerror', 'retval' ), + array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ), + array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ), + array( 'customcssprotected' ), + array( 'customjsprotected' ), + ) + ); } public function getAllowedParams() { return array( 'title' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true + ), + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer', ), 'section' => null, 'sectiontitle' => array( @@ -417,7 +417,10 @@ class ApiEditPage extends ApiBase { ApiBase::PARAM_REQUIRED => false, ), 'text' => null, - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'summary' => null, 'minor' => false, 'notminor' => false, @@ -463,19 +466,20 @@ class ApiEditPage extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); return array( - 'title' => 'Page title', + 'title' => "Title of the page you want to edit. Cannot be used together with {$p}pageid", + 'pageid' => "Page ID of the page you want to edit. Cannot be used together with {$p}title", 'section' => 'Section number. 0 for the top section, \'new\' for a new section', 'sectiontitle' => 'The title for a new section', 'text' => 'Page content', 'token' => array( 'Edit token. You can get one of these through prop=info.', - 'The token should always be sent as the last parameter, or at least, after the text parameter' + "The token should always be sent as the last parameter, or at least, after the {$p}text parameter" ), - 'summary' => 'Edit summary. Also section title when section=new', + 'summary' => "Edit summary. Also section title when {$p}section=new and {$p}sectiontitle is not set", 'minor' => 'Minor edit', 'notminor' => 'Non-minor edit', 'bot' => 'Mark this edit as bot', 'basetimestamp' => array( 'Timestamp of the base revision (obtained through prop=revisions&rvprop=timestamp).', - 'Used to detect edit conflicts; leave unset to ignore conflicts.' + 'Used to detect edit conflicts; leave unset to ignore conflicts' ), 'starttimestamp' => array( 'Timestamp when you obtained the edit token.', 'Used to detect edit conflicts; leave unset to ignore conflicts' @@ -489,13 +493,49 @@ class ApiEditPage extends ApiBase { 'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.", 'If set, the edit won\'t be done unless the hash is correct' ), 'prependtext' => "Add this text to the beginning of the page. Overrides {$p}text", - 'appendtext' => "Add this text to the end of the page. Overrides {$p}text", + 'appendtext' => array( "Add this text to the end of the page. Overrides {$p}text.", + "Use {$p}section=new to append a new section" ), 'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext", 'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision', 'redirect' => 'Automatically resolve redirects', ); } + public function getResultProperties() { + return array( + '' => array( + 'new' => 'boolean', + 'result' => array( + ApiBase::PROP_TYPE => array( + 'Success', + 'Failure' + ), + ), + 'pageid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'title' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'nochange' => 'boolean', + 'oldrevid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'newrevid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'newtimestamp' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function needsToken() { return true; } diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index d9eed60c..4fa03434 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -55,7 +55,7 @@ class ApiEmailUser extends ApiBase { 'Subject' => $params['subject'], 'CCMe' => $params['ccme'], ); - $retval = SpecialEmailUser::submit( $data ); + $retval = SpecialEmailUser::submit( $data, $this->getContext() ); if ( $retval instanceof Status ) { // SpecialEmailUser sometimes returns a status @@ -98,7 +98,10 @@ class ApiEmailUser extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'ccme' => false, ); } @@ -113,6 +116,23 @@ class ApiEmailUser extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'result' => array( + ApiBase::PROP_TYPE => array( + 'Success', + 'Failure' + ), + ), + 'message' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return 'Email a user.'; } diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index d570534d..160f5b91 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -4,7 +4,7 @@ * * Created on Oct 05, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -103,6 +103,14 @@ class ApiExpandTemplates extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + '*' => 'string' + ) + ); + } + public function getDescription() { return 'Expands all templates in wikitext'; } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 4e70bde2..1cf760ae 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -60,7 +60,7 @@ class ApiFeedContributions extends ApiBase { $this->dieUsage( 'Size difference is disabled in Miser Mode', 'sizediffdisabled' ); } - $msg = wfMsgForContent( 'Contributions' ); + $msg = wfMessage( 'Contributions' )->inContentLanguage()->text(); $feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']'; $feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL(); @@ -96,7 +96,7 @@ class ApiFeedContributions extends ApiBase { } protected function feedItem( $row ) { - $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title ); + $title = Title::makeTitle( intval( $row->page_namespace ), $row->page_title ); if( $title ) { $date = $row->rev_timestamp; $comments = $title->getTalkPage()->getFullURL(); @@ -129,7 +129,8 @@ class ApiFeedContributions extends ApiBase { */ protected function feedItemDesc( $revision ) { if( $revision ) { - return '<p>' . htmlspecialchars( $revision->getUserText() ) . wfMsgForContent( 'colon-separator' ) . + $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); + return '<p>' . htmlspecialchars( $revision->getUserText() ) . $msg . htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . "</p>\n<hr />\n<div>" . nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>"; @@ -150,8 +151,7 @@ class ApiFeedContributions extends ApiBase { ApiBase::PARAM_REQUIRED => true, ), 'namespace' => array( - ApiBase::PARAM_TYPE => 'namespace', - ApiBase::PARAM_ISMULTI => true + ApiBase::PARAM_TYPE => 'namespace' ), 'year' => array( ApiBase::PARAM_TYPE => 'integer' diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index eee8fa19..6ccb02fe 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -4,7 +4,7 @@ * * Created on Oct 13, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -117,7 +117,7 @@ class ApiFeedWatchlist extends ApiBase { $feedItems[] = $this->createFeedItem( $info ); } - $msg = wfMsgForContent( 'watchlist' ); + $msg = wfMessage( 'watchlist' )->inContentLanguage()->text(); $feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']'; $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL(); @@ -131,11 +131,12 @@ class ApiFeedWatchlist extends ApiBase { // Error results should not be cached $this->getMain()->setCacheMaxAge( 0 ); - $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent( 'watchlist' ) . ' [' . $wgLanguageCode . ']'; + $feedTitle = $wgSitename . ' - Error - ' . wfMessage( 'watchlist' )->inContentLanguage()->text() . ' [' . $wgLanguageCode . ']'; $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL(); $feedFormat = isset( $params['feedformat'] ) ? $params['feedformat'] : 'rss'; - $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, htmlspecialchars( wfMsgForContent( 'watchlist' ) ), $feedUrl ); + $msg = wfMessage( 'watchlist' )->inContentLanguage()->escaped(); + $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl ); if ( $e instanceof UsageException ) { $errorCode = $e->getCodeString(); diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php index 7ef1da0a..83d078d2 100644 --- a/includes/api/ApiFileRevert.php +++ b/includes/api/ApiFileRevert.php @@ -71,9 +71,10 @@ class ApiFileRevert extends ApiBase { * @param $user User The user to check. */ protected function checkPermissions( $user ) { + $title = $this->file->getTitle(); $permissionErrors = array_merge( - $this->file->getTitle()->getUserPermissionsErrors( 'edit' , $user ), - $this->file->getTitle()->getUserPermissionsErrors( 'upload' , $user ) + $title->getUserPermissionsErrors( 'edit' , $user ), + $title->getUserPermissionsErrors( 'upload' , $user ) ); if ( $permissionErrors ) { @@ -91,15 +92,17 @@ class ApiFileRevert extends ApiBase { if ( is_null( $title ) ) { $this->dieUsageMsg( array( 'invalidtitle', $this->params['filename'] ) ); } + $localRepo = RepoGroup::singleton()->getLocalRepo(); + // Check if the file really exists - $this->file = wfLocalFile( $title ); + $this->file = $localRepo->newFile( $title ); if ( !$this->file->exists() ) { $this->dieUsageMsg( 'notanarticle' ); } // Check if the archivename is valid for this file $this->archiveName = $this->params['archivename']; - $oldFile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $this->archiveName ); + $oldFile = $localRepo->newFromArchiveName( $title, $this->archiveName ); if ( !$oldFile->exists() ) { $this->dieUsageMsg( 'filerevert-badversion' ); } @@ -126,21 +129,38 @@ class ApiFileRevert extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), ); } public function getParamDescription() { - $params = array( - 'filename' => 'Target filename', + return array( + 'filename' => 'Target filename without the File: prefix', 'token' => 'Edit token. You can get one of these through prop=info', 'comment' => 'Upload comment', 'archivename' => 'Archive name of the revision to revert to', ); + } - return $params; - + public function getResultProperties() { + return array( + '' => array( + 'result' => array( + ApiBase::PROP_TYPE => array( + 'Success', + 'Failure' + ) + ), + 'errors' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); } public function getDescription() { diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 1eee717a..8ad9b8ca 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -169,8 +169,10 @@ abstract class ApiFormatBase extends ApiBase { <br /> <small> You are looking at the HTML representation of the <?php echo( $this->mFormat ); ?> format.<br /> -HTML is good for debugging, but probably is not suitable for your application.<br /> -See <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or +HTML is good for debugging, but is unsuitable for application use.<br /> +Specify the format parameter to change the output format.<br /> +To see the non HTML representation of the <?php echo( $this->mFormat ); ?> format, set format=<?php echo( strtolower( $this->mFormat ) ); ?>.<br /> +See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or <a href='<?php echo( $script ); ?>'>API help</a> for more information. </small> <?php @@ -264,11 +266,12 @@ See <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or $text = htmlspecialchars( $text ); // encode all comments or tags as safe blue strings - $text = preg_replace( '/\<(!--.*?--|.*?)\>/', '<span style="color:blue;"><\1></span>', $text ); + $text = str_replace( '<', '<span style="color:blue;"><', $text ); + $text = str_replace( '>', '></span>', $text ); // identify URLs $protos = wfUrlProtocolsWithoutProtRel(); // This regex hacks around bug 13218 (" included in the URL) - $text = preg_replace( "#(($protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '<a href="\\1">\\1</a>\\3\\4', $text ); + $text = preg_replace( "#(((?i)$protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '<a href="\\1">\\1</a>\\3\\4', $text ); // identify requests to api.php $text = preg_replace( "#api\\.php\\?[^ <\n\t]+#", '<a href="\\0">\\0</a>', $text ); if ( $this->mHelp ) { @@ -329,7 +332,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase { */ public static function setResult( $result, $feed, $feedItems ) { // Store output in the Result data. - // This way we can check during execution if any error has occured + // This way we can check during execution if any error has occurred // Disable size checking for this because we can't continue // cleanly; size checking would cause more problems than it'd // solve @@ -374,7 +377,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } $feed->outFooter(); } else { - // Error has occured, print something useful + // Error has occurred, print something useful ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); } } diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php index 92619f76..3d2a39ca 100644 --- a/includes/api/ApiFormatDbg.php +++ b/includes/api/ApiFormatDbg.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index e728d057..acbc7d3b 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index 60552c40..fac2ca58 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php index db81aacd..184f0a34 100644 --- a/includes/api/ApiFormatRaw.php +++ b/includes/api/ApiFormatRaw.php @@ -4,7 +4,7 @@ * * Created on Feb 2, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php index e26b82b0..71414593 100644 --- a/includes/api/ApiFormatTxt.php +++ b/includes/api/ApiFormatTxt.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 1bc9d025..65056e44 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 8f4abc15..5ccf1859 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -83,16 +83,40 @@ class ApiFormatXml extends ApiFormatBase { /** * This method takes an array and converts it to XML. + * * There are several noteworthy cases: * - * If array contains a key '_element', then the code assumes that ALL other keys are not important and replaces them with the value['_element']. - * Example: name='root', value = array( '_element'=>'page', 'x', 'y', 'z') creates <root> <page>x</page> <page>y</page> <page>z</page> </root> + * If array contains a key '_element', then the code assumes that ALL + * other keys are not important and replaces them with the + * value['_element']. + * + * @par Example: + * @verbatim + * name='root', value = array( '_element'=>'page', 'x', 'y', 'z') + * @endverbatim + * creates: + * @verbatim + * <root> <page>x</page> <page>y</page> <page>z</page> </root> + * @endverbatim + * + * If any of the array's element key is '*', then the code treats all + * other key->value pairs as attributes, and the value['*'] as the + * element's content. + * + * @par Example: + * @verbatim + * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) + * @endverbatim + * creates: + * @verbatim + * <root lang='en' id='10'>text</root> + * @endverbatim * - * If any of the array's element key is '*', then the code treats all other key->value pairs as attributes, and the value['*'] as the element's content. - * Example: name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) creates <root lang='en' id='10'>text</root> + * Finally neither key is found, all keys become element names, and values + * become element content. * - * If neither key is found, all keys become element names, and values become element content. - * The method is recursive, so the same rules apply to any sub-arrays. + * @note The method is recursive, so the same rules apply to any + * sub-arrays. * * @param $elemName * @param $elemValue @@ -215,7 +239,8 @@ class ApiFormatXml extends ApiFormatBase { public function getParamDescription() { return array( 'xmldoublequote' => 'If specified, double quotes all attributes and content', - 'xslt' => 'If specified, adds <xslt> as stylesheet', + 'xslt' => 'If specified, adds <xslt> as stylesheet. This should be a wiki page ' + . 'in the MediaWiki namespace whose page name ends with ".xsl"', 'includexmlnamespace' => 'If specified, adds an XML namespace' ); } diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index dbcdb21c..730ad8ea 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 97da786b..2b5de21a 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -4,7 +4,7 @@ * * Created on Sep 6, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index ade9f1f3..637c1fff 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -4,7 +4,7 @@ * * Created on Feb 4, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -68,6 +68,12 @@ class ApiImport extends ApiBase { if ( isset( $params['namespace'] ) ) { $importer->setTargetNamespace( $params['namespace'] ); } + if ( isset( $params['rootpage'] ) ) { + $statusRootPage = $importer->setTargetRootPage( $params['rootpage'] ); + if( !$statusRootPage->isGood() ) { + $this->dieUsageMsg( $statusRootPage->getErrorsArray() ); + } + } $reporter = new ApiImportReporter( $importer, $isUpload, @@ -98,7 +104,10 @@ class ApiImport extends ApiBase { public function getAllowedParams() { global $wgImportSources; return array( - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'summary' => null, 'xml' => null, 'interwikisource' => array( @@ -109,7 +118,8 @@ class ApiImport extends ApiBase { 'templates' => false, 'namespace' => array( ApiBase::PARAM_TYPE => 'namespace' - ) + ), + 'rootpage' => null, ); } @@ -123,6 +133,18 @@ class ApiImport extends ApiBase { 'fullhistory' => 'For interwiki imports: import the full history, not just the current version', 'templates' => 'For interwiki imports: import all included templates as well', 'namespace' => 'For interwiki imports: import to this namespace', + 'rootpage' => 'Import as subpage of this page', + ); + } + + public function getResultProperties() { + return array( + ApiBase::PROP_LIST => true, + '' => array( + 'ns' => 'namespace', + 'title' => 'string', + 'revisions' => 'integer' + ) ); } @@ -141,6 +163,8 @@ class ApiImport extends ApiBase { array( 'cantimport-upload' ), array( 'import-unknownerror', 'source' ), array( 'import-unknownerror', 'result' ), + array( 'import-rootpage-nosubpage', 'namespace' ), + array( 'import-rootpage-invalid' ), ) ); } @@ -186,8 +210,16 @@ class ApiImportReporter extends ImportReporter { function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) { // Add a result entry $r = array(); - ApiQueryBase::addTitleInfo( $r, $title ); - $r['revisions'] = intval( $successCount ); + + if ( $title === null ) { + # Invalid or non-importable title + $r['title'] = $pageInfo['title']; + $r['invalid'] = ''; + } else { + ApiQueryBase::addTitleInfo( $r, $title ); + $r['revisions'] = intval( $successCount ); + } + $this->mResultArr[] = $r; // Piggyback on the parent to do the logging diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index aa570cbc..1f91fe92 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006-2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * Copyright © 2006-2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com", * Daniel Cannon (cannon dot danielc at gmail dot com) * * This program is free software; you can redistribute it and/or modify @@ -79,6 +79,8 @@ class ApiLogin extends ApiBase { $user->setOption( 'rememberpassword', 1 ); $user->setCookies( $this->getRequest() ); + ApiQueryInfo::resetTokenCache(); + // Run hooks. // @todo FIXME: Split back and frontend from this hook. // @todo FIXME: This hook should be placed in the backend @@ -181,6 +183,66 @@ class ApiLogin extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'result' => array( + ApiBase::PROP_TYPE => array( + 'Success', + 'NeedToken', + 'WrongToken', + 'NoName', + 'Illegal', + 'WrongPluginPass', + 'NotExists', + 'WrongPass', + 'EmptyPass', + 'CreateBlocked', + 'Throttled', + 'Blocked', + 'Aborted' + ) + ), + 'lguserid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'lgusername' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'lgtoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'cookieprefix' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'sessionid' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'token' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'details' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'wait' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'reason' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return array( 'Log in and get the authentication tokens. ', diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index 81a054a6..b2f634d0 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -4,7 +4,7 @@ * * Created on Jan 4, 2008 * - * Copyright © 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * Copyright © 2008 Yuri Astrakhan "<Firstname><Lastname>@gmail.com", * * 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 @@ -54,6 +54,10 @@ class ApiLogout extends ApiBase { return array(); } + public function getResultProperties() { + return array(); + } + public function getParamDescription() { return array(); } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index fa95cfca..35febd95 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -4,7 +4,7 @@ * * Created on Sep 4, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -61,9 +61,11 @@ class ApiMain extends ApiBase { 'paraminfo' => 'ApiParamInfo', 'rsd' => 'ApiRsd', 'compare' => 'ApiComparePages', + 'tokens' => 'ApiTokens', // Write modules 'purge' => 'ApiPurge', + 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp', 'rollback' => 'ApiRollback', 'delete' => 'ApiDelete', 'undelete' => 'ApiUndelete', @@ -79,6 +81,7 @@ class ApiMain extends ApiBase { 'patrol' => 'ApiPatrol', 'import' => 'ApiImport', 'userrights' => 'ApiUserrights', + 'options' => 'ApiOptions', ); /** @@ -352,6 +355,12 @@ class ApiMain extends ApiBase { * have been accumulated, and replace it with an error message and a help screen. */ protected function executeActionWithErrorHandling() { + // Verify the CORS header before executing the action + if ( !$this->handleCORS() ) { + // handleCORS() has sent a 403, abort + return; + } + // In case an error occurs during data output, // clear the output buffer and print just the error information ob_start(); @@ -359,8 +368,11 @@ class ApiMain extends ApiBase { try { $this->executeAction(); } catch ( Exception $e ) { + // Allow extra cleanup and logging + wfRunHooks( 'ApiMain::onException', array( $this, $e ) ); + // Log it - if ( $e instanceof MWException ) { + if ( !( $e instanceof UsageException ) ) { wfDebugLog( 'exception', $e->getLogMessage() ); } @@ -384,7 +396,7 @@ class ApiMain extends ApiBase { // Reset and print just the error message ob_clean(); - // If the error occured during printing, do a printer->profileOut() + // If the error occurred during printing, do a printer->profileOut() $this->mPrinter->safeProfileOut(); $this->printResult( true ); } @@ -400,9 +412,101 @@ class ApiMain extends ApiBase { ob_end_flush(); } + /** + * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately. + * + * If no origin parameter is present, nothing happens. + * If an origin parameter is present but doesn't match the Origin header, a 403 status code + * is set and false is returned. + * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains + * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS + * headers are set. + * + * @return bool False if the caller should abort (403 case), true otherwise (all other cases) + */ + protected function handleCORS() { + global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions; + + $originParam = $this->getParameter( 'origin' ); // defaults to null + if ( $originParam === null ) { + // No origin parameter, nothing to do + return true; + } + + $request = $this->getRequest(); + $response = $request->response(); + // Origin: header is a space-separated list of origins, check all of them + $originHeader = $request->getHeader( 'Origin' ); + if ( $originHeader === false ) { + $origins = array(); + } else { + $origins = explode( ' ', $originHeader ); + } + if ( !in_array( $originParam, $origins ) ) { + // origin parameter set but incorrect + // Send a 403 response + $message = HttpStatus::getMessage( 403 ); + $response->header( "HTTP/1.1 403 $message", true, 403 ); + $response->header( 'Cache-Control: no-cache' ); + echo "'origin' parameter does not match Origin header\n"; + return false; + } + if ( self::matchOrigin( $originParam, $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions ) ) { + $response->header( "Access-Control-Allow-Origin: $originParam" ); + $response->header( 'Access-Control-Allow-Credentials: true' ); + $this->getOutput()->addVaryHeader( 'Origin' ); + } + return true; + } + + /** + * Attempt to match an Origin header against a set of rules and a set of exceptions + * @param $value string Origin header + * @param $rules array Set of wildcard rules + * @param $exceptions array Set of wildcard rules + * @return bool True if $value matches a rule in $rules and doesn't match any rules in $exceptions, false otherwise + */ + protected static function matchOrigin( $value, $rules, $exceptions ) { + foreach ( $rules as $rule ) { + if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) { + // Rule matches, check exceptions + foreach ( $exceptions as $exc ) { + if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) { + return false; + } + } + return true; + } + } + return false; + } + + /** + * Helper function to convert wildcard string into a regex + * '*' => '.*?' + * '?' => '.' + * + * @param $wildcard string String with wildcards + * @return string Regular expression + */ + protected static function wildcardToRegex( $wildcard ) { + $wildcard = preg_quote( $wildcard, '/' ); + $wildcard = str_replace( + array( '\*', '\?' ), + array( '.*?', '.' ), + $wildcard + ); + return "/https?:\/\/$wildcard/"; + } + protected function sendCacheHeaders() { global $wgUseXVO, $wgVaryOnXFP; $response = $this->getRequest()->response(); + $out = $this->getOutput(); + + if ( $wgVaryOnXFP ) { + $out->addVaryHeader( 'X-Forwarded-Proto' ); + } if ( $this->mCacheMode == 'private' ) { $response->header( 'Cache-Control: private' ); @@ -410,13 +514,9 @@ class ApiMain extends ApiBase { } if ( $this->mCacheMode == 'anon-public-user-private' ) { - $xfp = $wgVaryOnXFP ? ', X-Forwarded-Proto' : ''; - $response->header( 'Vary: Accept-Encoding, Cookie' . $xfp ); + $out->addVaryHeader( 'Cookie' ); + $response->header( $out->getVaryHeader() ); if ( $wgUseXVO ) { - $out = $this->getOutput(); - if ( $wgVaryOnXFP ) { - $out->addVaryHeader( 'X-Forwarded-Proto' ); - } $response->header( $out->getXVO() ); if ( $out->haveCacheVaryCookies() ) { // Logged in, mark this request private @@ -433,12 +533,9 @@ class ApiMain extends ApiBase { } // Send public headers - if ( $wgVaryOnXFP ) { - $response->header( 'Vary: Accept-Encoding, X-Forwarded-Proto' ); - if ( $wgUseXVO ) { - // Bleeeeegh. Our header setting system sucks - $response->header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip, X-Forwarded-Proto' ); - } + $response->header( $out->getVaryHeader() ); + if ( $wgUseXVO ) { + $response->header( $out->getXVO() ); } // If nobody called setCacheMaxAge(), use the (s)maxage parameters @@ -605,7 +702,7 @@ class ApiMain extends ApiBase { if ( !isset( $moduleParams['token'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'token' ) ); } else { - if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getRequest() ) ) { + if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getContext()->getRequest() ) ) { $this->dieUsageMsg( 'sessionfailure' ); } } @@ -664,6 +761,12 @@ class ApiMain extends ApiBase { $this->dieReadOnly(); } } + + // Allow extensions to stop execution for arbitrary reasons. + $message = false; + if( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) { + $this->dieUsageMsg( $message ); + } } /** @@ -713,6 +816,9 @@ class ApiMain extends ApiBase { $module->profileOut(); if ( !$this->mInternalMode ) { + //append Debug information + MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() ); + // Print result data $this->printResult( false ); } @@ -779,6 +885,7 @@ class ApiMain extends ApiBase { ), 'requestid' => null, 'servedby' => false, + 'origin' => null, ); } @@ -804,6 +911,12 @@ class ApiMain extends ApiBase { 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached', 'requestid' => 'Request ID to distinguish requests. This will just be output back to you', 'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error', + 'origin' => array( + 'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.', + 'This must match one of the origins in the Origin: header exactly, so it has to be set to something like http://en.wikipedia.org or https://meta.wikimedia.org .', + 'If this parameter does not match the Origin: header, a 403 response will be returned.', + 'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.', + ), ); } @@ -871,11 +984,11 @@ class ApiMain extends ApiBase { protected function getCredits() { return array( 'API developers:', - ' Roan Kattouw <Firstname>.<Lastname>@gmail.com (lead developer Sep 2007-present)', + ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-present)', ' Victor Vasiliev - vasilvv at gee mail dot com', ' Bryan Tong Minh - bryan . tongminh @ gmail . com', ' Sam Reed - sam @ reedyboy . net', - ' Yuri Astrakhan <Firstname><Lastname>@gmail.com (creator, lead developer Sep 2006-Sep 2007)', + ' Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007)', '', 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org', 'or file a bug report at https://bugzilla.wikimedia.org/' @@ -1061,11 +1174,21 @@ class ApiMain extends ApiBase { * * @ingroup API */ -class UsageException extends Exception { +class UsageException extends MWException { private $mCodestr; + + /** + * @var null|array + */ private $mExtraData; + /** + * @param $message string + * @param $codestr string + * @param $code int + * @param $extradata array|null + */ public function __construct( $message, $codestr, $code = 0, $extradata = null ) { parent::__construct( $message, $code ); $this->mCodestr = $codestr; diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index f0a25e4a..9d73562b 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -4,7 +4,7 @@ * * Created on Oct 31, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -37,9 +37,6 @@ class ApiMove extends ApiBase { public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( is_null( $params['reason'] ) ) { - $params['reason'] = ''; - } $this->requireOnlyOneParameter( $params, 'from', 'fromid' ); @@ -78,6 +75,7 @@ class ApiMove extends ApiBase { } // Move the page + $toTitleExists = $toTitle->exists(); $retval = $fromTitle->moveTo( $toTitle, true, $params['reason'], !$params['noredirect'] ); if ( $retval !== true ) { $this->dieUsageMsg( reset( $retval ) ); @@ -87,13 +85,20 @@ class ApiMove extends ApiBase { if ( !$params['noredirect'] || !$user->isAllowed( 'suppressredirect' ) ) { $r['redirectcreated'] = ''; } + if( $toTitleExists ) { + $r['moveoverredirect'] = ''; + } // Move the talk page if ( $params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage() ) { + $toTalkExists = $toTalk->exists(); $retval = $fromTalk->moveTo( $toTalk, true, $params['reason'], !$params['noredirect'] ); if ( $retval === true ) { $r['talkfrom'] = $fromTalk->getPrefixedText(); $r['talkto'] = $toTalk->getPrefixedText(); + if( $toTalkExists ) { + $r['talkmoveoverredirect'] = ''; + } } else { // We're not gonna dieUsage() on failure, since we already changed something $parsed = $this->parseMsg( reset( $retval ) ); @@ -180,8 +185,11 @@ class ApiMove extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, - 'reason' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + 'reason' => '', 'movetalk' => false, 'movesubpages' => false, 'noredirect' => false, @@ -213,7 +221,7 @@ class ApiMove extends ApiBase { 'fromid' => "Page ID of the page you want to move. Cannot be used together with {$p}from", 'to' => 'Title you want to rename the page to', 'token' => 'A move token previously retrieved through prop=info', - 'reason' => 'Reason for the move (optional)', + 'reason' => 'Reason for the move', 'movetalk' => 'Move the talk page, if it exists', 'movesubpages' => 'Move subpages, if applicable', 'noredirect' => 'Don\'t create a redirect', @@ -224,6 +232,35 @@ class ApiMove extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'from' => 'string', + 'to' => 'string', + 'reason' => 'string', + 'redirectcreated' => 'boolean', + 'moveoverredirect' => 'boolean', + 'talkfrom' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'talkto' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'talkmoveoverredirect' => 'boolean', + 'talkmove-error-code' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'talkmove-error-info' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return 'Move a page'; } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 0727cffd..ef562741 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -4,7 +4,7 @@ * * Created on Oct 13, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -45,7 +45,7 @@ class ApiOpenSearch extends ApiBase { $namespaces = $params['namespace']; $suggest = $params['suggest']; - // MWSuggest or similar hit + // Some script that was loaded regardless of wgEnableOpenSearchSuggest, likely cached. if ( $suggest && !$wgEnableOpenSearchSuggest ) { $searches = array(); } else { diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php new file mode 100644 index 00000000..265c2ccb --- /dev/null +++ b/includes/api/ApiOptions.php @@ -0,0 +1,183 @@ +<?php +/** + * + * + * Created on Apr 15, 2012 + * + * Copyright © 2012 Szymon Świerkosz beau@adres.pl + * + * 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 + */ + +/** +* API module that facilitates the changing of user's preferences. +* Requires API write mode to be enabled. +* + * @ingroup API + */ +class ApiOptions extends ApiBase { + + public function __construct( $main, $action ) { + parent::__construct( $main, $action ); + } + + /** + * Changes preferences of the current user. + */ + public function execute() { + $user = $this->getUser(); + + if ( $user->isAnon() ) { + $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' ); + } + + $params = $this->extractRequestParams(); + $changed = false; + + if ( isset( $params['optionvalue'] ) && !isset( $params['optionname'] ) ) { + $this->dieUsageMsg( array( 'missingparam', 'optionname' ) ); + } + + if ( $params['reset'] ) { + $user->resetOptions(); + $changed = true; + } + + $changes = array(); + if ( count( $params['change'] ) ) { + foreach ( $params['change'] as $entry ) { + $array = explode( '=', $entry, 2 ); + $changes[$array[0]] = isset( $array[1] ) ? $array[1] : null; + } + } + if ( isset( $params['optionname'] ) ) { + $newValue = isset( $params['optionvalue'] ) ? $params['optionvalue'] : null; + $changes[$params['optionname']] = $newValue; + } + if ( !$changed && !count( $changes ) ) { + $this->dieUsage( 'No changes were requested', 'nochanges' ); + } + + $prefs = Preferences::getPreferences( $user, $this->getContext() ); + foreach ( $changes as $key => $value ) { + if ( !isset( $prefs[$key] ) ) { + $this->setWarning( "Not a valid preference: $key" ); + continue; + } + $field = HTMLForm::loadInputFromParameters( $key, $prefs[$key] ); + $validation = $field->validate( $value, $user->getOptions() ); + if ( $validation === true ) { + $user->setOption( $key, $value ); + $changed = true; + } else { + $this->setWarning( "Validation error for '$key': $validation" ); + } + } + + if ( $changed ) { + // Commit changes + $user->saveSettings(); + } + + $this->getResult()->addValue( null, $this->getModuleName(), 'success' ); + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams() { + return array( + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + 'reset' => false, + 'change' => array( + ApiBase::PARAM_ISMULTI => true, + ), + 'optionname' => array( + ApiBase::PARAM_TYPE => 'string', + ), + 'optionvalue' => array( + ApiBase::PARAM_TYPE => 'string', + ), + ); + } + + public function getResultProperties() { + return array( + '' => array( + '*' => array( + ApiBase::PROP_TYPE => array( + 'success' + ) + ) + ) + ); + } + + public function getParamDescription() { + return array( + 'token' => 'An options token previously obtained through the action=tokens', + 'reset' => 'Resets all preferences to the site defaults', + 'change' => 'List of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters', + 'optionname' => 'A name of a option which should have an optionvalue set', + 'optionvalue' => 'A value of the option specified by the optionname, can contain pipe characters', + ); + } + + public function getDescription() { + return 'Change preferences of the current user'; + } + + public function getPossibleErrors() { + return array_merge( parent::getPossibleErrors(), array( + array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot change preferences' ), + array( 'code' => 'nochanges', 'info' => 'No changes were requested' ), + ) ); + } + + public function needsToken() { + return true; + } + + public function getTokenSalt() { + return ''; + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Options'; + } + + public function getExamples() { + return array( + 'api.php?action=options&reset=&token=123ABC', + 'api.php?action=options&change=skin=vector|hideminor=1&token=123ABC', + 'api.php?action=options&reset=&change=skin=monobook&optionname=nickname&optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 7b84c473..0f5be6b2 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -4,7 +4,7 @@ * * Created on Sep 24, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -52,7 +52,7 @@ class ApiPageSet extends ApiQueryBase { /** * Constructor - * @param $query ApiQueryBase + * @param $query ApiBase * @param $resolveRedirects bool Whether redirects should be resolved * @param $convertTitles bool */ @@ -266,8 +266,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Returns the number of revisions (requested with revids= parameter)\ - * @return int + * Returns the number of revisions (requested with revids= parameter). + * @return int Number of revisions. */ public function getRevisionCount() { return count( $this->getRevisionIDs() ); @@ -342,7 +342,7 @@ class ApiPageSet extends ApiQueryBase { /** * Populate this PageSet from a rowset returned from the database - * @param $db Database object + * @param $db DatabaseBase object * @param $queryResult ResultWrapper Query result object */ public function populateFromQueryResult( $db, $queryResult ) { @@ -367,7 +367,7 @@ class ApiPageSet extends ApiQueryBase { */ public function processDbRow( $row ) { // Store Title object in various data structures - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $title = Title::newFromRow( $row ); $pageId = intval( $row->page_id ); $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; @@ -481,6 +481,7 @@ class ApiPageSet extends ApiQueryBase { ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' ); } + $usernames = array(); if ( $res ) { foreach ( $res as $row ) { $pageId = intval( $row->page_id ); @@ -496,6 +497,11 @@ class ApiPageSet extends ApiQueryBase { // Store any extra fields requested by modules $this->processDbRow( $row ); + + // Need gender information + if( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) { + $usernames[] = $row->page_title; + } } } @@ -510,6 +516,11 @@ class ApiPageSet extends ApiQueryBase { $this->mMissingTitles[$this->mFakePageId] = $title; $this->mFakePageId--; $this->mTitles[] = $title; + + // need gender information + if( MWNamespace::hasGenderDistinction( $ns ) ) { + $usernames[] = $dbkey; + } } } } else { @@ -521,6 +532,10 @@ class ApiPageSet extends ApiQueryBase { } } } + + // Get gender information + $genderCache = GenderCache::singleton(); + $genderCache->doQuery( $usernames, __METHOD__ ); } /** @@ -664,6 +679,9 @@ class ApiPageSet extends ApiQueryBase { * @return LinkBatch */ private function processTitlesArray( $titles ) { + $genderCache = GenderCache::singleton(); + $genderCache->doTitlesArray( $titles, __METHOD__ ); + $linkBatch = new LinkBatch(); foreach ( $titles as $title ) { diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index f2263476..343a2625 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -4,7 +4,7 @@ * * Created on Dec 01, 2007 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -251,6 +251,62 @@ class ApiParamInfo extends ApiBase { } $result->setIndexedTagName( $retval['parameters'], 'param' ); + $props = $obj->getFinalResultProperties(); + $listResult = null; + if ( $props !== false ) { + $retval['props'] = array(); + + foreach ( $props as $prop => $properties ) { + $propResult = array(); + if ( $prop == ApiBase::PROP_LIST ) { + $listResult = $properties; + continue; + } + if ( $prop != ApiBase::PROP_ROOT ) { + $propResult['name'] = $prop; + } + $propResult['properties'] = array(); + + foreach ( $properties as $name => $p ) { + $propertyResult = array(); + + $propertyResult['name'] = $name; + + if ( !is_array( $p ) ) { + $p = array( ApiBase::PROP_TYPE => $p ); + } + + $propertyResult['type'] = $p[ApiBase::PROP_TYPE]; + + if ( is_array( $propertyResult['type'] ) ) { + $propertyResult['type'] = array_values( $propertyResult['type'] ); + $result->setIndexedTagName( $propertyResult['type'], 't' ); + } + + $nullable = null; + if ( isset( $p[ApiBase::PROP_NULLABLE] ) ) { + $nullable = $p[ApiBase::PROP_NULLABLE]; + } + + if ( $nullable === true ) { + $propertyResult['nullable'] = ''; + } + + $propResult['properties'][] = $propertyResult; + } + + $result->setIndexedTagName( $propResult['properties'], 'property' ); + $retval['props'][] = $propResult; + } + + // default is true for query modules, false for other modules, overriden by ApiBase::PROP_LIST + if ( $listResult === true || ( $listResult !== false && $obj instanceof ApiQueryBase ) ) { + $retval['listresult'] = ''; + } + + $result->setIndexedTagName( $retval['props'], 'prop' ); + } + // Errors $retval['errors'] = $this->parseErrors( $obj->getPossibleErrors() ); $result->setIndexedTagName( $retval['errors'], 'error' ); diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 893491b9..db6e2bb8 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -2,7 +2,7 @@ /** * Created on Dec 01, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -59,19 +59,15 @@ class ApiParse extends ApiBase { // The parser needs $wgTitle to be set, apparently the // $title parameter in Parser::parse isn't enough *sigh* // TODO: Does this still need $wgTitle? - global $wgParser, $wgTitle, $wgLang; + global $wgParser, $wgTitle; // Currently unnecessary, code to act as a safeguard against any change in current behaviour of uselang breaks $oldLang = null; - if ( isset( $params['uselang'] ) && $params['uselang'] != $wgLang->getCode() ) { - $oldLang = $wgLang; // Backup wgLang - $wgLang = Language::factory( $params['uselang'] ); + if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) { + $oldLang = $this->getContext()->getLanguage(); // Backup language + $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) ); } - $popts = ParserOptions::newFromContext( $this->getContext() ); - $popts->setTidy( true ); - $popts->enableLimitReport( !$params['disablepp'] ); - $redirValues = null; // Return result @@ -89,13 +85,15 @@ class ApiParse extends ApiBase { } $titleObj = $rev->getTitle(); - $wgTitle = $titleObj; + $pageObj = WikiPage::factory( $titleObj ); + $popts = $pageObj->makeParserOptions( $this->getContext() ); + $popts->enableLimitReport( !$params['disablepp'] ); // If for some reason the "oldid" is actually the current revision, it may be cached if ( $titleObj->getLatestRevID() === intval( $oldid ) ) { // May get from/save to parser cache - $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid, + $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ; } else { // This is an old revision, so get the text differently $this->text = $rev->getText( Revision::FOR_THIS_USER, $this->getUser() ); @@ -129,32 +127,26 @@ class ApiParse extends ApiBase { foreach ( (array)$redirValues as $r ) { $to = $r['to']; } - $titleObj = Title::newFromText( $to ); - } else { - if ( !is_null ( $pageid ) ) { - $reqParams['pageids'] = $pageid; - $titleObj = Title::newFromID( $pageid ); - } else { // $page - $to = $page; - $titleObj = Title::newFromText( $to ); - } - } - if ( !is_null ( $pageid ) ) { - if ( !$titleObj ) { - // Still throw nosuchpageid error if pageid was provided - $this->dieUsageMsg( array( 'nosuchpageid', $pageid ) ); - } - } elseif ( !$titleObj || !$titleObj->exists() ) { - $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' ); + $pageParams = array( 'title' => $to ); + } elseif ( !is_null( $pageid ) ) { + $pageParams = array( 'pageid' => $pageid ); + } else { // $page + $pageParams = array( 'title' => $page ); } + + $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' ); + $titleObj = $pageObj->getTitle(); $wgTitle = $titleObj; if ( isset( $prop['revid'] ) ) { - $oldid = $titleObj->getLatestRevID(); + $oldid = $pageObj->getLatest(); } + $popts = $pageObj->makeParserOptions( $this->getContext() ); + $popts->enableLimitReport( !$params['disablepp'] ); + // Potentially cached - $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid, + $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ; } } else { // Not $oldid, $pageid, $page. Hence based on $text @@ -168,6 +160,10 @@ class ApiParse extends ApiBase { $this->dieUsageMsg( array( 'invalidtitle', $title ) ); } $wgTitle = $titleObj; + $pageObj = WikiPage::factory( $titleObj ); + + $popts = $pageObj->makeParserOptions( $this->getContext() ); + $popts->enableLimitReport( !$params['disablepp'] ); if ( $this->section !== false ) { $this->text = $this->getSectionText( $this->text, $titleObj->getText() ); @@ -285,6 +281,21 @@ class ApiParse extends ApiBase { $result->setContent( $result_array['psttext'], $this->pstText ); } } + if ( isset( $prop['properties'] ) ) { + $result_array['properties'] = $this->formatProperties( $p_result->getProperties() ); + } + + if ( $params['generatexml'] ) { + $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS ); + $dom = $wgParser->preprocessToDom( $this->text ); + if ( is_callable( array( $dom, 'saveXML' ) ) ) { + $xml = $dom->saveXML(); + } else { + $xml = $dom->__toString(); + } + $result_array['parsetree'] = array(); + $result->setContent( $result_array['parsetree'], $xml ); + } $result_mapping = array( 'redirects' => 'r', @@ -297,37 +308,39 @@ class ApiParse extends ApiBase { 'iwlinks' => 'iw', 'sections' => 's', 'headitems' => 'hi', + 'properties' => 'pp', ); $this->setIndexedTagNames( $result_array, $result_mapping ); $result->addValue( null, $this->getModuleName(), $result_array ); if ( !is_null( $oldLang ) ) { - $wgLang = $oldLang; // Reset $wgLang to $oldLang + $this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang } } /** - * @param $titleObj Title + * @param $page WikiPage * @param $popts ParserOptions * @param $pageId Int * @param $getWikitext Bool * @return ParserOutput */ - private function getParsedSectionOrText( $titleObj, $popts, $pageId = null, $getWikitext = false ) { + private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) { global $wgParser; - $page = WikiPage::factory( $titleObj ); - if ( $this->section !== false ) { $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId ) - ? 'page id ' . $pageId : $titleObj->getText() ); + ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() ); // Not cached (save or load) - return $wgParser->parse( $this->text, $titleObj, $popts ); + return $wgParser->parse( $this->text, $page->getTitle(), $popts ); } else { // Try the parser cache first // getParserOutput will save to Parser cache if able $pout = $page->getParserOutput( $popts ); + if ( !$pout ) { + $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' ); + } if ( $getWikitext ) { $this->text = $page->getRawText(); } @@ -394,19 +407,19 @@ class ApiParse extends ApiBase { return ''; } - $s = htmlspecialchars( wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' ) ); + $s = htmlspecialchars( wfMessage( 'otherlanguages' )->text() . wfMessage( 'colon-separator' )->text() ); $langs = array(); foreach ( $languages as $l ) { $nt = Title::newFromText( $l ); - $text = $wgContLang->getLanguageName( $nt->getInterwiki() ); + $text = Language::fetchLanguageName( $nt->getInterwiki() ); $langs[] = Html::element( 'a', array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ), $text == '' ? $l : $text ); } - $s .= implode( htmlspecialchars( wfMsgExt( 'pipe-separator', 'escapenoentities' ) ), $langs ); + $s .= implode( wfMessage( 'pipe-separator' )->escaped(), $langs ); if ( $wgContLang->isRTL() ) { $s = Html::rawElement( 'span', array( 'dir' => "LTR" ), $s ); @@ -461,6 +474,17 @@ class ApiParse extends ApiBase { return $result; } + private function formatProperties( $properties ) { + $result = array(); + foreach ( $properties as $name => $value ) { + $entry = array(); + $entry['name'] = $name; + $this->getResult()->setContent( $entry, $value ); + $result[] = $entry; + } + return $result; + } + private function formatCss( $css ) { $result = array(); foreach ( $css as $file => $link ) { @@ -496,7 +520,7 @@ class ApiParse extends ApiBase { ApiBase::PARAM_TYPE => 'integer', ), 'prop' => array( - ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle', + ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle|iwlinks|properties', ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array( 'text', @@ -515,6 +539,7 @@ class ApiParse extends ApiBase { 'headhtml', 'iwlinks', 'wikitext', + 'properties', ) ), 'pst' => false, @@ -522,6 +547,7 @@ class ApiParse extends ApiBase { 'uselang' => null, 'section' => null, 'disablepp' => false, + 'generatexml' => false, ); } @@ -553,6 +579,7 @@ class ApiParse extends ApiBase { ' headhtml - Gives parsed <head> of the page', ' iwlinks - Gives interwiki links in the parsed wikitext', ' wikitext - Gives the original wikitext that was parsed', + ' properties - Gives various properties defined in the parsed wikitext', ), 'pst' => array( 'Do a pre-save transform on the input before parsing it', @@ -565,11 +592,15 @@ class ApiParse extends ApiBase { 'uselang' => 'Which language to parse the request in', 'section' => 'Only retrieve the content of this section number', 'disablepp' => 'Disable the PP Report from the parser output', + 'generatexml' => 'Generate XML parse tree', ); } public function getDescription() { - return 'Parses wikitext and returns parser output'; + return array( + 'Parses wikitext and returns parser output', + 'See the various prop-Modules of action=query to get information from the current version of a page', + ); } public function getPossibleErrors() { diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index 1332f263..cb5e081a 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -65,7 +65,10 @@ class ApiPatrol extends ApiBase { public function getAllowedParams() { return array( - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'rcid' => array( ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_REQUIRED => true @@ -80,6 +83,16 @@ class ApiPatrol extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'rcid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return 'Patrol a page or revision'; } diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index fb225d86..b3ca67e6 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -4,7 +4,7 @@ * * Created on Sep 1, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -37,10 +37,8 @@ class ApiProtect extends ApiBase { global $wgRestrictionLevels; $params = $this->extractRequestParams(); - $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj ) { - $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); - } + $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); + $titleObj = $pageObj->getTitle(); $errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() ); if ( $errors ) { @@ -58,7 +56,7 @@ class ApiProtect extends ApiBase { } $restrictionTypes = $titleObj->getRestrictionTypes(); - $dbr = wfGetDB( DB_SLAVE ); + $db = $this->getDB(); $protections = array(); $expiryarray = array(); @@ -82,7 +80,7 @@ class ApiProtect extends ApiBase { } if ( in_array( $expiry[$i], array( 'infinite', 'indefinite', 'never' ) ) ) { - $expiryarray[$p[0]] = $dbr->getInfinity(); + $expiryarray[$p[0]] = $db->getInfinity(); } else { $exp = strtotime( $expiry[$i] ); if ( $exp < 0 || !$exp ) { @@ -96,7 +94,7 @@ class ApiProtect extends ApiBase { $expiryarray[$p[0]] = $exp; } $resultProtections[] = array( $p[0] => $protections[$p[0]], - 'expiry' => ( $expiryarray[$p[0]] == $dbr->getInfinity() ? + 'expiry' => ( $expiryarray[$p[0]] == $db->getInfinity() ? 'infinite' : wfTimestamp( TS_ISO_8601, $expiryarray[$p[0]] ) ) ); } @@ -106,7 +104,6 @@ class ApiProtect extends ApiBase { $watch = $params['watch'] ? 'watch' : $params['watchlist']; $this->setWatch( $watch, $titleObj ); - $pageObj = WikiPage::factory( $titleObj ); $status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() ); if ( !$status->isOK() ) { @@ -138,9 +135,14 @@ class ApiProtect extends ApiBase { return array( 'title' => array( ApiBase::PARAM_TYPE => 'string', + ), + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer', + ), + 'token' => array( + ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, 'protections' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_REQUIRED => true, @@ -169,13 +171,15 @@ class ApiProtect extends ApiBase { } public function getParamDescription() { + $p = $this->getModulePrefix(); return array( - 'title' => 'Title of the page you want to (un)protect', + 'title' => "Title of the page you want to (un)protect. Cannot be used together with {$p}pageid", + 'pageid' => "ID of the page you want to (un)protect. Cannot be used together with {$p}title", 'token' => 'A protect token previously retrieved through prop=info', - 'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)', + 'protections' => 'List of protection levels, formatted action=group (e.g. edit=sysop)', 'expiry' => array( 'Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.', 'Use \'infinite\', \'indefinite\' or \'never\', for a neverexpiring protection.' ), - 'reason' => 'Reason for (un)protecting (optional)', + 'reason' => 'Reason for (un)protecting', 'cascade' => array( 'Enable cascading protection (i.e. protect pages included in this page)', 'Ignored if not all protection levels are \'sysop\' or \'protect\'' ), 'watch' => 'If set, add the page being (un)protected to your watchlist', @@ -183,21 +187,33 @@ class ApiProtect extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'title' => 'string', + 'reason' => 'string', + 'cascade' => 'boolean' + ) + ); + } + public function getDescription() { return 'Change the protection level of a page'; } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'invalidtitle', 'title' ), - array( 'toofewexpiries', 'noofexpiries', 'noofprotections' ), - array( 'create-titleexists' ), - array( 'missingtitle-createonly' ), - array( 'protect-invalidaction', 'action' ), - array( 'protect-invalidlevel', 'level' ), - array( 'invalidexpiry', 'expiry' ), - array( 'pastexpiry', 'expiry' ), - ) ); + return array_merge( parent::getPossibleErrors(), + $this->getTitleOrPageIdErrorMessage(), + array( + array( 'toofewexpiries', 'noofexpiries', 'noofprotections' ), + array( 'create-titleexists' ), + array( 'missingtitle-createonly' ), + array( 'protect-invalidaction', 'action' ), + array( 'protect-invalidlevel', 'level' ), + array( 'invalidexpiry', 'expiry' ), + array( 'pastexpiry', 'expiry' ), + ) + ); } public function needsToken() { diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 9e9320fb..9fedaf1b 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -88,13 +88,13 @@ class ApiPurge extends ApiBase { if ( !$user->pingLimiter() ) { global $wgParser, $wgEnableParserCache; - $popts = ParserOptions::newFromContext( $this->getContext() ); + $popts = $page->makeParserOptions( 'canonical' ); $p_result = $wgParser->parse( $page->getRawText(), $title, $popts, true, true, $page->getLatest() ); # Update the links tables - $u = new LinksUpdate( $title, $p_result ); - $u->doUpdate(); + $updates = $p_result->getSecondaryDataUpdates( $title ); + DataUpdate::runUpdates( $updates ); $r['linkupdate'] = ''; @@ -103,7 +103,8 @@ class ApiPurge extends ApiBase { $pcache->save( $p_result, $page, $popts ); } } else { - $this->setWarning( $this->parseMsg( array( 'actionthrottledtext' ) ) ); + $error = $this->parseMsg( array( 'actionthrottledtext' ) ); + $this->setWarning( $error['info'] ); $forceLinkUpdate = false; } } @@ -133,6 +134,34 @@ class ApiPurge extends ApiBase { ); } + public function getResultProperties() { + return array( + ApiBase::PROP_LIST => true, + '' => array( + 'ns' => array( + ApiBase::PROP_TYPE => 'namespace', + ApiBase::PROP_NULLABLE => true + ), + 'title' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'pageid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'revid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'invalid' => 'boolean', + 'missing' => 'boolean', + 'purged' => 'boolean', + 'linkupdate' => 'boolean' + ) + ); + } + public function getDescription() { return array( 'Purge the cache for the given titles.', 'Requires a POST request if the user is not logged in.' @@ -143,7 +172,6 @@ class ApiPurge extends ApiBase { $psModule = new ApiPageSet( $this ); return array_merge( parent::getPossibleErrors(), - array( array( 'cantpurge' ), ), $psModule->getPossibleErrors() ); } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index cd54a7da..554aae5a 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -47,55 +47,55 @@ class ApiQuery extends ApiBase { private $params, $redirects, $convertTitles, $iwUrl; private $mQueryPropModules = array( + 'categories' => 'ApiQueryCategories', + 'categoryinfo' => 'ApiQueryCategoryInfo', + 'duplicatefiles' => 'ApiQueryDuplicateFiles', + 'extlinks' => 'ApiQueryExternalLinks', + 'images' => 'ApiQueryImages', + 'imageinfo' => 'ApiQueryImageInfo', 'info' => 'ApiQueryInfo', - 'revisions' => 'ApiQueryRevisions', 'links' => 'ApiQueryLinks', 'iwlinks' => 'ApiQueryIWLinks', 'langlinks' => 'ApiQueryLangLinks', - 'images' => 'ApiQueryImages', - 'imageinfo' => 'ApiQueryImageInfo', + 'pageprops' => 'ApiQueryPageProps', + 'revisions' => 'ApiQueryRevisions', 'stashimageinfo' => 'ApiQueryStashImageInfo', 'templates' => 'ApiQueryLinks', - 'categories' => 'ApiQueryCategories', - 'extlinks' => 'ApiQueryExternalLinks', - 'categoryinfo' => 'ApiQueryCategoryInfo', - 'duplicatefiles' => 'ApiQueryDuplicateFiles', - 'pageprops' => 'ApiQueryPageProps', ); private $mQueryListModules = array( - 'allimages' => 'ApiQueryAllimages', - 'allpages' => 'ApiQueryAllpages', - 'alllinks' => 'ApiQueryAllLinks', 'allcategories' => 'ApiQueryAllCategories', + 'allimages' => 'ApiQueryAllImages', + 'alllinks' => 'ApiQueryAllLinks', + 'allpages' => 'ApiQueryAllPages', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', 'blocks' => 'ApiQueryBlocks', 'categorymembers' => 'ApiQueryCategoryMembers', 'deletedrevs' => 'ApiQueryDeletedrevs', 'embeddedin' => 'ApiQueryBacklinks', + 'exturlusage' => 'ApiQueryExtLinksUsage', 'filearchive' => 'ApiQueryFilearchive', 'imageusage' => 'ApiQueryBacklinks', 'iwbacklinks' => 'ApiQueryIWBacklinks', 'langbacklinks' => 'ApiQueryLangBacklinks', 'logevents' => 'ApiQueryLogEvents', + 'protectedtitles' => 'ApiQueryProtectedTitles', + 'querypage' => 'ApiQueryQueryPage', + 'random' => 'ApiQueryRandom', 'recentchanges' => 'ApiQueryRecentChanges', 'search' => 'ApiQuerySearch', 'tags' => 'ApiQueryTags', 'usercontribs' => 'ApiQueryContributions', + 'users' => 'ApiQueryUsers', 'watchlist' => 'ApiQueryWatchlist', 'watchlistraw' => 'ApiQueryWatchlistRaw', - 'exturlusage' => 'ApiQueryExtLinksUsage', - 'users' => 'ApiQueryUsers', - 'random' => 'ApiQueryRandom', - 'protectedtitles' => 'ApiQueryProtectedTitles', - 'querypage' => 'ApiQueryQueryPage', ); private $mQueryMetaModules = array( + 'allmessages' => 'ApiQueryAllMessages', 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', - 'allmessages' => 'ApiQueryAllmessages', ); private $mSlaveDB = null; @@ -103,11 +103,16 @@ class ApiQuery extends ApiBase { protected $mAllowedGenerators = array(); + /** + * @param $main ApiMain + * @param $action string + */ public function __construct( $main, $action ) { parent::__construct( $main, $action ); // Allow custom modules to be added in LocalSettings.php - global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules; + global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules, + $wgMemc, $wgAPICacheHelpTimeout; self::appendUserModules( $this->mQueryPropModules, $wgAPIPropModules ); self::appendUserModules( $this->mQueryListModules, $wgAPIListModules ); self::appendUserModules( $this->mQueryMetaModules, $wgAPIMetaModules ); @@ -116,8 +121,22 @@ class ApiQuery extends ApiBase { $this->mListModuleNames = array_keys( $this->mQueryListModules ); $this->mMetaModuleNames = array_keys( $this->mQueryMetaModules ); - $this->makeHelpMsgHelper( $this->mQueryPropModules, 'prop' ); - $this->makeHelpMsgHelper( $this->mQueryListModules, 'list' ); + // Get array of query generators from cache if present + $key = wfMemcKey( 'apiquerygenerators', SpecialVersion::getVersion( 'nodb' ) ); + + if ( $wgAPICacheHelpTimeout > 0 ) { + $cached = $wgMemc->get( $key ); + if ( $cached ) { + $this->mAllowedGenerators = $cached; + return; + } + } + $this->makeGeneratorList( $this->mQueryPropModules ); + $this->makeGeneratorList( $this->mQueryListModules ); + + if ( $wgAPICacheHelpTimeout > 0 ) { + $wgMemc->set( $key, $this->mAllowedGenerators, $wgAPICacheHelpTimeout ); + } } /** @@ -135,7 +154,7 @@ class ApiQuery extends ApiBase { /** * Gets a default slave database connection object - * @return Database + * @return DatabaseBase */ public function getDB() { if ( !isset( $this->mSlaveDB ) ) { @@ -154,7 +173,7 @@ class ApiQuery extends ApiBase { * @param $name string Name to assign to the database connection * @param $db int One of the DB_* constants * @param $groups array Query groups - * @return Database + * @return DatabaseBase */ public function getNamedDB( $name, $db, $groups ) { if ( !array_key_exists( $name, $this->mNamedDB ) ) { @@ -202,6 +221,9 @@ class ApiQuery extends ApiBase { return null; } + /** + * @return ApiFormatRaw|null + */ public function getCustomPrinter() { // If &exportnowrap is set, use the raw formatter if ( $this->getParameter( 'export' ) && @@ -258,6 +280,9 @@ class ApiQuery extends ApiBase { $this->outputGeneralPageInfo(); // Execute all requested modules. + /** + * @var $module ApiQueryBase + */ foreach ( $modules as $module ) { $params = $module->extractRequestParams(); $cacheMode = $this->mergeCacheMode( @@ -303,6 +328,9 @@ class ApiQuery extends ApiBase { */ private function addCustomFldsToPageSet( $modules, $pageSet ) { // Query all requested modules. + /** + * @var $module ApiQueryBase + */ foreach ( $modules as $module ) { $module->requestExtraData( $pageSet ); } @@ -384,6 +412,9 @@ class ApiQuery extends ApiBase { // Show redirect information $redirValues = array(); + /** + * @var $titleTo Title + */ foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleTo ) { $r = array( 'from' => strval( $titleStrFrom ), @@ -602,7 +633,6 @@ class ApiQuery extends ApiBase { // Make sure the internal object is empty // (just in case a sub-module decides to optimize during instantiation) $this->mPageSet = null; - $this->mAllowedGenerators = array(); // Will be repopulated $querySeparator = str_repeat( '--- ', 12 ); $moduleSeparator = str_repeat( '*** ', 14 ); @@ -614,8 +644,6 @@ class ApiQuery extends ApiBase { $msg .= $this->makeHelpMsgHelper( $this->mQueryMetaModules, 'meta' ); $msg .= "\n\n$moduleSeparator Modules: continuation $moduleSeparator\n\n"; - // Perform the base call last because the $this->mAllowedGenerators - // will be updated inside makeHelpMsgHelper() // Use parent to make default message for the query module $msg = parent::makeHelpMsg() . $msg; @@ -643,7 +671,6 @@ class ApiQuery extends ApiBase { $msg .= $msg2; } if ( $module instanceof ApiQueryGeneratorBase ) { - $this->mAllowedGenerators[] = $moduleName; $msg .= "Generator:\n This module may be used as a generator\n"; } $moduleDescriptions[] = $msg; @@ -653,6 +680,19 @@ class ApiQuery extends ApiBase { } /** + * Adds any classes that are a subclass of ApiQueryGeneratorBase + * to the allowed generator list + * @param $moduleList array() + */ + private function makeGeneratorList( $moduleList ) { + foreach( $moduleList as $moduleName => $moduleClass ) { + if ( is_subclass_of( $moduleClass, 'ApiQueryGeneratorBase' ) ) { + $this->mAllowedGenerators[] = $moduleName; + } + } + } + + /** * Override to add extra parameters from PageSet * @return string */ @@ -674,7 +714,7 @@ class ApiQuery extends ApiBase { 'NOTE: generator parameter names must be prefixed with a \'g\', see examples' ), 'redirects' => 'Automatically resolve redirects', 'converttitles' => array( "Convert titles to other variants if necessary. Only works if the wiki's content language supports variant conversion.", - 'Languages that support variant conversion include gan, iu, kk, ku, shi, sr, tg, zh' ), + 'Languages that support variant conversion include ' . implode( ', ', LanguageConverter::$languagesWithVariants ) ), 'indexpageids' => 'Include an additional pageids section listing all returned page IDs', 'export' => 'Export the current revisions of all given or generated pages', 'exportnowrap' => 'Return the export XML without wrapping it in an XML result (same format as Special:Export). Can only be used with export', diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 78367a45..4f4c77f0 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -4,7 +4,7 @@ * * Created on December 12, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -58,6 +58,17 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $this->addTables( 'category' ); $this->addFields( 'cat_title' ); + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( "Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue" ); + } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "cat_title $op= $cont_from" ); + } + $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); @@ -65,14 +76,20 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $min = $params['min']; $max = $params['max']; - $this->addWhereRange( 'cat_pages', $dir, $min, $max ); + if ( $dir == 'newer' ) { + $this->addWhereRange( 'cat_pages', 'newer', $min, $max ); + } else { + $this->addWhereRange( 'cat_pages', 'older', $max, $min); + } + if ( isset( $params['prefix'] ) ) { $this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); - $this->addOption( 'ORDER BY', 'cat_title' . ( $params['dir'] == 'descending' ? ' DESC' : '' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $this->addOption( 'ORDER BY', 'cat_title' . $sort ); $prop = array_flip( $params['prop'] ); $this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset( $prop['size'] ) ); @@ -86,7 +103,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { 'pp_page=page_id', 'pp_propname' => 'hiddencat' ) ), ) ); - $this->addFields( 'pp_propname AS cat_hidden' ); + $this->addFields( array( 'cat_hidden' => 'pp_propname' ) ); } $res = $this->select( __METHOD__ ); @@ -98,15 +115,14 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { foreach ( $res as $row ) { if ( ++ $count > $params['limit'] ) { // We've reached the one extra which shows that there are additional cats to be had. Stop here... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) ); + $this->setContinueEnumParameter( 'continue', $row->cat_title ); break; } // Normalize titles $titleObj = Title::makeTitle( NS_CATEGORY, $row->cat_title ); if ( !is_null( $resultPageSet ) ) { - $pages[] = $titleObj->getPrefixedText(); + $pages[] = $titleObj; } else { $item = array(); $result->setContent( $item, $titleObj->getText() ); @@ -121,7 +137,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $item ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) ); + $this->setContinueEnumParameter( 'continue', $row->cat_title ); break; } } @@ -137,6 +153,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { public function getAllowedParams() { return array( 'from' => null, + 'continue' => null, 'to' => null, 'prefix' => null, 'dir' => array( @@ -172,6 +189,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { public function getParamDescription() { return array( 'from' => 'The category to start enumerating from', + 'continue' => 'When more results are available, use this to continue', 'to' => 'The category to stop enumerating at', 'prefix' => 'Search for all category titles that begin with this value', 'dir' => 'Direction to sort in', @@ -186,10 +204,33 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + '*' => 'string' + ), + 'size' => array( + 'size' => 'integer', + 'pages' => 'integer', + 'files' => 'integer', + 'subcats' => 'integer' + ), + 'hidden' => array( + 'hidden' => 'boolean' + ) + ); + } + public function getDescription() { return 'Enumerate all categories'; } + public function getPossibleErrors() { + return array_merge( parent::getPossibleErrors(), array( + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), + ) ); + } + public function getExamples() { return array( 'api.php?action=query&list=allcategories&acprop=size', diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php new file mode 100644 index 00000000..b562da8e --- /dev/null +++ b/includes/api/ApiQueryAllImages.php @@ -0,0 +1,409 @@ +<?php + +/** + * API for MediaWiki 1.12+ + * + * Created on Mar 16, 2008 + * + * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com, + * based on ApiQueryAllPages.php + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Query module to enumerate all available pages. + * + * @ingroup API + */ +class ApiQueryAllImages extends ApiQueryGeneratorBase { + + protected $mRepo; + + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'ai' ); + $this->mRepo = RepoGroup::singleton()->getLocalRepo(); + } + + /** + * Override parent method to make sure to make sure the repo's DB is used + * which may not necesarilly be the same as the local DB. + * + * TODO: allow querying non-local repos. + * @return DatabaseBase + */ + protected function getDB() { + return $this->mRepo->getSlaveDB(); + } + + public function execute() { + $this->run(); + } + + public function getCacheMode( $params ) { + return 'public'; + } + + /** + * @param $resultPageSet ApiPageSet + * @return void + */ + public function executeGenerator( $resultPageSet ) { + if ( $resultPageSet->isResolvingRedirects() ) { + $this->dieUsage( 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params' ); + } + + $this->run( $resultPageSet ); + } + + /** + * @param $resultPageSet ApiPageSet + * @return void + */ + private function run( $resultPageSet = null ) { + $repo = $this->mRepo; + if ( !$repo instanceof LocalRepo ) { + $this->dieUsage( 'Local file repository does not support querying all images', 'unsupportedrepo' ); + } + + $prefix = $this->getModulePrefix(); + + $db = $this->getDB(); + + $params = $this->extractRequestParams(); + + // Table and return fields + $this->addTables( 'image' ); + + $prop = array_flip( $params['prop'] ); + $this->addFields( LocalFile::selectFields() ); + + $dir = ( in_array( $params['dir'], array( 'descending', 'older' ) ) ? 'older' : 'newer' ); + + if ( $params['sort'] == 'name' ) { + // Check mutually exclusive params + $disallowed = array( 'start', 'end', 'user' ); + foreach ( $disallowed as $pname ) { + if ( isset( $params[$pname] ) ) { + $this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=timestamp", 'badparams' ); + } + } + if ( $params['filterbots'] != 'all' ) { + $this->dieUsage( "Parameter '{$prefix}filterbots' can only be used with {$prefix}sort=timestamp", 'badparams' ); + } + + // Pagination + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( 'Invalid continue param. You should pass the ' . + 'original value returned by the previous query', '_badcontinue' ); + } + $op = ( $dir == 'older' ? '<' : '>' ); + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "img_name $op= $cont_from" ); + } + + // Image filters + $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); + $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); + $this->addWhereRange( 'img_name', $dir, $from, $to ); + + if ( isset( $params['prefix'] ) ) { + $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + } + } else { + // Check mutually exclusive params + $disallowed = array( 'from', 'to', 'prefix' ); + foreach ( $disallowed as $pname ) { + if ( isset( $params[$pname] ) ) { + $this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name", 'badparams' ); + } + } + if (!is_null( $params['user'] ) && $params['filterbots'] != 'all') { + // Since filterbots checks if each user has the bot right, it doesn't make sense to use it with user + $this->dieUsage( "Parameters 'user' and 'filterbots' cannot be used together", 'badparams' ); + } + + // Pagination + $this->addTimestampWhereRange( 'img_timestamp', $dir, $params['start'], $params['end'] ); + + // Image filters + if ( !is_null( $params['user'] ) ) { + $this->addWhereFld( 'img_user_text', $params['user'] ); + } + if ( $params['filterbots'] != 'all' ) { + $this->addTables( 'user_groups' ); + $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL': 'NOT NULL' ); + $this->addWhere( "ug_group IS $groupCond" ); + $this->addJoinConds( array( 'user_groups' => array( + 'LEFT JOIN', + array( + 'ug_group' => User::getGroupsWithPermission( 'bot' ), + 'ug_user = img_user' + ) + ) ) ); + } + } + + // Filters not depending on sort + if ( isset( $params['minsize'] ) ) { + $this->addWhere( 'img_size>=' . intval( $params['minsize'] ) ); + } + + if ( isset( $params['maxsize'] ) ) { + $this->addWhere( 'img_size<=' . intval( $params['maxsize'] ) ); + } + + $sha1 = false; + if ( isset( $params['sha1'] ) ) { + if ( !$this->validateSha1Hash( $params['sha1'] ) ) { + $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); + } + $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 ); + } elseif ( isset( $params['sha1base36'] ) ) { + $sha1 = $params['sha1base36']; + if ( !$this->validateSha1Base36Hash( $sha1 ) ) { + $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); + } + } + if ( $sha1 ) { + $this->addWhereFld( 'img_sha1', $sha1 ); + } + + if ( !is_null( $params['mime'] ) ) { + global $wgMiserMode; + if ( $wgMiserMode ) { + $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' ); + } + + list( $major, $minor ) = File::splitMime( $params['mime'] ); + + $this->addWhereFld( 'img_major_mime', $major ); + $this->addWhereFld( 'img_minor_mime', $minor ); + } + + $limit = $params['limit']; + $this->addOption( 'LIMIT', $limit + 1 ); + $sort = ( $dir == 'older' ? ' DESC' : '' ); + if ( $params['sort'] == 'timestamp' ) { + $this->addOption( 'ORDER BY', 'img_timestamp' . $sort ); + if ( $params['filterbots'] == 'all' ) { + $this->addOption( 'USE INDEX', array( 'image' => 'img_timestamp' ) ); + } else { + $this->addOption( 'USE INDEX', array( 'image' => 'img_usertext_timestamp' ) ); + } + } else { + $this->addOption( 'ORDER BY', 'img_name' . $sort ); + } + + $res = $this->select( __METHOD__ ); + + $titles = array(); + $count = 0; + $result = $this->getResult(); + foreach ( $res as $row ) { + if ( ++ $count > $limit ) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ( $params['sort'] == 'name' ) { + $this->setContinueEnumParameter( 'continue', $row->img_name ); + } else { + $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) ); + } + break; + } + + if ( is_null( $resultPageSet ) ) { + $file = $repo->newFileFromRow( $row ); + $info = array_merge( array( 'name' => $row->img_name ), + ApiQueryImageInfo::getInfo( $file, $prop, $result ) ); + self::addTitleInfo( $info, $file->getTitle() ); + + $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info ); + if ( !$fit ) { + if ( $params['sort'] == 'name' ) { + $this->setContinueEnumParameter( 'continue', $row->img_name ); + } else { + $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) ); + } + break; + } + } else { + $titles[] = Title::makeTitle( NS_FILE, $row->img_name ); + } + } + + if ( is_null( $resultPageSet ) ) { + $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' ); + } else { + $resultPageSet->populateFromTitles( $titles ); + } + } + + public function getAllowedParams() { + return array ( + 'sort' => array( + ApiBase::PARAM_DFLT => 'name', + ApiBase::PARAM_TYPE => array( + 'name', + 'timestamp' + ) + ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + // sort=name + 'ascending', + 'descending', + // sort=timestamp + 'newer', + 'older', + ) + ), + 'from' => null, + 'to' => null, + 'continue' => null, + 'start' => array( + ApiBase::PARAM_TYPE => 'timestamp' + ), + 'end' => array( + ApiBase::PARAM_TYPE => 'timestamp' + ), + 'prop' => array( + ApiBase::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( $this->propertyFilter ), + ApiBase::PARAM_DFLT => 'timestamp|url', + ApiBase::PARAM_ISMULTI => true + ), + 'prefix' => null, + 'minsize' => array( + ApiBase::PARAM_TYPE => 'integer', + ), + 'maxsize' => array( + ApiBase::PARAM_TYPE => 'integer', + ), + 'sha1' => null, + 'sha1base36' => null, + 'user' => array( + ApiBase::PARAM_TYPE => 'user' + ), + 'filterbots' => array( + ApiBase::PARAM_DFLT => 'all', + ApiBase::PARAM_TYPE => array( + 'all', + 'bots', + 'nobots' + ) + ), + 'mime' => null, + 'limit' => array( + ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, + ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 + ), + ); + } + + public function getParamDescription() { + $p = $this->getModulePrefix(); + return array( + 'sort' => 'Property to sort by', + 'dir' => 'The direction in which to list', + 'from' => "The image title to start enumerating from. Can only be used with {$p}sort=name", + 'to' => "The image title to stop enumerating at. Can only be used with {$p}sort=name", + 'continue' => 'When more results are available, use this to continue', + 'start' => "The timestamp to start enumerating from. Can only be used with {$p}sort=timestamp", + 'end' => "The timestamp to end enumerating. Can only be used with {$p}sort=timestamp", + 'prop' => ApiQueryImageInfo::getPropertyDescriptions( $this->propertyFilter ), + 'prefix' => "Search for all image titles that begin with this value. Can only be used with {$p}sort=name", + 'minsize' => 'Limit to images with at least this many bytes', + 'maxsize' => 'Limit to images with at most this many bytes', + 'sha1' => "SHA1 hash of image. Overrides {$p}sha1base36", + 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)', + 'user' => "Only return files uploaded by this user. Can only be used with {$p}sort=timestamp. Cannot be used together with {$p}filterbots", + 'filterbots' => "How to filter files uploaded by bots. Can only be used with {$p}sort=timestamp. Cannot be used together with {$p}user", + 'mime' => 'What MIME type to search for. e.g. image/jpeg. Disabled in Miser Mode', + 'limit' => 'How many images in total to return', + ); + } + + private $propertyFilter = array( 'archivename', 'thumbmime' ); + + public function getResultProperties() { + return array_merge( + array( + '' => array( + 'name' => 'string', + 'ns' => 'namespace', + 'title' => 'string' + ) + ), + ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter ) + ); + } + + public function getDescription() { + return 'Enumerate all images sequentially'; + } + + public function getPossibleErrors() { + $p = $this->getModulePrefix(); + return array_merge( parent::getPossibleErrors(), array( + array( 'code' => 'params', 'info' => 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator' ), + array( 'code' => 'badparams', 'info' => "Parameter'{$p}start' can only be used with {$p}sort=timestamp" ), + array( 'code' => 'badparams', 'info' => "Parameter'{$p}end' can only be used with {$p}sort=timestamp" ), + array( 'code' => 'badparams', 'info' => "Parameter'{$p}user' can only be used with {$p}sort=timestamp" ), + array( 'code' => 'badparams', 'info' => "Parameter'{$p}filterbots' can only be used with {$p}sort=timestamp" ), + array( 'code' => 'badparams', 'info' => "Parameter'{$p}from' can only be used with {$p}sort=name" ), + array( 'code' => 'badparams', 'info' => "Parameter'{$p}to' can only be used with {$p}sort=name" ), + array( 'code' => 'badparams', 'info' => "Parameter'{$p}prefix' can only be used with {$p}sort=name" ), + array( 'code' => 'badparams', 'info' => "Parameters 'user' and 'filterbots' cannot be used together" ), + array( 'code' => 'unsupportedrepo', 'info' => 'Local file repository does not support querying all images' ), + array( 'code' => 'mimesearchdisabled', 'info' => 'MIME search disabled in Miser Mode' ), + array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ), + array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), + ) ); + } + + public function getExamples() { + return array( + 'api.php?action=query&list=allimages&aifrom=B' => array( + 'Simple Use', + 'Show a list of files starting at the letter "B"', + ), + 'api.php?action=query&list=allimages&aiprop=user|timestamp|url&aisort=timestamp&aidir=older' => array( + 'Simple Use', + 'Show a list of recently uploaded files similar to Special:NewFiles', + ), + 'api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo' => array( + 'Using as Generator', + 'Show info about 4 files starting at the letter "T"', + ), + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Allimages'; + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 903f144f..da4840f0 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -4,7 +4,7 @@ * * Created on July 7, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -76,17 +76,26 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $this->dieUsage( 'alcontinue and alfrom cannot be used together', 'params' ); } if ( !is_null( $params['continue'] ) ) { - $arr = explode( '|', $params['continue'] ); - if ( count( $arr ) != 2 ) { - $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); + $continueArr = explode( '|', $params['continue'] ); + $op = $params['dir'] == 'descending' ? '<' : '>'; + if ( $params['unique'] ) { + if ( count( $continueArr ) != 1 ) { + $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); + } + $continueTitle = $db->addQuotes( $continueArr[0] ); + $this->addWhere( "pl_title $op= $continueTitle" ); + } else { + if ( count( $continueArr ) != 2 ) { + $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); + } + $continueTitle = $db->addQuotes( $continueArr[0] ); + $continueFrom = intval( $continueArr[1] ); + $this->addWhere( + "pl_title $op $continueTitle OR " . + "(pl_title = $continueTitle AND " . + "pl_from $op= $continueFrom)" + ); } - $from = $this->getDB()->strencode( $this->titleToKey( $arr[0] ) ); - $id = intval( $arr[1] ); - $this->addWhere( - "pl_title > '$from' OR " . - "(pl_title = '$from' AND " . - "pl_from > $id)" - ); } $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); @@ -104,9 +113,13 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $orderBy = array(); + $orderBy[] = 'pl_title' . $sort; if ( !$params['unique'] ) { - $this->addOption( 'ORDER BY', 'pl_title, pl_from' ); + $orderBy[] = 'pl_from' . $sort; } + $this->addOption( 'ORDER BY', $orderBy ); $res = $this->select( __METHOD__ ); @@ -116,11 +129,10 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { foreach ( $res as $row ) { if ( ++ $count > $limit ) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // TODO: Security issue - if the user has no right to view next title, it will still be shown if ( $params['unique'] ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->pl_title ); } else { - $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from ); + $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from ); } break; } @@ -137,9 +149,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { if ( $params['unique'] ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->pl_title ); } else { - $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from ); + $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from ); } break; } @@ -180,7 +192,14 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ) + ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ) + ), ); } @@ -199,6 +218,19 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 'namespace' => 'The namespace to enumerate', 'limit' => 'How many total links to return', 'continue' => 'When more results are available, use this to continue', + 'dir' => 'The direction in which to list', + ); + } + + public function getResultProperties() { + return array( + 'ids' => array( + 'fromid' => 'integer' + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string' + ) ); } diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllMessages.php index 44774927..f5e1146b 100644 --- a/includes/api/ApiQueryAllmessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -4,7 +4,7 @@ * * Created on Dec 1, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -29,7 +29,7 @@ * * @ingroup API */ -class ApiQueryAllmessages extends ApiQueryBase { +class ApiQueryAllMessages extends ApiQueryBase { public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'am' ); @@ -256,6 +256,27 @@ class ApiQueryAllmessages extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'name' => 'string', + 'customised' => 'boolean', + 'missing' => 'boolean', + '*' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'default' => array( + 'defaultmissing' => 'boolean', + 'default' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return 'Return messages from this site'; } diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllPages.php index e003ee91..16cc31d2 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllPages.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -29,7 +29,7 @@ * * @ingroup API */ -class ApiQueryAllpages extends ApiQueryGeneratorBase { +class ApiQueryAllPages extends ApiQueryGeneratorBase { public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ap' ); @@ -67,6 +67,17 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { // Page filters $this->addTables( 'page' ); + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( "Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue" ); + } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "page_title $op= $cont_from" ); + } + if ( $params['filterredir'] == 'redirects' ) { $this->addWhereFld( 'page_is_redirect', 1 ); } elseif ( $params['filterredir'] == 'nonredirects' ) { @@ -153,7 +164,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->addOption( 'STRAIGHT_JOIN' ); // We have to GROUP BY all selected fields to stop // PostgreSQL from whining - $this->addOption( 'GROUP BY', implode( ', ', $selectFields ) ); + $this->addOption( 'GROUP BY', $selectFields ); $forceNameTitleIndex = false; } @@ -165,13 +176,22 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->addOption( 'LIMIT', $limit + 1 ); $res = $this->select( __METHOD__ ); + //Get gender information + if( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) { + $users = array(); + foreach ( $res as $row ) { + $users[] = $row->page_title; + } + GenderCache::singleton()->doQuery( $users, __METHOD__ ); + $res->rewind(); //reset + } + $count = 0; $result = $this->getResult(); foreach ( $res as $row ) { if ( ++ $count > $limit ) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) ); + $this->setContinueEnumParameter( 'continue', $row->page_title ); break; } @@ -184,7 +204,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ); $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) ); + $this->setContinueEnumParameter( 'continue', $row->page_title ); break; } } else { @@ -202,6 +222,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { return array( 'from' => null, + 'continue' => null, 'to' => null, 'prefix' => null, 'namespace' => array( @@ -275,6 +296,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $p = $this->getModulePrefix(); return array( 'from' => 'The page title to start enumerating from', + 'continue' => 'When more results are available, use this to continue', 'to' => 'The page title to stop enumerating at', 'prefix' => 'Search for all page titles that begin with this value', 'namespace' => 'The namespace to enumerate', @@ -296,6 +318,16 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'pageid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return 'Enumerate all pages sequentially in a given namespace'; } @@ -304,6 +336,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { return array_merge( parent::getPossibleErrors(), array( array( 'code' => 'params', 'info' => 'Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator' ), array( 'code' => 'params', 'info' => 'prlevel may not be used without prtype' ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index ac112ef9..7f50cbad 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -4,7 +4,7 @@ * * Created on July 7, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -34,6 +34,16 @@ class ApiQueryAllUsers extends ApiQueryBase { parent::__construct( $query, $moduleName, 'au' ); } + /** + * This function converts the user name to a canonical form + * which is stored in the database. + * @param String $name + * @return String + */ + private function getCanonicalUserName( $name ) { + return str_replace( '_', ' ', $name ); + } + public function execute() { $db = $this->getDB(); $params = $this->extractRequestParams(); @@ -57,8 +67,8 @@ class ApiQueryAllUsers extends ApiQueryBase { $useIndex = true; $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); - $from = is_null( $params['from'] ) ? null : $this->keyToTitle( $params['from'] ); - $to = is_null( $params['to'] ) ? null : $this->keyToTitle( $params['to'] ); + $from = is_null( $params['from'] ) ? null : $this->getCanonicalUserName( $params['from'] ); + $to = is_null( $params['to'] ) ? null : $this->getCanonicalUserName( $params['to'] ); # MySQL doesn't seem to use 'equality propagation' here, so like the # ActiveUsers special page, we have to use rc_user_text for some cases. @@ -68,7 +78,7 @@ class ApiQueryAllUsers extends ApiQueryBase { if ( !is_null( $params['prefix'] ) ) { $this->addWhere( $userFieldToSort . - $db->buildLike( $this->keyToTitle( $params['prefix'] ), $db->anyString() ) ); + $db->buildLike( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) ); } if ( !is_null( $params['rights'] ) ) { @@ -142,11 +152,11 @@ class ApiQueryAllUsers extends ApiQueryBase { 'INNER JOIN', 'rc_user_text=user_name' ) ) ); - $this->addFields( 'COUNT(*) AS recentedits' ); + $this->addFields( array( 'recentedits' => 'COUNT(*)' ) ); - $this->addWhere( "rc_log_type IS NULL OR rc_log_type != 'newusers'" ); + $this->addWhere( 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ) ); $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ); - $this->addWhere( "rc_timestamp >= {$db->addQuotes( $timestamp )}" ); + $this->addWhere( 'rc_timestamp >= ' . $db->addQuotes( $timestamp ) ); $this->addOption( 'GROUP BY', $userFieldToSort ); } @@ -190,15 +200,14 @@ class ApiQueryAllUsers extends ApiQueryBase { $lastUserData = null; if ( !$fit ) { - $this->setContinueEnumParameter( 'from', - $this->keyToTitle( $lastUserData['name'] ) ); + $this->setContinueEnumParameter( 'from', $lastUserData['name'] ); break; } } if ( $count > $limit ) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->user_name ) ); + $this->setContinueEnumParameter( 'from', $row->user_name ); break; } @@ -209,7 +218,9 @@ class ApiQueryAllUsers extends ApiQueryBase { 'name' => $lastUser, ); if ( $fld_blockinfo && !is_null( $row->ipb_by_text ) ) { + $lastUserData['blockid'] = $row->ipb_id; $lastUserData['blockedby'] = $row->ipb_by_text; + $lastUserData['blockedbyid'] = $row->ipb_by; $lastUserData['blockreason'] = $row->ipb_reason; $lastUserData['blockexpiry'] = $row->ipb_expiry; } @@ -235,32 +246,45 @@ class ApiQueryAllUsers extends ApiQueryBase { 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function' ); } - $lastUserObj = User::newFromName( $lastUser ); + $lastUserObj = User::newFromId( $row->user_id ); // Add user's group info if ( $fld_groups ) { - if ( !isset( $lastUserData['groups'] ) && $lastUserObj ) { - $lastUserData['groups'] = ApiQueryUsers::getAutoGroups( $lastUserObj ); + if ( !isset( $lastUserData['groups'] ) ) { + if ( $lastUserObj ) { + $lastUserData['groups'] = $lastUserObj->getAutomaticGroups(); + } else { + // This should not normally happen + $lastUserData['groups'] = array(); + } } if ( !is_null( $row->ug_group2 ) ) { $lastUserData['groups'][] = $row->ug_group2; } + $result->setIndexedTagName( $lastUserData['groups'], 'g' ); } if ( $fld_implicitgroups && !isset( $lastUserData['implicitgroups'] ) && $lastUserObj ) { - $lastUserData['implicitgroups'] = ApiQueryUsers::getAutoGroups( $lastUserObj ); + $lastUserData['implicitgroups'] = $lastUserObj->getAutomaticGroups(); $result->setIndexedTagName( $lastUserData['implicitgroups'], 'g' ); } if ( $fld_rights ) { - if ( !isset( $lastUserData['rights'] ) && $lastUserObj ) { - $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() ); + if ( !isset( $lastUserData['rights'] ) ) { + if ( $lastUserObj ) { + $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() ); + } else { + // This should not normally happen + $lastUserData['rights'] = array(); + } } + if ( !is_null( $row->ug_group2 ) ) { $lastUserData['rights'] = array_unique( array_merge( $lastUserData['rights'], User::getGroupPermissions( array( $row->ug_group2 ) ) ) ); } + $result->setIndexedTagName( $lastUserData['rights'], 'r' ); } } @@ -269,8 +293,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $lastUserData ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', - $this->keyToTitle( $lastUserData['name'] ) ); + $this->setContinueEnumParameter( 'from', $lastUserData['name'] ); } } @@ -338,7 +361,7 @@ class ApiQueryAllUsers extends ApiQueryBase { 'dir' => 'Direction to sort in', 'group' => 'Limit users to given group name(s)', 'excludegroup' => 'Exclude users in given group name(s)', - 'rights' => 'Limit users to given right(s)', + 'rights' => 'Limit users to given right(s) (does not include rights granted by implicit or auto-promoted groups like *, user, or autoconfirmed)', 'prop' => array( 'What pieces of information to include.', ' blockinfo - Adds the information about a current block on the user', @@ -354,6 +377,48 @@ class ApiQueryAllUsers extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'userid' => 'integer', + 'name' => 'string', + 'recenteditcount' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'blockinfo' => array( + 'blockid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'blockedby' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'blockedbyid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'blockedreason' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'blockedexpiry' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'hidden' => 'boolean' + ), + 'editcount' => array( + 'editcount' => 'integer' + ), + 'registration' => array( + 'registration' => 'string' + ) + ); + } + public function getDescription() { return 'Enumerate all registered users'; } diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php deleted file mode 100644 index ca344f73..00000000 --- a/includes/api/ApiQueryAllimages.php +++ /dev/null @@ -1,267 +0,0 @@ -<?php - -/** - * API for MediaWiki 1.12+ - * - * Created on Mar 16, 2008 - * - * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com, - * based on ApiQueryAllpages.php - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * Query module to enumerate all available pages. - * - * @ingroup API - */ -class ApiQueryAllimages extends ApiQueryGeneratorBase { - - protected $mRepo; - - public function __construct( $query, $moduleName ) { - parent::__construct( $query, $moduleName, 'ai' ); - $this->mRepo = RepoGroup::singleton()->getLocalRepo(); - } - - /** - * Override parent method to make sure to make sure the repo's DB is used - * which may not necesarilly be the same as the local DB. - * - * TODO: allow querying non-local repos. - * @return DatabaseBase - */ - protected function getDB() { - return $this->mRepo->getSlaveDB(); - } - - public function execute() { - $this->run(); - } - - public function getCacheMode( $params ) { - return 'public'; - } - - /** - * @param $resultPageSet ApiPageSet - * @return void - */ - public function executeGenerator( $resultPageSet ) { - if ( $resultPageSet->isResolvingRedirects() ) { - $this->dieUsage( 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params' ); - } - - $this->run( $resultPageSet ); - } - - /** - * @param $resultPageSet ApiPageSet - * @return void - */ - private function run( $resultPageSet = null ) { - $repo = $this->mRepo; - if ( !$repo instanceof LocalRepo ) { - $this->dieUsage( 'Local file repository does not support querying all images', 'unsupportedrepo' ); - } - - $db = $this->getDB(); - - $params = $this->extractRequestParams(); - - // Image filters - $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); - $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); - $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); - $this->addWhereRange( 'img_name', $dir, $from, $to ); - - if ( isset( $params['prefix'] ) ) - $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); - - if ( isset( $params['minsize'] ) ) { - $this->addWhere( 'img_size>=' . intval( $params['minsize'] ) ); - } - - if ( isset( $params['maxsize'] ) ) { - $this->addWhere( 'img_size<=' . intval( $params['maxsize'] ) ); - } - - $sha1 = false; - if ( isset( $params['sha1'] ) ) { - if ( !$this->validateSha1Hash( $params['sha1'] ) ) { - $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); - } - $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 ); - } elseif ( isset( $params['sha1base36'] ) ) { - $sha1 = $params['sha1base36']; - if ( !$this->validateSha1Base36Hash( $sha1 ) ) { - $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); - } - } - if ( $sha1 ) { - $this->addWhereFld( 'img_sha1', $sha1 ); - } - - if ( !is_null( $params['mime'] ) ) { - global $wgMiserMode; - if ( $wgMiserMode ) { - $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' ); - } - - list( $major, $minor ) = File::splitMime( $params['mime'] ); - - $this->addWhereFld( 'img_major_mime', $major ); - $this->addWhereFld( 'img_minor_mime', $minor ); - } - - $this->addTables( 'image' ); - - $prop = array_flip( $params['prop'] ); - $this->addFields( LocalFile::selectFields() ); - - $limit = $params['limit']; - $this->addOption( 'LIMIT', $limit + 1 ); - $this->addOption( 'ORDER BY', 'img_name' . - ( $params['dir'] == 'descending' ? ' DESC' : '' ) ); - - $res = $this->select( __METHOD__ ); - - $titles = array(); - $count = 0; - $result = $this->getResult(); - foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) ); - break; - } - - if ( is_null( $resultPageSet ) ) { - $file = $repo->newFileFromRow( $row ); - $info = array_merge( array( 'name' => $row->img_name ), - ApiQueryImageInfo::getInfo( $file, $prop, $result ) ); - self::addTitleInfo( $info, $file->getTitle() ); - - $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info ); - if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) ); - break; - } - } else { - $titles[] = Title::makeTitle( NS_IMAGE, $row->img_name ); - } - } - - if ( is_null( $resultPageSet ) ) { - $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' ); - } else { - $resultPageSet->populateFromTitles( $titles ); - } - } - - public function getAllowedParams() { - return array ( - 'from' => null, - 'to' => null, - 'prefix' => null, - 'minsize' => array( - ApiBase::PARAM_TYPE => 'integer', - ), - 'maxsize' => array( - ApiBase::PARAM_TYPE => 'integer', - ), - 'limit' => array( - ApiBase::PARAM_DFLT => 10, - ApiBase::PARAM_TYPE => 'limit', - ApiBase::PARAM_MIN => 1, - ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, - ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ), - 'dir' => array( - ApiBase::PARAM_DFLT => 'ascending', - ApiBase::PARAM_TYPE => array( - 'ascending', - 'descending' - ) - ), - 'sha1' => null, - 'sha1base36' => null, - 'prop' => array( - ApiBase::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( $this->propertyFilter ), - ApiBase::PARAM_DFLT => 'timestamp|url', - ApiBase::PARAM_ISMULTI => true - ), - 'mime' => null, - ); - } - - public function getParamDescription() { - return array( - 'from' => 'The image title to start enumerating from', - 'to' => 'The image title to stop enumerating at', - 'prefix' => 'Search for all image titles that begin with this value', - 'dir' => 'The direction in which to list', - 'minsize' => 'Limit to images with at least this many bytes', - 'maxsize' => 'Limit to images with at most this many bytes', - 'limit' => 'How many images in total to return', - 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36", - 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)', - 'prop' => ApiQueryImageInfo::getPropertyDescriptions( $this->propertyFilter ), - 'mime' => 'What MIME type to search for. e.g. image/jpeg. Disabled in Miser Mode', - ); - } - - private $propertyFilter = array( 'archivename' ); - - public function getDescription() { - return 'Enumerate all images sequentially'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'params', 'info' => 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator' ), - array( 'code' => 'unsupportedrepo', 'info' => 'Local file repository does not support querying all images' ), - array( 'code' => 'mimesearchdisabled', 'info' => 'MIME search disabled in Miser Mode' ), - array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ), - array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ), - ) ); - } - - public function getExamples() { - return array( - 'api.php?action=query&list=allimages&aifrom=B' => array( - 'Simple Use', - 'Show a list of images starting at the letter "B"', - ), - 'api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo' => array( - 'Using as Generator', - 'Show info about 4 images starting at the letter "T"', - ), - ); - } - - public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allimages'; - } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } -} diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 381ef550..06db87bf 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -4,7 +4,7 @@ * * Created on Oct 16, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -40,7 +40,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { private $rootTitle; private $params, $contID, $redirID, $redirect; - private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_sort, $bl_fields, $hasNS; + private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS; /** * Maps ns and title to pageid @@ -91,14 +91,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->hasNS = $moduleName !== 'imageusage'; if ( $this->hasNS ) { $this->bl_title = $prefix . '_title'; - $this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}"; $this->bl_fields = array( $this->bl_ns, $this->bl_title ); } else { $this->bl_title = $prefix . '_to'; - $this->bl_sort = "{$this->bl_title}, {$this->bl_from}"; $this->bl_fields = array( $this->bl_title ); @@ -144,7 +142,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->addWhereFld( 'page_namespace', $this->params['namespace'] ); if ( !is_null( $this->contID ) ) { - $this->addWhere( "{$this->bl_from}>={$this->contID}" ); + $op = $this->params['dir'] == 'descending' ? '<' : '>'; + $this->addWhere( "{$this->bl_from}$op={$this->contID}" ); } if ( $this->params['filterredir'] == 'redirects' ) { @@ -155,7 +154,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); - $this->addOption( 'ORDER BY', $this->bl_from ); + $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' ); + $this->addOption( 'ORDER BY', $this->bl_from . $sort ); $this->addOption( 'STRAIGHT_JOIN' ); } @@ -186,28 +186,35 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { // We can't use LinkBatch here because $this->hasNS may be false $titleWhere = array(); + $allRedirNs = array(); + $allRedirDBkey = array(); foreach ( $this->redirTitles as $t ) { - $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $t->getDBkey() ) . - ( $this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : '' ); + $redirNs = $t->getNamespace(); + $redirDBkey = $t->getDBkey(); + $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) . + ( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' ); + $allRedirNs[] = $redirNs; + $allRedirDBkey[] = $redirDBkey; } $this->addWhere( $db->makeList( $titleWhere, LIST_OR ) ); $this->addWhereFld( 'page_namespace', $this->params['namespace'] ); if ( !is_null( $this->redirID ) ) { + $op = $this->params['dir'] == 'descending' ? '<' : '>'; $first = $this->redirTitles[0]; - $title = $db->strencode( $first->getDBkey() ); + $title = $db->addQuotes( $first->getDBkey() ); $ns = $first->getNamespace(); $from = $this->redirID; if ( $this->hasNS ) { - $this->addWhere( "{$this->bl_ns} > $ns OR " . + $this->addWhere( "{$this->bl_ns} $op $ns OR " . "({$this->bl_ns} = $ns AND " . - "({$this->bl_title} > '$title' OR " . - "({$this->bl_title} = '$title' AND " . - "{$this->bl_from} >= $from)))" ); + "({$this->bl_title} $op $title OR " . + "({$this->bl_title} = $title AND " . + "{$this->bl_from} $op= $from)))" ); } else { - $this->addWhere( "{$this->bl_title} > '$title' OR " . - "({$this->bl_title} = '$title' AND " . - "{$this->bl_from} >= $from)" ); + $this->addWhere( "{$this->bl_title} $op $title OR " . + "({$this->bl_title} = $title AND " . + "{$this->bl_from} $op= $from)" ); } } if ( $this->params['filterredir'] == 'redirects' ) { @@ -217,7 +224,17 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); - $this->addOption( 'ORDER BY', $this->bl_sort ); + $orderBy = array(); + $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' ); + // Don't order by namespace/title if it's constant in the WHERE clause + if( $this->hasNS && count( array_unique( $allRedirNs ) ) != 1 ) { + $orderBy[] = $this->bl_ns . $sort; + } + if( count( array_unique( $allRedirDBkey ) ) != 1 ) { + $orderBy[] = $this->bl_title . $sort; + } + $orderBy[] = $this->bl_from . $sort; + $this->addOption( 'ORDER BY', $orderBy ); $this->addOption( 'USE INDEX', array( 'page' => 'PRIMARY' ) ); } @@ -277,7 +294,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( $this->hasNS ) { $parentID = $this->pageMap[$row-> { $this->bl_ns } ][$row-> { $this->bl_title } ]; } else { - $parentID = $this->pageMap[NS_IMAGE][$row-> { $this->bl_title } ]; + $parentID = $this->pageMap[NS_FILE][$row-> { $this->bl_title } ]; } $this->continueStr = $this->getContinueRedirStr( $parentID, $row->page_id ); break; @@ -369,14 +386,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( !is_null( $this->params['continue'] ) ) { $this->parseContinueParam(); } else { - if ( $this->params['title'] !== '' ) { - $title = Title::newFromText( $this->params['title'] ); - if ( !$title ) { - $this->dieUsageMsg( array( 'invalidtitle', $this->params['title'] ) ); - } else { - $this->rootTitle = $title; - } - } + $this->rootTitle = $this->getTitleOrPageId( $this->params )->getTitle(); } // only image titles are allowed for the root in imageinfo mode @@ -436,13 +446,22 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $retval = array( 'title' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true + ), + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer', ), 'continue' => null, 'namespace' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => 'namespace' ), + 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
+ ), 'filterredir' => array( ApiBase::PARAM_DFLT => 'all', ApiBase::PARAM_TYPE => array( @@ -468,9 +487,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { public function getParamDescription() { $retval = array( - 'title' => 'Title to search', + 'title' => "Title to search. Cannot be used together with {$this->bl_code}pageid", + 'pageid' => "Pageid to search. Cannot be used together with {$this->bl_code}title", 'continue' => 'When more results are available, use this to continue', 'namespace' => 'The namespace to enumerate', + 'dir' => 'The direction in which to list', ); if ( $this->getModuleName() != 'embeddedin' ) { return array_merge( $retval, array( @@ -485,6 +506,17 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ) ); } + public function getResultProperties() { + return array( + '' => array( + 'pageid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string', + 'redirect' => 'boolean' + ) + ); + } + public function getDescription() { switch ( $this->getModuleName() ) { case 'backlinks': @@ -499,11 +531,13 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'invalidtitle', 'title' ), - array( 'code' => 'bad_image_title', 'info' => "The title for {$this->getModuleName()} query must be an image" ), - array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), - ) ); + return array_merge( parent::getPossibleErrors(), + $this->getTitleOrPageIdErrorMessage(), + array( + array( 'code' => 'bad_image_title', 'info' => "The title for {$this->getModuleName()} query must be an image" ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), + ) + ); } public function getExamples() { diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 4fe82de0..2c48aca0 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -233,7 +233,7 @@ abstract class ApiQueryBase extends ApiBase { */ protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) { $db = $this->getDb(); - return $this->addWhereRange( $field, $dir, + $this->addWhereRange( $field, $dir, $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort ); } @@ -392,7 +392,7 @@ abstract class ApiQueryBase extends ApiBase { * @param $name string Name to assign to the database connection * @param $db int One of the DB_* constants * @param $groups array Query groups - * @return Database + * @return DatabaseBase */ public function selectNamedDB( $name, $db, $groups ) { $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups ); @@ -519,7 +519,7 @@ abstract class ApiQueryBase extends ApiBase { $this->addFields( 'ipb_deleted' ); if ( $showBlockInfo ) { - $this->addFields( array( 'ipb_reason', 'ipb_by_text', 'ipb_expiry' ) ); + $this->addFields( array( 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_reason', 'ipb_expiry' ) ); } // Don't show hidden names @@ -571,6 +571,11 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { private $mIsGenerator; + /** + * @param $query ApiBase + * @param $moduleName string + * @param $paramPrefix string + */ public function __construct( $query, $moduleName, $paramPrefix = '' ) { parent::__construct( $query, $moduleName, $paramPrefix ); $this->mIsGenerator = false; diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index bebb5a7d..96b86962 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -4,7 +4,7 @@ * * Created on Sep 10, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -77,6 +77,9 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addOption( 'LIMIT', $params['limit'] + 1 ); $this->addTimestampWhereRange( 'ipb_timestamp', $params['dir'], $params['start'], $params['end'] ); + + $db = $this->getDB(); + if ( isset( $params['ids'] ) ) { $this->addWhereFld( 'ipb_id', $params['ids'] ); } @@ -87,7 +90,6 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addWhereFld( 'ipb_address', $this->usernames ); $this->addWhereFld( 'ipb_auto', 0 ); } - $db = $this->getDB(); if ( isset( $params['ip'] ) ) { list( $ip, $range ) = IP::parseCIDR( $params['ip'] ); if ( $ip && $range ) { @@ -101,10 +103,15 @@ class ApiQueryBlocks extends ApiQueryBase { } $prefix = substr( $lower, 0, 4 ); + # Fairly hard to make a malicious SQL statement out of hex characters, + # but it is good practice to add quotes + $lower = $db->addQuotes( $lower ); + $upper = $db->addQuotes( $upper ); + $this->addWhere( array( 'ipb_range_start' . $db->buildLike( $prefix, $db->anyString() ), - "ipb_range_start <= '$lower'", - "ipb_range_end >= '$upper'", + 'ipb_range_start <= ' . $lower, + 'ipb_range_end >= ' . $upper, 'ipb_auto' => 0 ) ); } @@ -292,8 +299,8 @@ class ApiQueryBlocks extends ApiQueryBase { 'start' => 'The timestamp to start enumerating from', 'end' => 'The timestamp to stop enumerating at', 'dir' => $this->getDirectionDescription( $p ), - 'ids' => 'Pipe-separated list of block IDs to list (optional)', - 'users' => 'Pipe-separated list of users to search for (optional)', + 'ids' => 'List of block IDs to list (optional)', + 'users' => 'List of users to search for (optional)', 'ip' => array( 'Get all blocks applying to this IP or CIDR range, including range blocks.', 'Cannot be used together with bkusers. CIDR ranges broader than /16 are not accepted' ), 'limit' => 'The maximum amount of blocks to list', @@ -317,18 +324,74 @@ class ApiQueryBlocks extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + 'id' => array( + 'id' => 'integer' + ), + 'user' => array( + 'user' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'userid' => array( + 'userid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'by' => array( + 'by' => 'string' + ), + 'byid' => array( + 'byid' => 'integer' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'expiry' => array( + 'expiry' => 'timestamp' + ), + 'reason' => array( + 'reason' => 'string' + ), + 'range' => array( + 'rangestart' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'rangeend' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'flags' => array( + 'automatic' => 'boolean', + 'anononly' => 'boolean', + 'nocreate' => 'boolean', + 'autoblock' => 'boolean', + 'noemail' => 'boolean', + 'hidden' => 'boolean', + 'allowusertalk' => 'boolean' + ) + ); + } + public function getDescription() { return 'List all blocked users and IP addresses'; } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( + return array_merge( parent::getPossibleErrors(), $this->getRequireOnlyOneParameterErrorMessages( array( 'users', 'ip' ) ), - array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ), - array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ), - array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), - array( 'show' ), - ) ); + array( + array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ), + array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ), + array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), + array( 'show' ), + ) + ); } public function getExamples() { diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 1c1f1550..309c2ce9 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -89,12 +89,13 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->dieUsage( "Invalid continue param. You should pass the " . "original value returned by the previous query", "_badcontinue" ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $clfrom = intval( $cont[0] ); - $clto = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $clto = $this->getDB()->addQuotes( $cont[1] ); $this->addWhere( - "cl_from > $clfrom OR " . + "cl_from $op $clfrom OR " . "(cl_from = $clfrom AND " . - "cl_to >= '$clto')" + "cl_to $op= $clto)" ); } @@ -123,14 +124,14 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->addOption( 'USE INDEX', array( 'categorylinks' => 'cl_from' ) ); - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Don't order by cl_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'cl_to' . $dir ); + $this->addOption( 'ORDER BY', 'cl_to' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'cl_from' . $dir, - 'cl_to' . $dir + 'cl_from' . $sort, + 'cl_to' . $sort )); } @@ -142,8 +143,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->cl_from . - '|' . $this->keyToTitle( $row->cl_to ) ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); break; } @@ -163,8 +163,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $fit = $this->addPageSubItem( $row->cl_from, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->cl_from . - '|' . $this->keyToTitle( $row->cl_to ) ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); break; } } @@ -174,8 +173,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->cl_from . - '|' . $this->keyToTitle( $row->cl_to ) ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); break; } @@ -239,6 +237,25 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'sortkey' => array( + 'sortkey' => 'string', + 'sortkeyprefix' => 'string' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'hidden' => array( + 'hidden' => 'boolean' + ) + ); + } + public function getDescription() { return 'List all categories the page(s) belong to'; } diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index c5070e87..31517fab 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -25,7 +25,8 @@ */ /** - * This query adds the <categories> subelement to all pages with the list of categories the page is in + * This query adds the "<categories>" subelement to all pages with the list of + * categories the page is in. * * @ingroup API */ @@ -61,7 +62,7 @@ class ApiQueryCategoryInfo extends ApiQueryBase { 'pp_propname' => 'hiddencat' ) ), ) ); - $this->addFields( array( 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden' ) ); + $this->addFields( array( 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'cat_hidden' => 'pp_propname' ) ); $this->addWhere( array( 'cat_title' => $cattitles ) ); if ( !is_null( $params['continue'] ) ) { @@ -106,6 +107,34 @@ class ApiQueryCategoryInfo extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + ApiBase::PROP_LIST => false, + '' => array( + 'size' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => false + ), + 'pages' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => false + ), + 'files' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => false + ), + 'subcats' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => false + ), + 'hidden' => array( + ApiBase::PROP_TYPE => 'boolean', + ApiBase::PROP_NULLABLE => false + ) + ) + ); + } + public function getDescription() { return 'Returns information about the given categories'; } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 4b19b7e8..55ce0234 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -4,7 +4,7 @@ * * Created on June 14, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -54,22 +54,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { private function run( $resultPageSet = null ) { $params = $this->extractRequestParams(); - $this->requireOnlyOneParameter( $params, 'title', 'pageid' ); - - if ( isset( $params['title'] ) ) { - $categoryTitle = Title::newFromText( $params['title'] ); - - if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY ) { - $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); - } - } elseif( isset( $params['pageid'] ) ) { - $categoryTitle = Title::newFromID( $params['pageid'] ); - - if ( !$categoryTitle ) { - $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) ); - } elseif ( $categoryTitle->getNamespace() != NS_CATEGORY ) { - $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); - } + $categoryTitle = $this->getTitleOrPageId( $params )->getTitle(); + if ( $categoryTitle->getNamespace() != NS_CATEGORY ) { + $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); } $prop = array_flip( $params['prop'] ); @@ -107,10 +94,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addWhereFld( 'page_namespace', $params['namespace'] ); } - $dir = $params['dir'] == 'asc' ? 'newer' : 'older'; + $dir = in_array( $params['dir'], array( 'asc', 'ascending', 'newer' ) ) ? 'newer' : 'older'; if ( $params['sort'] == 'timestamp' ) { - $this->addWhereRange( 'cl_timestamp', + $this->addTimestampWhereRange( 'cl_timestamp', $dir, $params['start'], $params['end'] ); @@ -313,10 +300,15 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { ) ), 'dir' => array( - ApiBase::PARAM_DFLT => 'asc', + ApiBase::PARAM_DFLT => 'ascending', ApiBase::PARAM_TYPE => array( 'asc', - 'desc' + 'desc', + // Normalising with other modules + 'ascending', + 'descending', + 'newer', + 'older', ) ), 'start' => array( @@ -357,7 +349,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'endsortkey' => "Sortkey to end listing at. Must be given in binary format. Can only be used with {$p}sort=sortkey", 'startsortkeyprefix' => "Sortkey prefix to start listing from. Can only be used with {$p}sort=sortkey. Overrides {$p}startsortkey", 'endsortkeyprefix' => "Sortkey prefix to end listing BEFORE (not at, if this value occurs it will not be included!). Can only be used with {$p}sort=sortkey. Overrides {$p}endsortkey", - 'continue' => 'For large categories, give the value retured from previous query', + 'continue' => 'For large categories, give the value returned from previous query', 'limit' => 'The maximum number of pages to return.', ); @@ -372,17 +364,46 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { return $desc; } + public function getResultProperties() { + return array( + 'ids' => array( + 'pageid' => 'integer' + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'sortkey' => array( + 'sortkey' => 'string' + ), + 'sortkeyprefix' => array( + 'sortkeyprefix' => 'string' + ), + 'type' => array( + 'type' => array( + ApiBase::PROP_TYPE => array( + 'page', + 'subcat', + 'file' + ) + ) + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ) + ); + } + public function getDescription() { return 'List all pages in a given category'; } public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), + $this->getTitleOrPageIdErrorMessage(), array( array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ), array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), - array( 'nosuchpageid', 'pageid' ), ) ); } diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 0a0cc93d..e69ccbd6 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -4,7 +4,7 @@ * * Created on Jul 2, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -155,7 +155,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addWhereFld( 'ar_user_text', $params['user'] ); } elseif ( !is_null( $params['excludeuser'] ) ) { $this->addWhere( 'ar_user_text != ' . - $this->getDB()->addQuotes( $params['excludeuser'] ) ); + $db->addQuotes( $params['excludeuser'] ) ); } if ( !is_null( $params['continue'] ) && ( $mode == 'all' || $mode == 'revs' ) ) { @@ -164,14 +164,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', 'badcontinue' ); } $ns = intval( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); - $ts = $this->getDB()->strencode( $cont[2] ); + $title = $db->addQuotes( $cont[1] ); + $ts = $db->addQuotes( $db->timestamp( $cont[2] ) ); $op = ( $dir == 'newer' ? '>' : '<' ); $this->addWhere( "ar_namespace $op $ns OR " . "(ar_namespace = $ns AND " . - "(ar_title $op '$title' OR " . - "(ar_title = '$title' AND " . - "ar_timestamp $op= '$ts')))" ); + "(ar_title $op $title OR " . + "(ar_title = $title AND " . + "ar_timestamp $op= $ts)))" ); } $this->addOption( 'LIMIT', $limit + 1 ); @@ -180,7 +180,11 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( $params['unique'] ) { $this->addOption( 'GROUP BY', 'ar_title' ); } else { - $this->addOption( 'ORDER BY', 'ar_title, ar_timestamp' ); + $sort = ( $dir == 'newer' ? '' : ' DESC' ); + $this->addOption( 'ORDER BY', array( + 'ar_title' . $sort, + 'ar_timestamp' . $sort + )); } } else { if ( $mode == 'revs' ) { @@ -199,7 +203,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { // We've had enough if ( $mode == 'all' || $mode == 'revs' ) { $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' . - $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp ); + $row->ar_title . '|' . $row->ar_timestamp ); } else { $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) ); } @@ -265,7 +269,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( !$fit ) { if ( $mode == 'all' || $mode == 'revs' ) { $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' . - $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp ); + $row->ar_title . '|' . $row->ar_timestamp ); } else { $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) ); } @@ -334,8 +338,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase { public function getParamDescription() { return array( - 'start' => 'The timestamp to start enumerating from (1,2)', - 'end' => 'The timestamp to stop enumerating at (1,2)', + 'start' => 'The timestamp to start enumerating from (1, 2)', + 'end' => 'The timestamp to stop enumerating at (1, 2)', 'dir' => $this->getDirectionDescription( $this->getModulePrefix(), ' (1, 3)' ), 'from' => 'Start listing at this title (3)', 'to' => 'Stop listing at this title (3)', @@ -363,6 +367,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'token' => array( + 'token' => 'string' + ) + ); + } + public function getDescription() { $p = $this->getModulePrefix(); return array( diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php index d68480c3..6715969a 100644 --- a/includes/api/ApiQueryDisabled.php +++ b/includes/api/ApiQueryDisabled.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php index beca5879..8f0fd3be 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -4,7 +4,7 @@ * * Created on Sep 27, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -59,67 +59,99 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } $images = $namespaces[NS_FILE]; - $this->addTables( 'image', 'i1' ); - $this->addTables( 'image', 'i2' ); - $this->addFields( array( - 'i1.img_name AS orig_name', - 'i2.img_name AS dup_name', - 'i2.img_user_text AS dup_user_text', - 'i2.img_timestamp AS dup_timestamp' - ) ); - - $this->addWhere( array( - 'i1.img_name' => array_keys( $images ), - 'i1.img_sha1 = i2.img_sha1', - 'i1.img_name != i2.img_name', - ) ); + if( $params['dir'] == 'descending' ) { + $images = array_reverse( $images ); + } + $skipUntilThisDup = false; if ( isset( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); if ( count( $cont ) != 2 ) { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } - $orig = $this->getDB()->strencode( $this->titleTokey( $cont[0] ) ); - $dup = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); - $this->addWhere( - "i1.img_name > '$orig' OR " . - "(i1.img_name = '$orig' AND " . - "i2.img_name >= '$dup')" - ); + $fromImage = $cont[0]; + $skipUntilThisDup = $cont[1]; + // Filter out any images before $fromImage + foreach ( $images as $image => $pageId ) { + if ( $image < $fromImage ) { + unset( $images[$image] ); + } else { + break; + } + } } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); - $this->addOption( 'ORDER BY', 'i1.img_name' . $dir ); - $this->addOption( 'LIMIT', $params['limit'] + 1 ); + $filesToFind = array_keys( $images ); + if( $params['localonly'] ) { + $files = RepoGroup::singleton()->getLocalRepo()->findFiles( $filesToFind ); + } else { + $files = RepoGroup::singleton()->findFiles( $filesToFind ); + } - $res = $this->select( __METHOD__ ); + $fit = true; $count = 0; $titles = array(); - foreach ( $res as $row ) { - if ( ++$count > $params['limit'] ) { - // We've reached the one extra which shows that - // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', - $this->keyToTitle( $row->orig_name ) . '|' . - $this->keyToTitle( $row->dup_name ) ); - break; + + $sha1s = array(); + foreach ( $files as $file ) { + $sha1s[$file->getName()] = $file->getSha1(); + } + + // find all files with the hashes, result format is: array( hash => array( dup1, dup2 ), hash1 => ... ) + $filesToFindBySha1s = array_unique( array_values( $sha1s ) ); + if( $params['localonly'] ) { + $filesBySha1s = RepoGroup::singleton()->getLocalRepo()->findBySha1s( $filesToFindBySha1s ); + } else { + $filesBySha1s = RepoGroup::singleton()->findBySha1s( $filesToFindBySha1s ); + } + + // iterate over $images to handle continue param correct + foreach( $images as $image => $pageId ) { + if( !isset( $sha1s[$image] ) ) { + continue; //file does not exist + } + $sha1 = $sha1s[$image]; + $dupFiles = $filesBySha1s[$sha1]; + if( $params['dir'] == 'descending' ) { + $dupFiles = array_reverse( $dupFiles ); } - if ( !is_null( $resultPageSet ) ) { - $titles[] = Title::makeTitle( NS_FILE, $row->dup_name ); - } else { - $r = array( - 'name' => $row->dup_name, - 'user' => $row->dup_user_text, - 'timestamp' => wfTimestamp( TS_ISO_8601, $row->dup_timestamp ) - ); - $fit = $this->addPageSubItem( $images[$row->orig_name], $r ); - if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', - $this->keyToTitle( $row->orig_name ) . '|' . - $this->keyToTitle( $row->dup_name ) ); + foreach ( $dupFiles as $dupFile ) { + $dupName = $dupFile->getName(); + if( $image == $dupName && $dupFile->isLocal() ) { + continue; //ignore the local file itself + } + if( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) { + continue; //skip to pos after the image from continue param + } + $skipUntilThisDup = false; + if ( ++$count > $params['limit'] ) { + $fit = false; //break outer loop + // We're one over limit which shows that + // there are additional images to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName ); break; } + if ( !is_null( $resultPageSet ) ) { + $titles[] = $file->getTitle(); + } else { + $r = array( + 'name' => $dupName, + 'user' => $dupFile->getUser( 'text' ), + 'timestamp' => wfTimestamp( TS_ISO_8601, $dupFile->getTimestamp() ) + ); + if( !$dupFile->isLocal() ) { + $r['shared'] = ''; + } + $fit = $this->addPageSubItem( $pageId, $r ); + if ( !$fit ) { + $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName ); + break; + } + } + } + if( !$fit ) { + break; } } if ( !is_null( $resultPageSet ) ) { @@ -144,19 +176,32 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { 'descending' ) ), + 'localonly' => false, ); } public function getParamDescription() { return array( - 'limit' => 'How many files to return', + 'limit' => 'How many duplicate files to return', 'continue' => 'When more results are available, use this to continue', 'dir' => 'The direction in which to list', + 'localonly' => 'Look only for files in the local repository', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'name' => 'string', + 'user' => 'string', + 'timestamp' => 'timestamp', + 'shared' => 'boolean', + ) ); } public function getDescription() { - return 'List all files that are duplicates of the given file(s)'; + return 'List all files that are duplicates of the given file(s) based on hash values'; } public function getPossibleErrors() { diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 93c71e2f..42b398ba 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -4,7 +4,7 @@ * * Created on July 7, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -232,6 +232,21 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { return $desc; } + public function getResultProperties() { + return array( + 'ids' => array( + 'pageid' => 'integer' + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'url' => array( + 'url' => 'string' + ) + ); + } + public function getDescription() { return 'Enumerate pages that contain a given URL'; } diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index a9fbc839..9365a9b8 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -133,6 +133,14 @@ class ApiQueryExternalLinks extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + '*' => 'string' + ) + ); + } + public function getDescription() { return 'Returns all external urls (not interwikies) from the given page(s)'; } diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index be995f30..a5486ef4 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -6,7 +6,7 @@ * * Copyright © 2010 Sam Reed * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com, - * based on ApiQueryAllpages.php + * based on ApiQueryAllPages.php * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -56,8 +56,10 @@ class ApiQueryFilearchive extends ApiQueryBase { $fld_dimensions = isset( $prop['dimensions'] ); $fld_description = isset( $prop['description'] ) || isset( $prop['parseddescription'] ); $fld_mime = isset( $prop['mime'] ); + $fld_mediatype = isset( $prop['mediatype'] ); $fld_metadata = isset( $prop['metadata'] ); $fld_bitdepth = isset( $prop['bitdepth'] ); + $fld_archivename = isset( $prop['archivename'] ); $this->addTables( 'filearchive' ); @@ -68,12 +70,28 @@ class ApiQueryFilearchive extends ApiQueryBase { $this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size ); $this->addFieldsIf( 'fa_description', $fld_description ); $this->addFieldsIf( array( 'fa_major_mime', 'fa_minor_mime' ), $fld_mime ); + $this->addFieldsIf( 'fa_media_type', $fld_mediatype ); $this->addFieldsIf( 'fa_metadata', $fld_metadata ); $this->addFieldsIf( 'fa_bits', $fld_bitdepth ); + $this->addFieldsIf( 'fa_archive_name', $fld_archivename ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( "Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue" ); + } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "fa_name $op= $cont_from" ); + } // Image filters $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); + if ( !is_null( $params['continue'] ) ) { + $from = $params['continue']; + } $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); $this->addWhereRange( 'fa_name', $dir, $from, $to ); if ( isset( $params['prefix'] ) ) { @@ -117,8 +135,8 @@ class ApiQueryFilearchive extends ApiQueryBase { $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); - $this->addOption( 'ORDER BY', 'fa_name' . - ( $params['dir'] == 'descending' ? ' DESC' : '' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $this->addOption( 'ORDER BY', 'fa_name' . $sort ); $res = $this->select( __METHOD__ ); @@ -127,8 +145,7 @@ class ApiQueryFilearchive extends ApiQueryBase { foreach ( $res as $row ) { if ( ++$count > $limit ) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) ); + $this->setContinueEnumParameter( 'continue', $row->fa_name ); break; } @@ -165,6 +182,9 @@ class ApiQueryFilearchive extends ApiQueryBase { $row->fa_description, $title ); } } + if ( $fld_mediatype ) { + $file['mediatype'] = $row->fa_media_type; + } if ( $fld_metadata ) { $file['metadata'] = $row->fa_metadata ? ApiQueryImageInfo::processMetaData( unserialize( $row->fa_metadata ), $result ) @@ -176,6 +196,9 @@ class ApiQueryFilearchive extends ApiQueryBase { if ( $fld_mime ) { $file['mime'] = "$row->fa_major_mime/$row->fa_minor_mime"; } + if ( $fld_archivename && !is_null( $row->fa_archive_name ) ) { + $file['archivename'] = $row->fa_archive_name; + } if ( $row->fa_deleted & File::DELETED_FILE ) { $file['filehidden'] = ''; @@ -194,7 +217,7 @@ class ApiQueryFilearchive extends ApiQueryBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) ); + $this->setContinueEnumParameter( 'continue', $row->fa_name ); break; } } @@ -205,6 +228,7 @@ class ApiQueryFilearchive extends ApiQueryBase { public function getAllowedParams() { return array ( 'from' => null, + 'continue' => null, 'to' => null, 'prefix' => null, 'limit' => array( @@ -235,8 +259,10 @@ class ApiQueryFilearchive extends ApiQueryBase { 'description', 'parseddescription', 'mime', + 'mediatype', 'metadata', - 'bitdepth' + 'bitdepth', + 'archivename', ), ), ); @@ -245,6 +271,7 @@ class ApiQueryFilearchive extends ApiQueryBase { public function getParamDescription() { return array( 'from' => 'The image title to start enumerating from', + 'continue' => 'When more results are available, use this to continue', 'to' => 'The image title to stop enumerating at', 'prefix' => 'Search for all image titles that begin with this value', 'dir' => 'The direction in which to list', @@ -261,9 +288,75 @@ class ApiQueryFilearchive extends ApiQueryBase { ' description - Adds description the image version', ' parseddescription - Parse the description on the version', ' mime - Adds MIME of the image', + ' mediatype - Adds the media type of the image', ' metadata - Lists EXIF metadata for the version of the image', ' bitdepth - Adds the bit depth of the version', - ), + ' archivename - Adds the file name of the archive version for non-latest versions' + ), + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'name' => 'string', + 'ns' => 'namespace', + 'title' => 'string', + 'filehidden' => 'boolean', + 'commenthidden' => 'boolean', + 'userhidden' => 'boolean', + 'suppressed' => 'boolean' + ), + 'sha1' => array( + 'sha1' => 'string' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'user' => array( + 'userid' => 'integer', + 'user' => 'string' + ), + 'size' => array( + 'size' => 'integer', + 'pagecount' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'height' => 'integer', + 'width' => 'integer' + ), + 'dimensions' => array( + 'size' => 'integer', + 'pagecount' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'height' => 'integer', + 'width' => 'integer' + ), + 'description' => array( + 'description' => 'string' + ), + 'parseddescription' => array( + 'description' => 'string', + 'parseddescription' => 'string' + ), + 'metadata' => array( + 'metadata' => 'string' + ), + 'bitdepth' => array( + 'bitdepth' => 'integer' + ), + 'mime' => array( + 'mime' => 'string' + ), + 'mediatype' => array( + 'mediatype' => 'string' + ), + 'archivename' => array( + 'archivename' => 'string' + ), ); } @@ -277,6 +370,7 @@ class ApiQueryFilearchive extends ApiQueryBase { array( 'code' => 'hashsearchdisabled', 'info' => 'Search by hash disabled in Miser Mode' ), array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ), array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index feda1779..c5012f08 100644 --- a/includes/api/ApiQueryIWBacklinks.php +++ b/includes/api/ApiQueryIWBacklinks.php @@ -5,7 +5,7 @@ * Created on May 14, 2010 * * Copyright © 2010 Sam Reed - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -61,15 +61,17 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { 'original value returned by the previous query', '_badcontinue' ); } - $prefix = $this->getDB()->strencode( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $db = $this->getDB(); + $op = $params['dir'] == 'descending' ? '<' : '>'; + $prefix = $db->addQuotes( $cont[0] ); + $title = $db->addQuotes( $cont[1] ); $from = intval( $cont[2] ); $this->addWhere( - "iwl_prefix > '$prefix' OR " . - "(iwl_prefix = '$prefix' AND " . - "(iwl_title > '$title' OR " . - "(iwl_title = '$title' AND " . - "iwl_from >= $from)))" + "iwl_prefix $op $prefix OR " . + "(iwl_prefix = $prefix AND " . + "(iwl_title $op $title OR " . + "(iwl_title = $title AND " . + "iwl_from $op= $from)))" ); } @@ -83,16 +85,24 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { $this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect', 'iwl_from', 'iwl_prefix', 'iwl_title' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); if ( isset( $params['prefix'] ) ) { $this->addWhereFld( 'iwl_prefix', $params['prefix'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'iwl_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'iwl_from' ); + $this->addOption( 'ORDER BY', 'iwl_from' . $sort ); } else { - $this->addOption( 'ORDER BY', 'iwl_title, iwl_from' ); + $this->addOption( 'ORDER BY', array( + 'iwl_title' . $sort, + 'iwl_from' . $sort + )); } } else { - $this->addOption( 'ORDER BY', 'iwl_prefix, iwl_title, iwl_from' ); + $this->addOption( 'ORDER BY', array( + 'iwl_prefix' . $sort, + 'iwl_title' . $sort, + 'iwl_from' . $sort + )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -170,6 +180,13 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { 'iwtitle', ), ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ) + ), ); } @@ -184,6 +201,24 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { ' iwtitle - Adds the title of the interwiki', ), 'limit' => 'How many total pages to return', + 'dir' => 'The direction in which to list', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'pageid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string', + 'redirect' => 'boolean' + ), + 'iwprefix' => array( + 'iwprefix' => 'string' + ), + 'iwtitle' => array( + 'iwtitle' => 'string' + ) ); } @@ -205,7 +240,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { public function getExamples() { return array( 'api.php?action=query&list=iwbacklinks&iwbltitle=Test&iwblprefix=wikibooks', - 'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&iwblprefix=wikibooks&prop=info' + 'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&giwblprefix=wikibooks&prop=info' ); } diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index 13256ad8..30c7f5a8 100644 --- a/includes/api/ApiQueryIWLinks.php +++ b/includes/api/ApiQueryIWLinks.php @@ -5,7 +5,7 @@ * Created on May 14, 2010 * * Copyright © 2010 Sam Reed - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -62,38 +62,40 @@ class ApiQueryIWLinks extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $db = $this->getDB(); $iwlfrom = intval( $cont[0] ); - $iwlprefix = $this->getDB()->strencode( $cont[1] ); - $iwltitle = $this->getDB()->strencode( $this->titleToKey( $cont[2] ) ); + $iwlprefix = $db->addQuotes( $cont[1] ); + $iwltitle = $db->addQuotes( $cont[2] ); $this->addWhere( - "iwl_from > $iwlfrom OR " . + "iwl_from $op $iwlfrom OR " . "(iwl_from = $iwlfrom AND " . - "(iwl_prefix > '$iwlprefix' OR " . - "(iwl_prefix = '$iwlprefix' AND " . - "iwl_title >= '$iwltitle')))" + "(iwl_prefix $op $iwlprefix OR " . + "(iwl_prefix = $iwlprefix AND " . + "iwl_title $op= $iwltitle)))" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); if ( isset( $params['prefix'] ) ) { $this->addWhereFld( 'iwl_prefix', $params['prefix'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'iwl_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'iwl_from' . $dir ); + $this->addOption( 'ORDER BY', 'iwl_from' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'iwl_title' . $dir, - 'iwl_from' . $dir + 'iwl_title' . $sort, + 'iwl_from' . $sort )); } } else { // Don't order by iwl_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'iwl_prefix' . $dir ); + $this->addOption( 'ORDER BY', 'iwl_prefix' . $sort ); } else { $this->addOption( 'ORDER BY', array ( - 'iwl_from' . $dir, - 'iwl_prefix' . $dir + 'iwl_from' . $sort, + 'iwl_prefix' . $sort )); } } @@ -165,6 +167,19 @@ class ApiQueryIWLinks extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'prefix' => 'string', + 'url' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + '*' => 'string' + ) + ); + } + public function getDescription() { return 'Returns all interwiki links from the given page(s)'; } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 03a24821..d822eed5 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -4,7 +4,7 @@ * * Created on July 6, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -73,7 +73,12 @@ class ApiQueryImageInfo extends ApiQueryBase { } $result = $this->getResult(); - $images = RepoGroup::singleton()->findFiles( $titles ); + //search only inside the local repo + if( $params['localonly'] ) { + $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $titles ); + } else { + $images = RepoGroup::singleton()->findFiles( $titles ); + } foreach ( $images as $img ) { // Skip redirects if ( $img->getOriginalTitle()->isRedirect() ) { @@ -81,14 +86,14 @@ class ApiQueryImageInfo extends ApiQueryBase { } $start = $skip ? $fromTimestamp : $params['start']; - $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ]; + $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ]; $fit = $result->addValue( array( 'query', 'pages', intval( $pageId ) ), 'imagerepository', $img->getRepoName() ); if ( !$fit ) { - if ( count( $pageIds[NS_IMAGE] ) == 1 ) { + if ( count( $pageIds[NS_FILE] ) == 1 ) { // The user is screwed. imageinfo can't be solely // responsible for exceeding the limit in this case, // so set a query-continue that just returns the same @@ -119,7 +124,7 @@ class ApiQueryImageInfo extends ApiQueryBase { self::getInfo( $img, $prop, $result, $finalThumbParams, $params['metadataversion'] ) ); if ( !$fit ) { - if ( count( $pageIds[NS_IMAGE] ) == 1 ) { + if ( count( $pageIds[NS_FILE] ) == 1 ) { // See the 'the user is screwed' comment above $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) ); @@ -149,7 +154,7 @@ class ApiQueryImageInfo extends ApiQueryBase { self::getInfo( $oldie, $prop, $result, $finalThumbParams, $params['metadataversion'] ) ); if ( !$fit ) { - if ( count( $pageIds[NS_IMAGE] ) == 1 ) { + if ( count( $pageIds[NS_FILE] ) == 1 ) { $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) ); } else { @@ -356,8 +361,7 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) { list( $ext, $mime ) = $file->getHandler()->getThumbType( - substr( $mto->getPath(), strrpos( $mto->getPath(), '.' ) + 1 ), - $file->getMimeType(), $thumbParams ); + $mto->getExtension(), $file->getMimeType(), $thumbParams ); $vals['thumbmime'] = $mime; } } elseif ( $mto && $mto->isError() ) { @@ -430,7 +434,7 @@ class ApiQueryImageInfo extends ApiQueryBase { * @param $img File * @return string */ - private function getContinueStr( $img ) { + protected function getContinueStr( $img ) { return $img->getOriginalTitle()->getText() . '|' . $img->getTimestamp(); } @@ -472,6 +476,7 @@ class ApiQueryImageInfo extends ApiQueryBase { ApiBase::PARAM_TYPE => 'string', ), 'continue' => null, + 'localonly' => false, ); } @@ -491,7 +496,7 @@ class ApiQueryImageInfo extends ApiQueryBase { * * @return array */ - private static function getProperties() { + private static function getProperties( $modulePrefix = '' ) { return array( 'timestamp' => ' timestamp - Adds timestamp for the uploaded version', 'user' => ' user - Adds the user who uploaded the image version', @@ -503,7 +508,8 @@ class ApiQueryImageInfo extends ApiQueryBase { 'dimensions' => ' dimensions - Alias for size', // For backwards compatibility with Allimages 'sha1' => ' sha1 - Adds SHA-1 hash for the image', 'mime' => ' mime - Adds MIME type of the image', - 'thumbmime' => ' thumbmime - Adds MIME type of the image thumbnail (requires url)', + 'thumbmime' => ' thumbmime - Adds MIME type of the image thumbnail' . + ' (requires url and param ' . $modulePrefix . 'urlwidth)', 'mediatype' => ' mediatype - Adds the media type of the image', 'metadata' => ' metadata - Lists EXIF metadata for the version of the image', 'archivename' => ' archivename - Adds the file name of the archive version for non-latest versions', @@ -518,10 +524,10 @@ class ApiQueryImageInfo extends ApiQueryBase { * * @return array */ - public static function getPropertyDescriptions( $filter = array() ) { + public static function getPropertyDescriptions( $filter = array(), $modulePrefix = '' ) { return array_merge( array( 'What image information to get:' ), - array_values( array_diff_key( self::getProperties(), array_flip( $filter ) ) ) + array_values( array_diff_key( self::getProperties( $modulePrefix ), array_flip( $filter ) ) ) ); } @@ -532,7 +538,7 @@ class ApiQueryImageInfo extends ApiQueryBase { public function getParamDescription() { $p = $this->getModulePrefix(); return array( - 'prop' => self::getPropertyDescriptions(), + 'prop' => self::getPropertyDescriptions( array(), $p ), 'urlwidth' => array( "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", 'Only the current version of the image can be scaled' ), 'urlheight' => "Similar to {$p}urlwidth. Cannot be used without {$p}urlwidth", @@ -543,10 +549,119 @@ class ApiQueryImageInfo extends ApiQueryBase { 'end' => 'Timestamp to stop listing at', 'metadataversion' => array( "Version of metadata to use. if 'latest' is specified, use latest version.", "Defaults to '1' for backwards compatibility" ), - 'continue' => 'If the query response includes a continue value, use it here to get another page of results' + 'continue' => 'If the query response includes a continue value, use it here to get another page of results', + 'localonly' => 'Look only for files in the local repository', ); } + public static function getResultPropertiesFiltered( $filter = array() ) { + $props = array( + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'user' => array( + 'userhidden' => 'boolean', + 'user' => 'string', + 'anon' => 'boolean' + ), + 'userid' => array( + 'userhidden' => 'boolean', + 'userid' => 'integer', + 'anon' => 'boolean' + ), + 'size' => array( + 'size' => 'integer', + 'width' => 'integer', + 'height' => 'integer', + 'pagecount' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'comment' => array( + 'commenthidden' => 'boolean', + 'comment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'parsedcomment' => array( + 'commenthidden' => 'boolean', + 'parsedcomment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'url' => array( + 'filehidden' => 'boolean', + 'thumburl' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'thumbwidth' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'thumbheight' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'thumberror' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'url' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'descriptionurl' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'sha1' => array( + 'filehidden' => 'boolean', + 'sha1' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'mime' => array( + 'filehidden' => 'boolean', + 'mime' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'mediatype' => array( + 'filehidden' => 'boolean', + 'mediatype' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'archivename' => array( + 'filehidden' => 'boolean', + 'archivename' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'bitdepth' => array( + 'filehidden' => 'boolean', + 'bitdepth' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + ); + return array_diff_key( $props, array_flip( $filter ) ); + } + + public function getResultProperties() { + return self::getResultPropertiesFiltered(); + } + public function getDescription() { return 'Returns image information and upload history'; } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index f03b2874..6052a75f 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -25,7 +25,8 @@ */ /** - * This query adds an <images> subelement to all pages with the list of images embedded into those pages. + * This query adds an "<images>" subelement to all pages with the list of + * images embedded into those pages. * * @ingroup API */ @@ -65,23 +66,24 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $ilfrom = intval( $cont[0] ); - $ilto = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $ilto = $this->getDB()->addQuotes( $cont[1] ); $this->addWhere( - "il_from > $ilfrom OR " . + "il_from $op $ilfrom OR " . "(il_from = $ilfrom AND " . - "il_to >= '$ilto')" + "il_to $op= $ilto)" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Don't order by il_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'il_to' . $dir ); + $this->addOption( 'ORDER BY', 'il_to' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'il_from' . $dir, - 'il_to' . $dir + 'il_from' . $sort, + 'il_to' . $sort )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -107,16 +109,14 @@ class ApiQueryImages extends ApiQueryGeneratorBase { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->il_from . - '|' . $this->keyToTitle( $row->il_to ) ); + $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to ); break; } $vals = array(); ApiQueryBase::addTitleInfo( $vals, Title::makeTitle( NS_FILE, $row->il_to ) ); $fit = $this->addPageSubItem( $row->il_from, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->il_from . - '|' . $this->keyToTitle( $row->il_to ) ); + $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to ); break; } } @@ -127,8 +127,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->il_from . - '|' . $this->keyToTitle( $row->il_to ) ); + $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to ); break; } $titles[] = Title::makeTitle( NS_FILE, $row->il_to ); @@ -173,6 +172,15 @@ class ApiQueryImages extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return 'Returns all images contained on the given page(s)'; } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index f0d0faa3..5d4f0346 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -33,7 +33,7 @@ class ApiQueryInfo extends ApiQueryBase { private $fld_protection = false, $fld_talkid = false, $fld_subjectid = false, $fld_url = false, - $fld_readable = false, $fld_watched = false, + $fld_readable = false, $fld_watched = false, $fld_notificationtimestamp = false, $fld_preload = false, $fld_displaytitle = false; private $params, $titles, $missing, $everything, $pageCounter; @@ -41,7 +41,7 @@ class ApiQueryInfo extends ApiQueryBase { private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched, $pageLatest, $pageLength; - private $protections, $watched, $talkids, $subjectids, $displaytitles; + private $protections, $watched, $notificationtimestamps, $talkids, $subjectids, $displaytitles; private $tokenFunctions; @@ -57,7 +57,10 @@ class ApiQueryInfo extends ApiQueryBase { global $wgDisableCounters; $pageSet->requestField( 'page_restrictions' ); - $pageSet->requestField( 'page_is_redirect' ); + // when resolving redirects, no page will have this field + if( !$pageSet->isResolvingRedirects() ) { + $pageSet->requestField( 'page_is_redirect' ); + } $pageSet->requestField( 'page_is_new' ); if ( !$wgDisableCounters ) { $pageSet->requestField( 'page_counter' ); @@ -99,6 +102,12 @@ class ApiQueryInfo extends ApiQueryBase { return $this->tokenFunctions; } + static $cachedTokens = array(); + + public static function resetTokenCache() { + ApiQueryInfo::$cachedTokens = array(); + } + public static function getEditToken( $pageid, $title ) { // We could check for $title->userCan('edit') here, // but that's too expensive for this purpose @@ -108,14 +117,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - // The edit token is always the same, let's exploit that - static $cachedEditToken = null; - if ( !is_null( $cachedEditToken ) ) { - return $cachedEditToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'edit' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'edit' ] = $wgUser->getEditToken(); } - $cachedEditToken = $wgUser->getEditToken(); - return $cachedEditToken; + return ApiQueryInfo::$cachedTokens[ 'edit' ]; } public static function getDeleteToken( $pageid, $title ) { @@ -124,13 +131,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedDeleteToken = null; - if ( !is_null( $cachedDeleteToken ) ) { - return $cachedDeleteToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'delete' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'delete' ] = $wgUser->getEditToken(); } - $cachedDeleteToken = $wgUser->getEditToken(); - return $cachedDeleteToken; + return ApiQueryInfo::$cachedTokens[ 'delete' ]; } public static function getProtectToken( $pageid, $title ) { @@ -139,13 +145,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedProtectToken = null; - if ( !is_null( $cachedProtectToken ) ) { - return $cachedProtectToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'protect' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'protect' ] = $wgUser->getEditToken(); } - $cachedProtectToken = $wgUser->getEditToken(); - return $cachedProtectToken; + return ApiQueryInfo::$cachedTokens[ 'protect' ]; } public static function getMoveToken( $pageid, $title ) { @@ -154,13 +159,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedMoveToken = null; - if ( !is_null( $cachedMoveToken ) ) { - return $cachedMoveToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'move' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'move' ] = $wgUser->getEditToken(); } - $cachedMoveToken = $wgUser->getEditToken(); - return $cachedMoveToken; + return ApiQueryInfo::$cachedTokens[ 'move' ]; } public static function getBlockToken( $pageid, $title ) { @@ -169,13 +173,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedBlockToken = null; - if ( !is_null( $cachedBlockToken ) ) { - return $cachedBlockToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'block' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'block' ] = $wgUser->getEditToken(); } - $cachedBlockToken = $wgUser->getEditToken(); - return $cachedBlockToken; + return ApiQueryInfo::$cachedTokens[ 'block' ]; } public static function getUnblockToken( $pageid, $title ) { @@ -189,13 +192,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedEmailToken = null; - if ( !is_null( $cachedEmailToken ) ) { - return $cachedEmailToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'email' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'email' ] = $wgUser->getEditToken(); } - $cachedEmailToken = $wgUser->getEditToken(); - return $cachedEmailToken; + return ApiQueryInfo::$cachedTokens[ 'email' ]; } public static function getImportToken( $pageid, $title ) { @@ -204,13 +206,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedImportToken = null; - if ( !is_null( $cachedImportToken ) ) { - return $cachedImportToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'import' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'import' ] = $wgUser->getEditToken(); } - $cachedImportToken = $wgUser->getEditToken(); - return $cachedImportToken; + return ApiQueryInfo::$cachedTokens[ 'import' ]; } public static function getWatchToken( $pageid, $title ) { @@ -219,13 +220,26 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedWatchToken = null; - if ( !is_null( $cachedWatchToken ) ) { - return $cachedWatchToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'watch' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'watch' ] = $wgUser->getEditToken( 'watch' ); } - $cachedWatchToken = $wgUser->getEditToken( 'watch' ); - return $cachedWatchToken; + return ApiQueryInfo::$cachedTokens[ 'watch' ]; + } + + public static function getOptionsToken( $pageid, $title ) { + global $wgUser; + if ( !$wgUser->isLoggedIn() ) { + return false; + } + + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'options' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'options' ] = $wgUser->getEditToken(); + } + + return ApiQueryInfo::$cachedTokens[ 'options' ]; } public function execute() { @@ -234,6 +248,7 @@ class ApiQueryInfo extends ApiQueryBase { $prop = array_flip( $this->params['prop'] ); $this->fld_protection = isset( $prop['protection'] ); $this->fld_watched = isset( $prop['watched'] ); + $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] ); $this->fld_talkid = isset( $prop['talkid'] ); $this->fld_subjectid = isset( $prop['subjectid'] ); $this->fld_url = isset( $prop['url'] ); @@ -269,7 +284,10 @@ class ApiQueryInfo extends ApiQueryBase { } $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' ); - $this->pageIsRedir = $pageSet->getCustomField( 'page_is_redirect' ); + // when resolving redirects, no page will have this field + $this->pageIsRedir = !$pageSet->isResolvingRedirects() + ? $pageSet->getCustomField( 'page_is_redirect' ) + : array(); $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' ); global $wgDisableCounters; @@ -286,7 +304,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->getProtectionInfo(); } - if ( $this->fld_watched ) { + if ( $this->fld_watched || $this->fld_notificationtimestamp ) { $this->getWatchedInfo(); } @@ -322,7 +340,10 @@ class ApiQueryInfo extends ApiQueryBase { */ private function extractPageInfo( $pageid, $title ) { $pageInfo = array(); - if ( $title->exists() ) { + $titleExists = $pageid > 0; //$title->exists() needs pageid, which is not set for all title objects + $ns = $title->getNamespace(); + $dbkey = $title->getDBkey(); + if ( $titleExists ) { global $wgDisableCounters; $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] ); @@ -332,7 +353,7 @@ class ApiQueryInfo extends ApiQueryBase { : intval( $this->pageCounter[$pageid] ); $pageInfo['length'] = intval( $this->pageLength[$pageid] ); - if ( $this->pageIsRedir[$pageid] ) { + if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) { $pageInfo['redirect'] = ''; } if ( $this->pageIsNew[$pageid] ) { @@ -355,23 +376,30 @@ class ApiQueryInfo extends ApiQueryBase { if ( $this->fld_protection ) { $pageInfo['protection'] = array(); - if ( isset( $this->protections[$title->getNamespace()][$title->getDBkey()] ) ) { + if ( isset( $this->protections[$ns][$dbkey] ) ) { $pageInfo['protection'] = - $this->protections[$title->getNamespace()][$title->getDBkey()]; + $this->protections[$ns][$dbkey]; } $this->getResult()->setIndexedTagName( $pageInfo['protection'], 'pr' ); } - if ( $this->fld_watched && isset( $this->watched[$title->getNamespace()][$title->getDBkey()] ) ) { + if ( $this->fld_watched && isset( $this->watched[$ns][$dbkey] ) ) { $pageInfo['watched'] = ''; } - if ( $this->fld_talkid && isset( $this->talkids[$title->getNamespace()][$title->getDBkey()] ) ) { - $pageInfo['talkid'] = $this->talkids[$title->getNamespace()][$title->getDBkey()]; + if ( $this->fld_notificationtimestamp ) { + $pageInfo['notificationtimestamp'] = ''; + if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) { + $pageInfo['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] ); + } + } + + if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) { + $pageInfo['talkid'] = $this->talkids[$ns][$dbkey]; } - if ( $this->fld_subjectid && isset( $this->subjectids[$title->getNamespace()][$title->getDBkey()] ) ) { - $pageInfo['subjectid'] = $this->subjectids[$title->getNamespace()][$title->getDBkey()]; + if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) { + $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey]; } if ( $this->fld_url ) { @@ -383,7 +411,7 @@ class ApiQueryInfo extends ApiQueryBase { } if ( $this->fld_preload ) { - if ( $title->exists() ) { + if ( $titleExists ) { $pageInfo['preload'] = ''; } else { $text = null; @@ -394,8 +422,8 @@ class ApiQueryInfo extends ApiQueryBase { } if ( $this->fld_displaytitle ) { - if ( isset( $this->displaytitles[$title->getArticleId()] ) ) { - $pageInfo['displaytitle'] = $this->displaytitles[$title->getArticleId()]; + if ( isset( $this->displaytitles[$pageid] ) ) { + $pageInfo['displaytitle'] = $this->displaytitles[$pageid]; } else { $pageInfo['displaytitle'] = $title->getPrefixedText(); } @@ -415,15 +443,14 @@ class ApiQueryInfo extends ApiQueryBase { // Get normal protections for existing titles if ( count( $this->titles ) ) { $this->resetQueryParams(); - $this->addTables( array( 'page_restrictions', 'page' ) ); - $this->addWhere( 'page_id=pr_page' ); + $this->addTables( 'page_restrictions' ); $this->addFields( array( 'pr_page', 'pr_type', 'pr_level', - 'pr_expiry', 'pr_cascade', 'page_namespace', - 'page_title' ) ); + 'pr_expiry', 'pr_cascade' ) ); $this->addWhereFld( 'pr_page', array_keys( $this->titles ) ); $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { + $title = $this->titles[$row->pr_page]; $a = array( 'type' => $row->pr_type, 'level' => $row->pr_level, @@ -432,11 +459,14 @@ class ApiQueryInfo extends ApiQueryBase { if ( $row->pr_cascade ) { $a['cascade'] = ''; } - $this->protections[$row->page_namespace][$row->page_title][] = $a; - - // Also check old restrictions - if ( $this->pageRestrictions[$row->pr_page] ) { - $restrictions = explode( ':', trim( $this->pageRestrictions[$row->pr_page] ) ); + $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a; + } + // Also check old restrictions + foreach( $this->titles as $pageId => $title ) { + if ( $this->pageRestrictions[$pageId] ) { + $namespace = $title->getNamespace(); + $dbKey = $title->getDBkey(); + $restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) ); foreach ( $restrictions as $restrict ) { $temp = explode( '=', trim( $restrict ) ); if ( count( $temp ) == 1 ) { @@ -446,12 +476,12 @@ class ApiQueryInfo extends ApiQueryBase { if ( $restriction == '' ) { continue; } - $this->protections[$row->page_namespace][$row->page_title][] = array( + $this->protections[$namespace][$dbKey][] = array( 'type' => 'edit', 'level' => $restriction, 'expiry' => 'infinity', ); - $this->protections[$row->page_namespace][$row->page_title][] = array( + $this->protections[$namespace][$dbKey][] = array( 'type' => 'move', 'level' => $restriction, 'expiry' => 'infinity', @@ -461,7 +491,7 @@ class ApiQueryInfo extends ApiQueryBase { if ( $restriction == '' ) { continue; } - $this->protections[$row->page_namespace][$row->page_title][] = array( + $this->protections[$namespace][$dbKey][] = array( 'type' => $temp[0], 'level' => $restriction, 'expiry' => 'infinity', @@ -612,6 +642,7 @@ class ApiQueryInfo extends ApiQueryBase { /** * Get information about watched status and put it in $this->watched + * and $this->notificationtimestamps */ private function getWatchedInfo() { $user = $this->getUser(); @@ -621,6 +652,7 @@ class ApiQueryInfo extends ApiQueryBase { } $this->watched = array(); + $this->notificationtimestamps = array(); $db = $this->getDB(); $lb = new LinkBatch( $this->everything ); @@ -628,6 +660,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->resetQueryParams(); $this->addTables( array( 'watchlist' ) ); $this->addFields( array( 'wl_title', 'wl_namespace' ) ); + $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp ); $this->addWhere( array( $lb->constructSet( 'wl', $db ), 'wl_user' => $user->getID() @@ -636,7 +669,12 @@ class ApiQueryInfo extends ApiQueryBase { $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { - $this->watched[$row->wl_namespace][$row->wl_title] = true; + if ( $this->fld_watched ) { + $this->watched[$row->wl_namespace][$row->wl_title] = true; + } + if ( $this->fld_notificationtimestamp ) { + $this->notificationtimestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; + } } } @@ -671,6 +709,7 @@ class ApiQueryInfo extends ApiQueryBase { 'protection', 'talkid', 'watched', # private + 'notificationtimestamp', # private 'subjectid', 'url', 'readable', # private @@ -692,20 +731,80 @@ class ApiQueryInfo extends ApiQueryBase { return array( 'prop' => array( 'Which additional properties to get:', - ' protection - List the protection level of each page', - ' talkid - The page ID of the talk page for each non-talk page', - ' watched - List the watched status of each page', - ' subjectid - The page ID of the parent page for each talk page', - ' url - Gives a full URL to the page, and also an edit URL', - ' readable - Whether the user can read this page', - ' preload - Gives the text returned by EditFormPreloadText', - ' displaytitle - Gives the way the page title is actually displayed', + ' protection - List the protection level of each page', + ' talkid - The page ID of the talk page for each non-talk page', + ' watched - List the watched status of each page', + ' notificationtimestamp - The watchlist notification timestamp of each page', + ' subjectid - The page ID of the parent page for each talk page', + ' url - Gives a full URL to the page, and also an edit URL', + ' readable - Whether the user can read this page', + ' preload - Gives the text returned by EditFormPreloadText', + ' displaytitle - Gives the way the page title is actually displayed', ), 'token' => 'Request a token to perform a data-modifying action on a page', 'continue' => 'When more results are available, use this to continue', ); } + public function getResultProperties() { + $props = array( + ApiBase::PROP_LIST => false, + '' => array( + 'touched' => 'timestamp', + 'lastrevid' => 'integer', + 'counter' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'length' => 'integer', + 'redirect' => 'boolean', + 'new' => 'boolean', + 'starttimestamp' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + 'watched' => array( + 'watched' => 'boolean' + ), + 'notificationtimestamp' => array( + 'notificationtimestamp' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + 'talkid' => array( + 'talkid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'subjectid' => array( + 'subjectid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'url' => array( + 'fullurl' => 'string', + 'editurl' => 'string' + ), + 'readable' => array( + 'readable' => 'boolean' + ), + 'preload' => array( + 'preload' => 'string' + ), + 'displaytitle' => array( + 'displaytitle' => 'string' + ) + ); + + self::addTokenProperties( $props, $this->getTokenFunctions() ); + + return $props; + } + public function getDescription() { return 'Get basic page information such as namespace, title, last touched date, ...'; } diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php index 15734944..3920407b 100644 --- a/includes/api/ApiQueryLangBacklinks.php +++ b/includes/api/ApiQueryLangBacklinks.php @@ -5,7 +5,7 @@ * Created on May 14, 2011 * * Copyright © 2011 Sam Reed - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -61,15 +61,17 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { 'original value returned by the previous query', '_badcontinue' ); } - $prefix = $this->getDB()->strencode( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $db = $this->getDB(); + $op = $params['dir'] == 'descending' ? '<' : '>'; + $prefix = $db->addQuotes( $cont[0] ); + $title = $db->addQuotes( $cont[1] ); $from = intval( $cont[2] ); $this->addWhere( - "ll_lang > '$prefix' OR " . - "(ll_lang = '$prefix' AND " . - "(ll_title > '$title' OR " . - "(ll_title = '$title' AND " . - "ll_from >= $from)))" + "ll_lang $op $prefix OR " . + "(ll_lang = $prefix AND " . + "(ll_title $op $title OR " . + "(ll_title = $title AND " . + "ll_from $op= $from)))" ); } @@ -83,16 +85,24 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { $this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect', 'll_from', 'll_lang', 'll_title' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); if ( isset( $params['lang'] ) ) { $this->addWhereFld( 'll_lang', $params['lang'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'll_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'll_from' ); + $this->addOption( 'ORDER BY', 'll_from' . $sort ); } else { - $this->addOption( 'ORDER BY', 'll_title, ll_from' ); + $this->addOption( 'ORDER BY', array( + 'll_title' . $sort, + 'll_from' . $sort + )); } } else { - $this->addOption( 'ORDER BY', 'll_lang, ll_title, ll_from' ); + $this->addOption( 'ORDER BY', array( + 'll_lang' . $sort, + 'll_title' . $sort, + 'll_from' . $sort + )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -170,6 +180,13 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { 'lltitle', ), ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ) + ), ); } @@ -184,6 +201,24 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { ' lltitle - Adds the title of the language ink', ), 'limit' => 'How many total pages to return', + 'dir' => 'The direction in which to list', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'pageid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string', + 'redirect' => 'boolean' + ), + 'lllang' => array( + 'lllang' => 'string' + ), + 'lltitle' => array( + 'lltitle' => 'string' + ) ); } @@ -205,7 +240,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { public function getExamples() { return array( 'api.php?action=query&list=langbacklinks&lbltitle=Test&lbllang=fr', - 'api.php?action=query&generator=langbacklinks&glbltitle=Test&lbllang=fr&prop=info' + 'api.php?action=query&generator=langbacklinks&glbltitle=Test&glbllang=fr&prop=info' ); } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index fdba8465..3109a090 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -60,35 +60,36 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $llfrom = intval( $cont[0] ); - $lllang = $this->getDB()->strencode( $cont[1] ); + $lllang = $this->getDB()->addQuotes( $cont[1] ); $this->addWhere( - "ll_from > $llfrom OR " . + "ll_from $op $llfrom OR " . "(ll_from = $llfrom AND " . - "ll_lang >= '$lllang')" + "ll_lang $op= $lllang)" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); - if ( isset( $params['lang'] ) ) { + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + if ( isset( $params['lang'] ) ) { $this->addWhereFld( 'll_lang', $params['lang'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'll_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'll_from' . $dir ); + $this->addOption( 'ORDER BY', 'll_from' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'll_title' . $dir, - 'll_from' . $dir + 'll_title' . $sort, + 'll_from' . $sort )); } } else { // Don't order by ll_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'll_lang' . $dir ); + $this->addOption( 'ORDER BY', 'll_lang' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'll_from' . $dir, - 'll_lang' . $dir + 'll_from' . $sort, + 'll_lang' . $sort )); } } @@ -158,6 +159,19 @@ class ApiQueryLangLinks extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'lang' => 'string', + 'url' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + '*' => 'string' + ) + ); + } + public function getDescription() { return 'Returns all interlanguage links from the given page(s)'; } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 0377eddb..9e4b7ebb 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -4,7 +4,7 @@ * * Created on May 12, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -85,9 +85,9 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $this->addFields( array( - $this->prefix . '_from AS pl_from', - $this->prefix . '_namespace AS pl_namespace', - $this->prefix . '_title AS pl_title' + 'pl_from' => $this->prefix . '_from', + 'pl_namespace' => $this->prefix . '_namespace', + 'pl_title' => $this->prefix . '_title' ) ); $this->addTables( $this->table ); @@ -116,19 +116,20 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $plfrom = intval( $cont[0] ); $plns = intval( $cont[1] ); - $pltitle = $this->getDB()->strencode( $this->titleToKey( $cont[2] ) ); + $pltitle = $this->getDB()->addQuotes( $cont[2] ); $this->addWhere( - "{$this->prefix}_from > $plfrom OR " . + "{$this->prefix}_from $op $plfrom OR " . "({$this->prefix}_from = $plfrom AND " . - "({$this->prefix}_namespace > $plns OR " . + "({$this->prefix}_namespace $op $plns OR " . "({$this->prefix}_namespace = $plns AND " . - "{$this->prefix}_title >= '$pltitle')))" + "{$this->prefix}_title $op= $pltitle)))" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Here's some MySQL craziness going on: if you use WHERE foo='bar' // and later ORDER BY foo MySQL doesn't notice the ORDER BY is pointless // but instead goes and filesorts, because the index for foo was used @@ -136,13 +137,13 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { // clause from the ORDER BY clause $order = array(); if ( count( $this->getPageSet()->getGoodTitles() ) != 1 ) { - $order[] = $this->prefix . '_from' . $dir; + $order[] = $this->prefix . '_from' . $sort; } if ( count( $params['namespace'] ) != 1 ) { - $order[] = $this->prefix . '_namespace' . $dir; + $order[] = $this->prefix . '_namespace' . $sort; } - $order[] = $this->prefix . "_title" . $dir; + $order[] = $this->prefix . '_title' . $sort; $this->addOption( 'ORDER BY', $order ); $this->addOption( 'USE INDEX', $this->prefix . '_from' ); $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -156,8 +157,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', - "{$row->pl_from}|{$row->pl_namespace}|" . - $this->keyToTitle( $row->pl_title ) ); + "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" ); break; } $vals = array(); @@ -165,8 +165,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $fit = $this->addPageSubItem( $row->pl_from, $vals ); if ( !$fit ) { $this->setContinueEnumParameter( 'continue', - "{$row->pl_from}|{$row->pl_namespace}|" . - $this->keyToTitle( $row->pl_title ) ); + "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" ); break; } } @@ -178,8 +177,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', - "{$row->pl_from}|{$row->pl_namespace}|" . - $this->keyToTitle( $row->pl_title ) ); + "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" ); break; } $titles[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); @@ -226,6 +224,15 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return "Returns all {$this->description}s from the given page(s)"; } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 0d07a254..5d85c221 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -4,7 +4,7 @@ * * Created on Oct 16, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -195,6 +195,7 @@ class ApiQueryLogEvents extends ApiQueryBase { * @param $type string * @param $action string * @param $ts + * @param $legacy bool * @return array */ public static function addLogParams( $result, &$vals, $params, $type, $action, $ts, $legacy = false ) { @@ -262,6 +263,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } if ( !is_null( $params ) ) { $result->setIndexedTagName( $params, 'param' ); + $result->setIndexedTagName_recursive( $params, 'param' ); $vals = array_merge( $vals, $params ); } return $vals; @@ -358,7 +360,7 @@ class ApiQueryLogEvents extends ApiQueryBase { public function getCacheMode( $params ) { if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } else { return 'public'; @@ -366,7 +368,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getAllowedParams() { - global $wgLogTypes, $wgLogActions; + global $wgLogTypes, $wgLogActions, $wgLogActionsHandlers; return array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, @@ -388,7 +390,7 @@ class ApiQueryLogEvents extends ApiQueryBase { ApiBase::PARAM_TYPE => $wgLogTypes ), 'action' => array( - ApiBase::PARAM_TYPE => array_keys( $wgLogActions ) + ApiBase::PARAM_TYPE => array_keys( array_merge( $wgLogActions, $wgLogActionsHandlers ) ) ), 'start' => array( ApiBase::PARAM_TYPE => 'timestamp' @@ -446,6 +448,62 @@ class ApiQueryLogEvents extends ApiQueryBase { ); } + public function getResultProperties() { + global $wgLogTypes; + return array( + 'ids' => array( + 'logid' => 'integer', + 'pageid' => 'integer' + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'type' => array( + 'type' => array( + ApiBase::PROP_TYPE => $wgLogTypes + ), + 'action' => 'string' + ), + 'details' => array( + 'actionhidden' => 'boolean' + ), + 'user' => array( + 'userhidden' => 'boolean', + 'user' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'anon' => 'boolean' + ), + 'userid' => array( + 'userhidden' => 'boolean', + 'userid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'anon' => 'boolean' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'comment' => array( + 'commenthidden' => 'boolean', + 'comment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'parsedcomment' => array( + 'commenthidden' => 'boolean', + 'parsedcomment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return 'Get events from logs'; } diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php index 44cc1d32..14aed28d 100644 --- a/includes/api/ApiQueryProtectedTitles.php +++ b/includes/api/ApiQueryProtectedTitles.php @@ -4,7 +4,7 @@ * * Created on Feb 13, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -139,7 +139,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { public function getCacheMode( $params ) { if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } else { return 'public'; @@ -214,6 +214,40 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + global $wgRestrictionLevels; + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'user' => array( + 'user' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'userid' => 'integer' + ), + 'comment' => array( + 'comment' => 'string' + ), + 'parsedcomment' => array( + 'parsedcomment' => 'string' + ), + 'expiry' => array( + 'expiry' => 'timestamp' + ), + 'level' => array( + 'level' => array( + ApiBase::PROP_TYPE => array_diff( $wgRestrictionLevels, array( '' ) ) + ) + ) + ); + } + public function getDescription() { return 'List all titles protected from creation'; } diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 5eba0de6..a8be26d3 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -4,7 +4,7 @@ * * Created on Dec 22, 2010 * - * Copyright © 2010 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2010 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -70,6 +70,8 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { * @param $resultPageSet ApiPageSet */ public function run( $resultPageSet = null ) { + global $wgQueryCacheLimit; + $params = $this->extractRequestParams(); $result = $this->getResult(); @@ -88,6 +90,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { if ( $ts ) { $r['cachedtimestamp'] = wfTimestamp( TS_ISO_8601, $ts ); } + $r['maxresults'] = $wgQueryCacheLimit; } } $result->addValue( array( 'query' ), $this->getModuleName(), $r ); @@ -170,6 +173,38 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + ApiBase::PROP_ROOT => array( + 'name' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => false + ), + 'disabled' => array( + ApiBase::PROP_TYPE => 'boolean', + ApiBase::PROP_NULLABLE => false + ), + 'cached' => array( + ApiBase::PROP_TYPE => 'boolean', + ApiBase::PROP_NULLABLE => false + ), + 'cachedtimestamp' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + '' => array( + 'value' => 'string', + 'timestamp' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ), + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return 'Get a list provided by a QueryPage-based special page'; } diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index 2e9e2dd5..ddf5841b 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -161,6 +161,16 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'id' => 'integer', + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return array( 'Get a set of random pages', diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index bf5bbd9b..7ae4f371 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -4,7 +4,7 @@ * * Created on Oct 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -70,24 +70,37 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /** * @param $pageid * @param $title - * @param $rc RecentChange + * @param $rc RecentChange (optional) * @return bool|String */ - public static function getPatrolToken( $pageid, $title, $rc ) { + public static function getPatrolToken( $pageid, $title, $rc = null ) { global $wgUser; - if ( !$wgUser->useRCPatrol() && ( !$wgUser->useNPPatrol() || - $rc->getAttribute( 'rc_type' ) != RC_NEW ) ) - { - return false; + + $validTokenUser = false; + + if ( $rc ) { + if ( ( $wgUser->useRCPatrol() && $rc->getAttribute( 'rc_type' ) == RC_EDIT ) || + ( $wgUser->useNPPatrol() && $rc->getAttribute( 'rc_type' ) == RC_NEW ) ) + { + $validTokenUser = true; + } + } else { + if ( $wgUser->useRCPatrol() || $wgUser->useNPPatrol() ) { + $validTokenUser = true; + } } - // The patrol token is always the same, let's exploit that - static $cachedPatrolToken = null; - if ( is_null( $cachedPatrolToken ) ) { - $cachedPatrolToken = $wgUser->getEditToken( 'patrol' ); + if ( $validTokenUser ) { + // The patrol token is always the same, let's exploit that + static $cachedPatrolToken = null; + if ( is_null( $cachedPatrolToken ) ) { + $cachedPatrolToken = $wgUser->getEditToken( 'patrol' ); + } + return $cachedPatrolToken; + } else { + return false; } - return $cachedPatrolToken; } /** @@ -131,7 +144,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Build our basic query. Namely, something along the lines of: * SELECT * FROM recentchanges WHERE rc_timestamp > $start * AND rc_timestamp < $end AND rc_namespace = $namespace - * AND rc_deleted = '0' + * AND rc_deleted = 0 */ $this->addTables( 'recentchanges' ); $index = array( 'recentchanges' => 'rc_timestamp' ); // May change @@ -223,7 +236,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment ); $this->addFieldsIf( 'rc_user', $this->fld_user ); $this->addFieldsIf( 'rc_user_text', $this->fld_user || $this->fld_userid ); - $this->addFieldsIf( array( 'rc_minor', 'rc_new', 'rc_bot' ) , $this->fld_flags ); + $this->addFieldsIf( array( 'rc_minor', 'rc_type', 'rc_bot' ) , $this->fld_flags ); $this->addFieldsIf( array( 'rc_old_len', 'rc_new_len' ), $this->fld_sizes ); $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); $this->addFieldsIf( array( 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ), $this->fld_loginfo ); @@ -304,7 +317,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { * Extracts from a single sql row the data needed to describe one recent change. * * @param $row The row from which to extract the data. - * @return An array mapping strings (descriptors) to their respective string values. + * @return array An array mapping strings (descriptors) to their respective string values. * @access public */ public function extractRowInfo( $row ) { @@ -380,7 +393,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( $row->rc_bot ) { $vals['bot'] = ''; } - if ( $row->rc_new ) { + if ( $row->rc_type == RC_NEW ) { $vals['new'] = ''; } if ( $row->rc_minor ) { @@ -423,13 +436,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $vals['logid'] = intval( $row->rc_logid ); $vals['logtype'] = $row->rc_log_type; $vals['logaction'] = $row->rc_log_action; + $logEntry = DatabaseLogEntry::newFromRow( (array)$row ); ApiQueryLogEvents::addLogParams( $this->getResult(), $vals, - $row->rc_params, - $row->rc_log_action, - $row->rc_log_type, - $row->rc_timestamp + $logEntry->getParameters(), + $logEntry->getType(), + $logEntry->getSubtype(), + $logEntry->getTimestamp() ); } @@ -489,7 +503,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { return 'private'; } if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } return 'public'; @@ -615,6 +629,97 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + global $wgLogTypes; + $props = array( + '' => array( + 'type' => array( + ApiBase::PROP_TYPE => array( + 'edit', + 'new', + 'move', + 'log', + 'move over redirect' + ) + ) + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string', + 'new_ns' => array( + ApiBase::PROP_TYPE => 'namespace', + ApiBase::PROP_NULLABLE => true + ), + 'new_title' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'ids' => array( + 'rcid' => 'integer', + 'pageid' => 'integer', + 'revid' => 'integer', + 'old_revid' => 'integer' + ), + 'user' => array( + 'user' => 'string', + 'anon' => 'boolean' + ), + 'userid' => array( + 'userid' => 'integer', + 'anon' => 'boolean' + ), + 'flags' => array( + 'bot' => 'boolean', + 'new' => 'boolean', + 'minor' => 'boolean' + ), + 'sizes' => array( + 'oldlen' => 'integer', + 'newlen' => 'integer' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'comment' => array( + 'comment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'parsedcomment' => array( + 'parsedcomment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'redirect' => array( + 'redirect' => 'boolean' + ), + 'patrolled' => array( + 'patrolled' => 'boolean' + ), + 'loginfo' => array( + 'logid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'logtype' => array( + ApiBase::PROP_TYPE => $wgLogTypes, + ApiBase::PROP_NULLABLE => true + ), + 'logaction' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + + self::addTokenProperties( $props, $this->getTokenFunctions() ); + + return $props; + } + public function getDescription() { return 'Enumerate recent changes'; } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index fa58bdf0..b89a8ea9 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -224,6 +224,13 @@ class ApiQueryRevisions extends ApiQueryBase { } } + // add user name, if needed + if ( $this->fld_user ) { + $this->addTables( 'user' ); + $this->addJoinConds( array( 'user' => Revision::userJoinCond() ) ); + $this->addFields( Revision::selectUserFields() ); + } + // Bug 24166 - API error when using rvprop=tags $this->addTables( 'revision' ); @@ -241,6 +248,16 @@ class ApiQueryRevisions extends ApiQueryBase { $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' ); } + // Continuing effectively uses startid. But we can't use rvstartid + // directly, because there is no way to tell the client to ''not'' + // send rvstart if it sent it in the original query. So instead we + // send the continuation startid as rvcontinue, and ignore both + // rvstart and rvstartid when that is supplied. + if ( !is_null( $params['continue'] ) ) { + $params['startid'] = $params['continue']; + unset( $params['start'] ); + } + // This code makes an assumption that sorting by rev_id and rev_timestamp produces // the same result. This way users may request revisions starting at a given time, // but to page through results use the rev_id returned after each page. @@ -290,7 +307,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addWhereFld( 'rev_id', array_keys( $revs ) ); if ( !is_null( $params['continue'] ) ) { - $this->addWhere( "rev_id >= '" . intval( $params['continue'] ) . "'" ); + $this->addWhere( 'rev_id >= ' . intval( $params['continue'] ) ); } $this->addOption( 'ORDER BY', 'rev_id' ); @@ -322,12 +339,15 @@ class ApiQueryRevisions extends ApiQueryBase { $pageid = intval( $cont[0] ); $revid = intval( $cont[1] ); $this->addWhere( - "rev_page > '$pageid' OR " . - "(rev_page = '$pageid' AND " . - "rev_id >= '$revid')" + "rev_page > $pageid OR " . + "(rev_page = $pageid AND " . + "rev_id >= $revid)" ); } - $this->addOption( 'ORDER BY', 'rev_page, rev_id' ); + $this->addOption( 'ORDER BY', array( + 'rev_page', + 'rev_id' + )); // assumption testing -- we should never get more then $pageCount rows. $limit = $pageCount; @@ -347,14 +367,14 @@ class ApiQueryRevisions extends ApiQueryBase { if ( !$enumRevMode ) { ApiBase::dieDebug( __METHOD__, 'Got more rows then expected' ); // bug report } - $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) ); + $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) ); break; } $fit = $this->addPageSubItem( $row->rev_page, $this->extractRowInfo( $row ), 'rev' ); if ( !$fit ) { if ( $enumRevMode ) { - $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) ); + $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) ); } elseif ( $revCount > 0 ) { $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) ); } else { @@ -528,7 +548,7 @@ class ApiQueryRevisions extends ApiQueryBase { return 'private'; } if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } return 'public'; @@ -638,6 +658,66 @@ class ApiQueryRevisions extends ApiQueryBase { ); } + public function getResultProperties() { + $props = array( + '' => array(), + 'ids' => array( + 'revid' => 'integer', + 'parentid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'flags' => array( + 'minor' => 'boolean' + ), + 'user' => array( + 'userhidden' => 'boolean', + 'user' => 'string', + 'anon' => 'boolean' + ), + 'userid' => array( + 'userhidden' => 'boolean', + 'userid' => 'integer', + 'anon' => 'boolean' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'size' => array( + 'size' => 'integer' + ), + 'sha1' => array( + 'sha1' => 'string' + ), + 'comment' => array( + 'commenthidden' => 'boolean', + 'comment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'parsedcomment' => array( + 'commenthidden' => 'boolean', + 'parsedcomment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'content' => array( + '*' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'texthidden' => 'boolean' + ) + ); + + self::addTokenProperties( $props, $this->getTokenFunctions() ); + + return $props; + } + public function getDescription() { return array( 'Get revision information', diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 40aac050..364433d5 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -4,7 +4,7 @@ * * Created on July 30, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -280,6 +280,63 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'snippet' => array( + 'snippet' => 'string' + ), + 'size' => array( + 'size' => 'integer' + ), + 'wordcount' => array( + 'wordcount' => 'integer' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'score' => array( + 'score' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'titlesnippet' => array( + 'titlesnippet' => 'string' + ), + 'redirecttitle' => array( + 'redirecttitle' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'redirectsnippet' => array( + 'redirectsnippet' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'sectiontitle' => array( + 'sectiontitle' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'sectionsnippet' => array( + 'sectionsnippet' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'hasrelated' => array( + 'hasrelated' => 'boolean' + ) + ); + } + public function getDescription() { return 'Perform a full text search'; } diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index e2580ac6..ec503d64 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -93,6 +93,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'showhooks': $fit = $this->appendSubscribedHooks( $p ); break; + case 'variables': + $fit = $this->appendVariables( $p ); + break; default: ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ); } @@ -121,9 +124,14 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['dbtype'] = $GLOBALS['wgDBtype']; $data['dbversion'] = $this->getDB()->getServerVersion(); - $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] ); - if ( $svn ) { - $data['rev'] = $svn; + $git = SpecialVersion::getGitHeadSha1( $GLOBALS['IP'] ); + if ( $git ) { + $data['git-hash'] = $git; + } else { + $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] ); + if ( $svn ) { + $data['rev'] = $svn; + } } // 'case-insensitive' option is reserved for future @@ -142,6 +150,15 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['fallback'] = $fallbacks; $this->getResult()->setIndexedTagName( $data['fallback'], 'lang' ); + if( $wgContLang->hasVariants() ) { + $variants = array(); + foreach( $wgContLang->getVariants() as $code ) { + $variants[] = array( 'code' => $code ); + } + $data['variants'] = $variants; + $this->getResult()->setIndexedTagName( $data['variants'], 'lang' ); + } + if ( $wgContLang->isRTL() ) { $data['rtl'] = ''; } @@ -177,6 +194,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['misermode'] = ''; } + $data['maxuploadsize'] = UploadBase::getMaxUploadSize(); + wfRunHooks( 'APIQuerySiteInfoGeneralInfo', array( $this, &$data ) ); return $this->getResult()->addValue( 'query', $property, $data ); @@ -204,6 +223,10 @@ class ApiQuerySiteinfo extends ApiQueryBase { if ( MWNamespace::isContent( $ns ) ) { $data[$ns]['content'] = ''; } + + if ( MWNamespace::isNonincludable( $ns ) ) { + $data[$ns]['nonincludable'] = ''; + } } $this->getResult()->setIndexedTagName( $data, 'ns' ); @@ -234,10 +257,13 @@ class ApiQuerySiteinfo extends ApiQueryBase { protected function appendSpecialPageAliases( $property ) { global $wgContLang; $data = array(); - foreach ( $wgContLang->getSpecialPageAliases() as $specialpage => $aliases ) { - $arr = array( 'realname' => $specialpage, 'aliases' => $aliases ); - $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' ); - $data[] = $arr; + $aliases = $wgContLang->getSpecialPageAliases(); + foreach ( SpecialPageFactory::getList() as $specialpage => $stuff ) { + if ( isset( $aliases[$specialpage] ) ) { + $arr = array( 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] ); + $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' ); + $data[] = $arr; + } } $this->getResult()->setIndexedTagName( $data, 'specialpage' ); return $this->getResult()->addValue( 'query', $property, $data ); @@ -271,12 +297,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $params = $this->extractRequestParams(); $langCode = isset( $params['inlanguagecode'] ) ? $params['inlanguagecode'] : ''; - - if( $langCode ) { - $langNames = Language::getTranslatedLanguageNames( $langCode ); - } else { - $langNames = Language::getLanguageNames(); - } + $langNames = Language::fetchLanguageNames( $langCode ); $getPrefixes = Interwiki::getAllPrefixes( $local ); $data = array(); @@ -477,12 +498,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { public function appendLanguages( $property ) { $params = $this->extractRequestParams(); $langCode = isset( $params['inlanguagecode'] ) ? $params['inlanguagecode'] : ''; - - if( $langCode ) { - $langNames = Language::getTranslatedLanguageNames( $langCode ); - } else { - $langNames = Language::getLanguageNames(); - } + $langNames = Language::fetchLanguageNames( $langCode ); $data = array(); @@ -522,6 +538,12 @@ class ApiQuerySiteinfo extends ApiQueryBase { return $this->getResult()->addValue( 'query', $property, $hooks ); } + public function appendVariables( $property ) { + $variables = MagicWord::getVariableIDs(); + $this->getResult()->setIndexedTagName( $variables, 'v' ); + return $this->getResult()->addValue( 'query', $property, $variables ); + } + private function formatParserTags( $item ) { return "<{$item}>"; } @@ -573,6 +595,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'extensiontags', 'functionhooks', 'showhooks', + 'variables', ) ), 'filteriw' => array( @@ -608,7 +631,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { ' skins - Returns a list of all enabled skins', ' extensiontags - Returns a list of parser extension tags', ' functionhooks - Returns a list of parser function hooks', - ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)' + ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)', + ' variables - Returns a list of variable IDs', ), 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index 4501ec58..a310d109 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -113,7 +113,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { public function getParamDescription() { $p = $this->getModulePrefix(); return array( - 'prop' => self::getPropertyDescriptions( $this->propertyFilter ), + 'prop' => self::getPropertyDescriptions( $this->propertyFilter, $p ), 'filekey' => 'Key that identifies a previous upload that was stashed temporarily.', 'sessionkey' => 'Alias for filekey, for backward compatibility.', 'urlwidth' => "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", @@ -123,6 +123,10 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { ); } + public function getResultProperties() { + return ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter ); + } + public function getDescription() { return 'Returns image information for stashed images'; } diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php index 12cea1d7..f97c1b2a 100644 --- a/includes/api/ApiQueryTags.php +++ b/includes/api/ApiQueryTags.php @@ -59,7 +59,7 @@ class ApiQueryTags extends ApiQueryBase { $this->addTables( 'change_tag' ); $this->addFields( 'ct_tag' ); - $this->addFieldsIf( 'count(*) AS hitcount', $this->fld_hitcount ); + $this->addFieldsIf( array( 'hitcount' => 'COUNT(*)' ), $this->fld_hitcount ); $this->addOption( 'LIMIT', $this->limit + 1 ); $this->addOption( 'GROUP BY', 'ct_tag' ); @@ -73,7 +73,7 @@ class ApiQueryTags extends ApiQueryBase { if ( !$ok ) { break; } - $ok = $this->doTag( $row->ct_tag, $row->hitcount ); + $ok = $this->doTag( $row->ct_tag, $this->fld_hitcount ? $row->hitcount : 0 ); } // include tags with no hits yet @@ -169,6 +169,23 @@ class ApiQueryTags extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'name' => 'string' + ), + 'displayname' => array( + 'displayname' => 'string' + ), + 'description' => array( + 'description' => 'string' + ), + 'hitcount' => array( + 'hitcount' => 'integer' + ) + ); + } + public function getDescription() { return 'List change tags'; } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 8e2f20db..f30b1325 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -4,7 +4,7 @@ * * Created on Oct 16, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -35,10 +35,10 @@ class ApiQueryContributions extends ApiQueryBase { parent::__construct( $query, $moduleName, 'uc' ); } - private $params, $prefixMode, $userprefix, $multiUserMode, $usernames; + private $params, $prefixMode, $userprefix, $multiUserMode, $usernames, $parentLens; private $fld_ids = false, $fld_title = false, $fld_timestamp = false, $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false, - $fld_patrolled = false, $fld_tags = false, $fld_size = false; + $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; public function execute() { // Parse some parameters @@ -50,6 +50,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->fld_comment = isset( $prop['comment'] ); $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); $this->fld_size = isset( $prop['size'] ); + $this->fld_sizediff = isset( $prop['sizediff'] ); $this->fld_flags = isset( $prop['flags'] ); $this->fld_timestamp = isset( $prop['timestamp'] ); $this->fld_patrolled = isset( $prop['patrolled'] ); @@ -82,6 +83,17 @@ class ApiQueryContributions extends ApiQueryBase { // Do the actual query. $res = $this->select( __METHOD__ ); + if( $this->fld_sizediff ) { + $revIds = array(); + foreach ( $res as $row ) { + if( $row->rev_parent_id ) { + $revIds[] = $row->rev_parent_id; + } + } + $this->parentLens = Revision::getParentLengths( $this->getDB(), $revIds ); + $res->rewind(); // reset + } + // Initialise some variables $count = 0; $limit = $this->params['limit']; @@ -152,13 +164,14 @@ class ApiQueryContributions extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the original ' . 'value returned by the previous query', '_badcontinue' ); } - $encUser = $this->getDB()->strencode( $continue[0] ); - $encTS = wfTimestamp( TS_MW, $continue[1] ); + $db = $this->getDB(); + $encUser = $db->addQuotes( $continue[0] ); + $encTS = $db->addQuotes( $db->timestamp( $continue[1] ) ); $op = ( $this->params['dir'] == 'older' ? '<' : '>' ); $this->addWhere( - "rev_user_text $op '$encUser' OR " . - "(rev_user_text = '$encUser' AND " . - "rev_timestamp $op= '$encTS')" + "rev_user_text $op $encUser OR " . + "(rev_user_text = $encUser AND " . + "rev_timestamp $op= $encTS)" ); } @@ -185,7 +198,7 @@ class ApiQueryContributions extends ApiQueryBase { if ( !is_null( $show ) ) { $show = array_flip( $show ); if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) - || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) ) { + || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) ) { $this->dieUsageMsg( 'show' ); } @@ -243,8 +256,9 @@ class ApiQueryContributions extends ApiQueryBase { $this->addFieldsIf( 'page_latest', $this->fld_flags ); // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed? $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment ); - $this->addFieldsIf( 'rev_len', $this->fld_size ); - $this->addFieldsIf( array( 'rev_minor_edit', 'rev_parent_id' ), $this->fld_flags ); + $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff ); + $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags ); + $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff ); $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); if ( $this->fld_tags ) { @@ -332,6 +346,11 @@ class ApiQueryContributions extends ApiQueryBase { $vals['size'] = intval( $row->rev_len ); } + if ( $this->fld_sizediff && !is_null( $row->rev_len ) && !is_null( $row->rev_parent_id ) ) { + $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) ? $this->parentLens[$row->rev_parent_id] : 0; + $vals['sizediff'] = intval( $row->rev_len - $parentLen ); + } + if ( $this->fld_tags ) { if ( $row->ts_tags ) { $tags = explode( ',', $row->ts_tags ); @@ -397,6 +416,7 @@ class ApiQueryContributions extends ApiQueryBase { 'comment', 'parsedcomment', 'size', + 'sizediff', 'flags', 'patrolled', 'tags' @@ -435,7 +455,8 @@ class ApiQueryContributions extends ApiQueryBase { ' timestamp - Adds the timestamp of the edit', ' comment - Adds the comment of the edit', ' parsedcomment - Adds the parsed comment of the edit', - ' size - Adds the size of the page', + ' size - Adds the new size of the edit', + ' sizediff - Adds the size delta of the edit against its parent', ' flags - Adds flags of the edit', ' patrolled - Tags patrolled edits', ' tags - Lists tags for the edit', @@ -447,6 +468,61 @@ class ApiQueryContributions extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'userid' => 'integer', + 'user' => 'string', + 'userhidden' => 'boolean' + ), + 'ids' => array( + 'pageid' => 'integer', + 'revid' => 'integer' + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'flags' => array( + 'new' => 'boolean', + 'minor' => 'boolean', + 'top' => 'boolean' + ), + 'comment' => array( + 'commenthidden' => 'boolean', + 'comment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'parsedcomment' => array( + 'commenthidden' => 'boolean', + 'parsedcomment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'patrolled' => array( + 'patrolled' => 'boolean' + ), + 'size' => array( + 'size' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'sizediff' => array( + 'sizediff' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return 'Get all edits by a user'; } diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index a0ee227f..66906659 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -4,7 +4,7 @@ * * Created on July 30, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -50,7 +50,7 @@ class ApiQueryUserInfo extends ApiQueryBase { } protected function getCurrentUserInfo() { - global $wgRequest, $wgHiddenPrefs; + global $wgHiddenPrefs; $user = $this->getUser(); $result = $this->getResult(); $vals = array(); @@ -63,7 +63,10 @@ class ApiQueryUserInfo extends ApiQueryBase { if ( isset( $this->prop['blockinfo'] ) ) { if ( $user->isBlocked() ) { - $vals['blockedby'] = User::whoIs( $user->blockedBy() ); + $block = $user->getBlock(); + $vals['blockid'] = $block->getId(); + $vals['blockedby'] = $block->getByName(); + $vals['blockedbyid'] = $block->getBy(); $vals['blockreason'] = $user->blockedFor(); } } @@ -73,14 +76,12 @@ class ApiQueryUserInfo extends ApiQueryBase { } if ( isset( $this->prop['groups'] ) ) { - $autolist = ApiQueryUsers::getAutoGroups( $user ); - - $vals['groups'] = array_merge( $autolist, $user->getGroups() ); + $vals['groups'] = $user->getEffectiveGroups(); $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty } if ( isset( $this->prop['implicitgroups'] ) ) { - $vals['implicitgroups'] = ApiQueryUsers::getAutoGroups( $user ); + $vals['implicitgroups'] = $user->getAutomaticGroups(); $result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty } @@ -136,7 +137,7 @@ class ApiQueryUserInfo extends ApiQueryBase { } if ( isset( $this->prop['acceptlang'] ) ) { - $langs = $wgRequest->getAcceptLang(); + $langs = $this->getRequest()->getAcceptLang(); $acceptLang = array(); foreach ( $langs as $lang => $val ) { $r = array( 'q' => $val ); @@ -231,6 +232,63 @@ class ApiQueryUserInfo extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + ApiBase::PROP_LIST => false, + '' => array( + 'id' => 'integer', + 'name' => 'string', + 'anon' => 'boolean' + ), + 'blockinfo' => array( + 'blockid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'blockedby' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'blockedbyid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'blockedreason' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'hasmsg' => array( + 'messages' => 'boolean' + ), + 'preferencestoken' => array( + 'preferencestoken' => 'string' + ), + 'editcount' => array( + 'editcount' => 'integer' + ), + 'realname' => array( + 'realname' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'email' => array( + 'email' => 'string', + 'emailauthenticated' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + 'registrationdate' => array( + 'registrationdate' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return 'Get information about the current user'; } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 31624bdf..bf438d1d 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -4,7 +4,7 @@ * * Created on July 30, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -61,10 +61,10 @@ class ApiQueryUsers extends ApiQueryBase { return $this->tokenFunctions; } - /** - * @param $user User - * @return String - */ + /** + * @param $user User + * @return String + */ public static function getUserrightsToken( $user ) { global $wgUser; // Since the permissions check for userrights is non-trivial, @@ -107,7 +107,7 @@ class ApiQueryUsers extends ApiQueryBase { if ( count( $goodNames ) ) { $this->addTables( 'user' ); - $this->addFields( '*' ); + $this->addFields( User::selectFields() ); $this->addWhereFld( 'user_name', $goodNames ); if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) { @@ -138,7 +138,7 @@ class ApiQueryUsers extends ApiQueryBase { if ( isset( $this->prop['groups'] ) ) { if ( !isset( $data[$name]['groups'] ) ) { - $data[$name]['groups'] = self::getAutoGroups( $user ); + $data[$name]['groups'] = $user->getAutomaticGroups(); } if ( !is_null( $row->ug_group ) ) { @@ -148,7 +148,7 @@ class ApiQueryUsers extends ApiQueryBase { } if ( isset( $this->prop['implicitgroups'] ) && !isset( $data[$name]['implicitgroups'] ) ) { - $data[$name]['implicitgroups'] = self::getAutoGroups( $user ); + $data[$name]['implicitgroups'] = $user->getAutomaticGroups(); } if ( isset( $this->prop['rights'] ) ) { @@ -165,7 +165,9 @@ class ApiQueryUsers extends ApiQueryBase { $data[$name]['hidden'] = ''; } if ( isset( $this->prop['blockinfo'] ) && !is_null( $row->ipb_by_text ) ) { + $data[$name]['blockid'] = $row->ipb_id; $data[$name]['blockedby'] = $row->ipb_by_text; + $data[$name]['blockedbyid'] = $row->ipb_by; $data[$name]['blockreason'] = $row->ipb_reason; $data[$name]['blockexpiry'] = $row->ipb_expiry; } @@ -247,18 +249,15 @@ class ApiQueryUsers extends ApiQueryBase { /** * Gets all the groups that a user is automatically a member of (implicit groups) + * + * @deprecated since 1.20; call User::getAutomaticGroups() directly. * @param $user User * @return array */ public static function getAutoGroups( $user ) { - $groups = array(); - $groups[] = '*'; + wfDeprecated( __METHOD__, '1.20' ); - if ( !$user->isAnon() ) { - $groups[] = 'user'; - } - - return array_merge( $groups, Autopromote::getAutopromoteGroups( $user ) ); + return $user->getAutomaticGroups(); } public function getCacheMode( $params ) { @@ -313,6 +312,73 @@ class ApiQueryUsers extends ApiQueryBase { ); } + public function getResultProperties() { + $props = array( + '' => array( + 'userid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'name' => 'string', + 'invalid' => 'boolean', + 'hidden' => 'boolean', + 'interwiki' => 'boolean', + 'missing' => 'boolean' + ), + 'editcount' => array( + 'editcount' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), + 'registration' => array( + 'registration' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + 'blockinfo' => array( + 'blockid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'blockedby' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'blockedbyid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'blockedreason' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'blockedexpiry' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + 'emailable' => array( + 'emailable' => 'boolean' + ), + 'gender' => array( + 'gender' => array( + ApiBase::PROP_TYPE => array( + 'male', + 'female', + 'unknown' + ), + ApiBase::PROP_NULLABLE => true + ) + ) + ); + + self::addTokenProperties( $props, $this->getTokenFunctions() ); + + return $props; + } + public function getDescription() { return 'Get information about a list of users'; } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index ea56fcd9..a1a33728 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -96,7 +96,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'rc_last_oldid', ) ); - $this->addFieldsIf( array( 'rc_new', 'rc_minor', 'rc_bot' ), $this->fld_flags ); + $this->addFieldsIf( array( 'rc_type', 'rc_minor', 'rc_bot' ), $this->fld_flags ); $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid ); $this->addFieldsIf( 'rc_user_text', $this->fld_user ); $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment ); @@ -254,7 +254,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } if ( $this->fld_flags ) { - if ( $row->rc_new ) { + if ( $row->rc_type == RC_NEW ) { $vals['new'] = ''; } if ( $row->rc_minor ) { @@ -296,13 +296,14 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $vals['logid'] = intval( $row->rc_logid ); $vals['logtype'] = $row->rc_log_type; $vals['logaction'] = $row->rc_log_action; + $logEntry = DatabaseLogEntry::newFromRow( (array)$row ); ApiQueryLogEvents::addLogParams( $this->getResult(), $vals, - $row->rc_params, - $row->rc_log_type, - $row->rc_log_action, - $row->rc_timestamp + $logEntry->getParameters(), + $logEntry->getType(), + $logEntry->getSubtype(), + $logEntry->getTimestamp() ); } @@ -417,6 +418,76 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + global $wgLogTypes; + return array( + 'ids' => array( + 'pageid' => 'integer', + 'revid' => 'integer', + 'old_revid' => 'integer' + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'user' => array( + 'user' => 'string', + 'anon' => 'boolean' + ), + 'userid' => array( + 'userid' => 'integer', + 'anon' => 'boolean' + ), + 'flags' => array( + 'new' => 'boolean', + 'minor' => 'boolean', + 'bot' => 'boolean' + ), + 'patrol' => array( + 'patrolled' => 'boolean' + ), + 'timestamp' => array( + 'timestamp' => 'timestamp' + ), + 'sizes' => array( + 'oldlen' => 'integer', + 'newlen' => 'integer' + ), + 'notificationtimestamp' => array( + 'notificationtimestamp' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + 'comment' => array( + 'comment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'parsedcomment' => array( + 'parsedcomment' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), + 'loginfo' => array( + 'logid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'logtype' => array( + ApiBase::PROP_TYPE => $wgLogTypes, + ApiBase::PROP_NULLABLE => true + ), + 'logaction' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return "Get all recent changes to pages in the logged in user's watchlist"; } diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index 506944f0..6b24aef3 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -4,7 +4,7 @@ * * Created on Oct 4, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -76,19 +76,24 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { "original value returned by the previous query", "_badcontinue" ); } $ns = intval( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $title = $this->getDB()->addQuotes( $cont[1] ); + $op = $params['dir'] == 'ascending' ? '>' : '<'; $this->addWhere( - "wl_namespace > '$ns' OR " . - "(wl_namespace = '$ns' AND " . - "wl_title >= '$title')" + "wl_namespace $op $ns OR " . + "(wl_namespace = $ns AND " . + "wl_title $op= $title)" ); } + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Don't ORDER BY wl_namespace if it's constant in the WHERE clause if ( count( $params['namespace'] ) == 1 ) { - $this->addOption( 'ORDER BY', 'wl_title' ); + $this->addOption( 'ORDER BY', 'wl_title' . $sort ); } else { - $this->addOption( 'ORDER BY', 'wl_namespace, wl_title' ); + $this->addOption( 'ORDER BY', array( + 'wl_namespace' . $sort, + 'wl_title' . $sort + )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); $res = $this->select( __METHOD__ ); @@ -98,8 +103,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { foreach ( $res as $row ) { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . - $this->keyToTitle( $row->wl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title ); break; } $t = Title::makeTitle( $row->wl_namespace, $row->wl_title ); @@ -113,8 +117,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { } $fit = $this->getResult()->addValue( $this->getModuleName(), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . - $this->keyToTitle( $row->wl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title ); break; } } else { @@ -160,7 +163,14 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ), 'token' => array( ApiBase::PARAM_TYPE => 'string' - ) + ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ), + ), ); } @@ -176,6 +186,22 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { 'show' => 'Only list items that meet these criteria', 'owner' => 'The name of the user whose watchlist you\'d like to access', 'token' => 'Give a security token (settable in preferences) to allow access to another user\'s watchlist', + 'dir' => 'Direction to sort the titles and namespaces in', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'changed' => array( + 'changed' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ) ); } diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 798b2275..91e20812 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -4,7 +4,7 @@ * * Created on Sep 4, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" * * 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 @@ -165,7 +165,7 @@ class ApiResult extends ApiBase { * @param $value Mixed * @param $subElemName string when present, content element is created * as a sub item of $arr. Use this parameter to create elements in - * format <elem>text</elem> without attributes + * format "<elem>text</elem>" without attributes. */ public static function setContent( &$arr, $value, $subElemName = null ) { if ( is_array( $value ) ) { diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 436c392b..677df16a 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -4,7 +4,7 @@ * * Created on Jun 20, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -49,7 +49,7 @@ class ApiRollback extends ApiBase { // User and title already validated in call to getTokenSalt from Main $titleObj = $this->getRbTitle(); $pageObj = WikiPage::factory( $titleObj ); - $summary = ( isset( $params['summary'] ) ? $params['summary'] : '' ); + $summary = $params['summary']; $details = array(); $retval = $pageObj->doRollback( $this->getRbUser(), $summary, $params['token'], $params['markbot'], $details, $this->getUser() ); @@ -90,8 +90,11 @@ class ApiRollback extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, - 'summary' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + 'summary' => '', 'markbot' => false, 'watchlist' => array( ApiBase::PARAM_DFLT => 'preferences', @@ -110,12 +113,25 @@ class ApiRollback extends ApiBase { 'title' => 'Title of the page you want to rollback.', 'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.', 'token' => "A rollback token previously retrieved through {$this->getModulePrefix()}prop=revisions", - 'summary' => 'Custom edit summary. If not set, default summary will be used', + 'summary' => 'Custom edit summary. If empty, default summary will be used', 'markbot' => 'Mark the reverted edits and the revert as bot edits', 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', ); } + public function getResultProperties() { + return array( + '' => array( + 'title' => 'string', + 'pageid' => 'integer', + 'summary' => 'string', + 'revid' => 'integer', + 'old_revid' => 'integer', + 'last_revid' => 'integer' + ) + ); + } + public function getDescription() { return array( 'Undo the last edit to the page. If the last user who edited the page made multiple edits in a row,', diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php new file mode 100644 index 00000000..098b1a66 --- /dev/null +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -0,0 +1,285 @@ +<?php + +/** + * API for MediaWiki 1.14+ + * + * Created on Jun 18, 2012 + * + * Copyright © 2012 Brad Jorsch + * + * 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 + */ + +/** + * API interface for setting the wl_notificationtimestamp field + * @ingroup API + */ +class ApiSetNotificationTimestamp extends ApiBase { + + public function __construct( $main, $action ) { + parent::__construct( $main, $action ); + } + + public function execute() { + $user = $this->getUser(); + + if ( $user->isAnon() ) { + $this->dieUsage( 'Anonymous users cannot use watchlist change notifications', 'notloggedin' ); + } + + $params = $this->extractRequestParams(); + $this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' ); + + $pageSet = new ApiPageSet( $this ); + $args = array_merge( array( $params, 'entirewatchlist' ), array_keys( $pageSet->getAllowedParams() ) ); + call_user_func_array( array( $this, 'requireOnlyOneParameter' ), $args ); + + $dbw = $this->getDB( DB_MASTER ); + + $timestamp = null; + if ( isset( $params['timestamp'] ) ) { + $timestamp = $dbw->timestamp( $params['timestamp'] ); + } + + if ( !$params['entirewatchlist'] ) { + $pageSet->execute(); + } + + if ( isset( $params['torevid'] ) ) { + if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { + $this->dieUsage( 'torevid may only be used with a single page', 'multpages' ); + } + $title = reset( $pageSet->getGoodTitles() ); + $timestamp = Revision::getTimestampFromId( $title, $params['torevid'] ); + if ( $timestamp ) { + $timestamp = $dbw->timestamp( $timestamp ); + } else { + $timestamp = null; + } + } elseif ( isset( $params['newerthanrevid'] ) ) { + if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { + $this->dieUsage( 'newerthanrevid may only be used with a single page', 'multpages' ); + } + $title = reset( $pageSet->getGoodTitles() ); + $revid = $title->getNextRevisionID( $params['newerthanrevid'] ); + if ( $revid ) { + $timestamp = $dbw->timestamp( Revision::getTimestampFromId( $title, $revid ) ); + } else { + $timestamp = null; + } + } + + $apiResult = $this->getResult(); + $result = array(); + if ( $params['entirewatchlist'] ) { + // Entire watchlist mode: Just update the thing and return a success indicator + $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $timestamp ), + array( 'wl_user' => $user->getID() ), + __METHOD__ + ); + + $result['notificationtimestamp'] = ( is_null( $timestamp ) ? '' : wfTimestamp( TS_ISO_8601, $timestamp ) ); + } else { + // First, log the invalid titles + foreach( $pageSet->getInvalidTitles() as $title ) { + $r = array(); + $r['title'] = $title; + $r['invalid'] = ''; + $result[] = $r; + } + foreach( $pageSet->getMissingPageIDs() as $p ) { + $page = array(); + $page['pageid'] = $p; + $page['missing'] = ''; + $page['notwatched'] = ''; + $result[] = $page; + } + foreach( $pageSet->getMissingRevisionIDs() as $r ) { + $rev = array(); + $rev['revid'] = $r; + $rev['missing'] = ''; + $rev['notwatched'] = ''; + $result[] = $rev; + } + + // Now process the valid titles + $lb = new LinkBatch( $pageSet->getTitles() ); + $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $timestamp ), + array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ), + __METHOD__ + ); + + // Query the results of our update + $timestamps = array(); + $res = $dbw->select( 'watchlist', array( 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ), + array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ), + __METHOD__ + ); + foreach ( $res as $row ) { + $timestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; + } + + // Now, put the valid titles into the result + foreach ( $pageSet->getTitles() as $title ) { + $ns = $title->getNamespace(); + $dbkey = $title->getDBkey(); + $r = array( + 'ns' => intval( $ns ), + 'title' => $title->getPrefixedText(), + ); + if ( !$title->exists() ) { + $r['missing'] = ''; + } + if ( isset( $timestamps[$ns] ) && array_key_exists( $dbkey, $timestamps[$ns] ) ) { + $r['notificationtimestamp'] = ''; + if ( $timestamps[$ns][$dbkey] !== null ) { + $r['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $timestamps[$ns][$dbkey] ); + } + } else { + $r['notwatched'] = ''; + } + $result[] = $r; + } + + $apiResult->setIndexedTagName( $result, 'page' ); + } + $apiResult->addValue( null, $this->getModuleName(), $result ); + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function needsToken() { + return true; + } + + public function getTokenSalt() { + return ''; + } + + public function getAllowedParams() { + $psModule = new ApiPageSet( $this ); + return $psModule->getAllowedParams() + array( + 'entirewatchlist' => array( + ApiBase::PARAM_TYPE => 'boolean' + ), + 'token' => null, + 'timestamp' => array( + ApiBase::PARAM_TYPE => 'timestamp' + ), + 'torevid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), + 'newerthanrevid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), + ); + } + + public function getParamDescription() { + $psModule = new ApiPageSet( $this ); + return $psModule->getParamDescription() + array( + 'entirewatchlist' => 'Work on all watched pages', + 'timestamp' => 'Timestamp to which to set the notification timestamp', + 'torevid' => 'Revision to set the notification timestamp to (one page only)', + 'newerthanrevid' => 'Revision to set the notification timestamp newer than (one page only)', + 'token' => 'A token previously acquired via prop=info', + ); + } + + public function getResultProperties() { + return array( + ApiBase::PROP_LIST => true, + ApiBase::PROP_ROOT => array( + 'notificationtimestamp' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ), + '' => array( + 'ns' => array( + ApiBase::PROP_TYPE => 'namespace', + ApiBase::PROP_NULLABLE => true + ), + 'title' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'pageid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'revid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'invalid' => 'boolean', + 'missing' => 'boolean', + 'notwatched' => 'boolean', + 'notificationtimestamp' => array( + ApiBase::PROP_TYPE => 'timestamp', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + + public function getDescription() { + return array( 'Update the notification timestamp for watched pages.', + 'This affects the highlighting of changed pages in the watchlist and history,', + 'and the sending of email when the "E-mail me when a page on my watchlist is', + 'changed" preference is enabled.' + ); + } + + public function getPossibleErrors() { + $psModule = new ApiPageSet( $this ); + return array_merge( + parent::getPossibleErrors(), + $psModule->getPossibleErrors(), + $this->getRequireMaxOneParameterErrorMessages( array( 'timestamp', 'torevid', 'newerthanrevid' ) ), + $this->getRequireOnlyOneParameterErrorMessages( array_merge( array( 'entirewatchlist' ), array_keys( $psModule->getAllowedParams() ) ) ), + array( + array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot use watchlist change notifications' ), + array( 'code' => 'multpages', 'info' => 'torevid may only be used with a single page' ), + array( 'code' => 'multpages', 'info' => 'newerthanrevid may only be used with a single page' ), + ) + ); + } + + public function getExamples() { + return array( + 'api.php?action=setnotificationtimestamp&entirewatchlist=&token=ABC123' => 'Reset the notification status for the entire watchlist', + 'api.php?action=setnotificationtimestamp&titles=Main_page&token=ABC123' => 'Reset the notification status for "Main page"', + 'api.php?action=setnotificationtimestamp&titles=Main_page×tamp=2012-01-01T00:00:00Z&token=ABC123' => 'Set the notification timestamp for "Main page" so all edits since 1 January 2012 are unviewed', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:SetNotificationTimestamp'; + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiTokens.php b/includes/api/ApiTokens.php new file mode 100644 index 00000000..2c9b482c --- /dev/null +++ b/includes/api/ApiTokens.php @@ -0,0 +1,158 @@ +<?php +/** + * + * + * Created on Jul 29, 2011 + * + * Copyright © 2011 John Du Hart john@johnduhart.me + * + * 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 API + */ +class ApiTokens extends ApiBase { + + public function __construct( $main, $action ) { + parent::__construct( $main, $action ); + } + + public function execute() { + wfProfileIn( __METHOD__ ); + $params = $this->extractRequestParams(); + $res = array(); + + $types = $this->getTokenTypes(); + foreach ( $params['type'] as $type ) { + $type = strtolower( $type ); + + $val = call_user_func( $types[$type], null, null ); + + if ( $val === false ) { + $this->setWarning( "Action '$type' is not allowed for the current user" ); + } else { + $res[$type . 'token'] = $val; + } + } + + $this->getResult()->addValue( null, $this->getModuleName(), $res ); + wfProfileOut( __METHOD__ ); + } + + private function getTokenTypes() { + static $types = null; + if ( $types ) { + return $types; + } + wfProfileIn( __METHOD__ ); + $types = array( 'patrol' => 'ApiQueryRecentChanges::getPatrolToken' ); + $names = array( 'edit', 'delete', 'protect', 'move', 'block', 'unblock', + 'email', 'import', 'watch', 'options' ); + foreach ( $names as $name ) { + $types[$name] = 'ApiQueryInfo::get' . ucfirst( $name ) . 'Token'; + } + wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) ); + ksort( $types ); + wfProfileOut( __METHOD__ ); + return $types; + } + + public function getAllowedParams() { + return array( + 'type' => array( + ApiBase::PARAM_DFLT => 'edit', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array_keys( $this->getTokenTypes() ), + ), + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'patroltoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'edittoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'deletetoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'protecttoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'movetoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'blocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'unblocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'emailtoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'importtoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'watchtoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'optionstoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + + public function getParamDescription() { + return array( + 'type' => 'Type of token(s) to request' + ); + } + + public function getDescription() { + return 'Gets tokens for data-modifying actions'; + } + + protected function getExamples() { + return array( + 'api.php?action=tokens' => 'Retrieve an edit token (the default)', + 'api.php?action=tokens&type=email|move' => 'Retrieve an email token and a move token' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index db94fd5b..e34771fc 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -44,7 +44,7 @@ class ApiUnblock extends ApiBase { $params = $this->extractRequestParams(); if ( $params['gettoken'] ) { - $res['unblocktoken'] = $user->getEditToken( '', $this->getMain()->getRequest() ); + $res['unblocktoken'] = $user->getEditToken(); $this->getResult()->addValue( null, $this->getModuleName(), $res ); return; } @@ -69,7 +69,7 @@ class ApiUnblock extends ApiBase { $data = array( 'Target' => is_null( $params['id'] ) ? $params['user'] : "#{$params['id']}", - 'Reason' => is_null( $params['reason'] ) ? '' : $params['reason'] + 'Reason' => $params['reason'] ); $block = Block::newFromTarget( $data['Target'] ); $retval = SpecialUnblock::processUnblock( $data, $this->getContext() ); @@ -78,7 +78,9 @@ class ApiUnblock extends ApiBase { } $res['id'] = $block->getId(); - $res['user'] = $block->getType() == Block::TYPE_AUTO ? '' : $block->getTarget(); + $target = $block->getType() == Block::TYPE_AUTO ? '' : $block->getTarget(); + $res['user'] = $target; + $res['userid'] = $target instanceof User ? $target->getId() : 0; $res['reason'] = $params['reason']; $this->getResult()->addValue( null, $this->getModuleName(), $res ); } @@ -98,8 +100,11 @@ class ApiUnblock extends ApiBase { ), 'user' => null, 'token' => null, - 'gettoken' => false, - 'reason' => null, + 'gettoken' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), + 'reason' => '', ); } @@ -108,9 +113,36 @@ class ApiUnblock extends ApiBase { return array( 'id' => "ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with {$p}user", 'user' => "Username, IP address or IP range you want to unblock. Cannot be used together with {$p}id", - 'token' => "An unblock token previously obtained through the gettoken parameter or {$p}prop=info", + 'token' => "An unblock token previously obtained through prop=info", 'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken', - 'reason' => 'Reason for unblock (optional)', + 'reason' => 'Reason for unblock', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'unblocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'id' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'user' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'userid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'reason' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) ); } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index d3429972..c9962517 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -4,7 +4,7 @@ * * Created on Jul 3, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -76,7 +76,7 @@ class ApiUndelete extends ApiBase { $info['title'] = $titleObj->getPrefixedText(); $info['revisions'] = intval( $retval[0] ); $info['fileversions'] = intval( $retval[1] ); - $info['reason'] = intval( $retval[2] ); + $info['reason'] = $retval[2]; $this->getResult()->addValue( null, $this->getModuleName(), $info ); } @@ -94,7 +94,10 @@ class ApiUndelete extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'reason' => '', 'timestamps' => array( ApiBase::PARAM_TYPE => 'timestamp', @@ -116,12 +119,23 @@ class ApiUndelete extends ApiBase { return array( 'title' => 'Title of the page you want to restore', 'token' => 'An undelete token previously retrieved through list=deletedrevs', - 'reason' => 'Reason for restoring (optional)', + 'reason' => 'Reason for restoring', 'timestamps' => 'Timestamps of the revisions to restore. If not set, all revisions will be restored.', 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', ); } + public function getResultProperties() { + return array( + '' => array( + 'title' => 'string', + 'revisions' => 'integer', + 'filerevisions' => 'integer', + 'reason' => 'string' + ) + ); + } + public function getDescription() { return array( 'Restore certain revisions of a deleted page. A list of deleted revisions (including timestamps) can be', diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index fdc1eff0..3a9b5c56 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -113,27 +113,30 @@ class ApiUpload extends ApiBase { } /** * Get an uplaod result based on upload context + * @return array */ private function getContextResult(){ $warnings = $this->getApiWarnings(); - if ( $warnings ) { + if ( $warnings && !$this->mParams['ignorewarnings'] ) { // Get warnings formated in result array format return $this->getWarningsResult( $warnings ); } elseif ( $this->mParams['chunk'] ) { // Add chunk, and get result - return $this->getChunkResult(); + return $this->getChunkResult( $warnings ); } elseif ( $this->mParams['stash'] ) { // Stash the file and get stash result - return $this->getStashResult(); + return $this->getStashResult( $warnings ); } // This is the most common case -- a normal upload with no warnings // performUpload will return a formatted properly for the API with status - return $this->performUpload(); + return $this->performUpload( $warnings ); } /** - * Get Stash Result, throws an expetion if the file could not be stashed. + * Get Stash Result, throws an expetion if the file could not be stashed. + * @param $warnings array Array of Api upload warnings + * @return array */ - private function getStashResult(){ + private function getStashResult( $warnings ){ $result = array (); // Some uploads can request they be stashed, so as not to publish them immediately. // In this case, a failure to stash ought to be fatal @@ -141,6 +144,9 @@ class ApiUpload extends ApiBase { $result['result'] = 'Success'; $result['filekey'] = $this->performStash(); $result['sessionkey'] = $result['filekey']; // backwards compatibility + if ( $warnings && count( $warnings ) > 0 ) { + $result['warnings'] = $warnings; + } } catch ( MWException $e ) { $this->dieUsage( $e->getMessage(), 'stashfailed' ); } @@ -148,7 +154,8 @@ class ApiUpload extends ApiBase { } /** * Get Warnings Result - * @param $warnings Array of Api upload warnings + * @param $warnings array Array of Api upload warnings + * @return array */ private function getWarningsResult( $warnings ){ $result = array(); @@ -165,12 +172,17 @@ class ApiUpload extends ApiBase { return $result; } /** - * Get the result of a chunk upload. + * Get the result of a chunk upload. + * @param $warnings array Array of Api upload warnings + * @return array */ - private function getChunkResult(){ + private function getChunkResult( $warnings ){ $result = array(); - + $result['result'] = 'Continue'; + if ( $warnings && count( $warnings ) > 0 ) { + $result['warnings'] = $warnings; + } $request = $this->getMain()->getRequest(); $chunkPath = $request->getFileTempname( 'chunk' ); $chunkSize = $request->getUpload( 'chunk' )->getSize(); @@ -181,17 +193,30 @@ class ApiUpload extends ApiBase { $this->mParams['offset']); if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); - return ; + return array(); } - $result['filekey'] = $this->mParams['filekey']; + // Check we added the last chunk: if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) { $status = $this->mUpload->concatenateChunks(); + if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); - return ; + return array(); } + + // We have a new filekey for the fully concatenated file. + $result['filekey'] = $this->mUpload->getLocalFile()->getFileKey(); + + // Remove chunk from stash. (Checks against user ownership of chunks.) + $this->mUpload->stash->removeFile( $this->mParams['filekey'] ); + $result['result'] = 'Success'; + + } else { + + // Continue passing through the filekey for adding further chunks. + $result['filekey'] = $this->mParams['filekey']; } } $result['offset'] = $this->mParams['offset'] + $chunkSize; @@ -318,6 +343,10 @@ class ApiUpload extends ApiBase { $this->dieUsageMsg( 'copyuploaddisabled' ); } + if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) { + $this->dieUsageMsg( 'copyuploadbaddomain' ); + } + $async = false; if ( $this->mParams['asyncdownload'] ) { $this->checkAsyncDownloadEnabled(); @@ -399,11 +428,21 @@ class ApiUpload extends ApiBase { break; case UploadBase::FILETYPE_BADTYPE: - $this->dieUsage( 'This type of file is banned', 'filetype-banned', - 0, array( - 'filetype' => $verification['finalExt'], - 'allowed' => $wgFileExtensions - ) ); + $extradata = array( + 'filetype' => $verification['finalExt'], + 'allowed' => $wgFileExtensions + ); + $this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' ); + + $msg = "Filetype not permitted: "; + if ( isset( $verification['blacklistedExt'] ) ) { + $msg .= join( ', ', $verification['blacklistedExt'] ); + $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] ); + $this->getResult()->setIndexedTagName( $extradata['blacklisted'], 'ext' ); + } else { + $msg .= $verification['finalExt']; + } + $this->dieUsage( $msg, 'filetype-banned', 0, $extradata ); break; case UploadBase::VERIFICATION_ERROR: $this->getResult()->setIndexedTagName( $verification['details'], 'detail' ); @@ -423,18 +462,15 @@ class ApiUpload extends ApiBase { /** - * Check warnings if ignorewarnings is not set. + * Check warnings. * Returns a suitable array for inclusion into API results if there were warnings * Returns the empty array if there were no warnings * * @return array */ protected function getApiWarnings() { - $warnings = array(); + $warnings = $this->mUpload->checkWarnings(); - if ( !$this->mParams['ignorewarnings'] ) { - $warnings = $this->mUpload->checkWarnings(); - } return $this->transformWarnings( $warnings ); } @@ -467,9 +503,10 @@ class ApiUpload extends ApiBase { * Perform the actual upload. Returns a suitable result array on success; * dies on failure. * + * @param $warnings array Array of Api upload warnings * @return array */ - protected function performUpload() { + protected function performUpload( $warnings ) { // Use comment as initial page text by default if ( is_null( $this->mParams['text'] ) ) { $this->mParams['text'] = $this->mParams['comment']; @@ -508,6 +545,9 @@ class ApiUpload extends ApiBase { $result['result'] = 'Success'; $result['filename'] = $file->getName(); + if ( $warnings && count( $warnings ) > 0 ) { + $result['warnings'] = $warnings; + } return $result; } @@ -539,7 +579,10 @@ class ApiUpload extends ApiBase { ApiBase::PARAM_DFLT => '' ), 'text' => null, - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'watch' => array( ApiBase::PARAM_DFLT => false, ApiBase::PARAM_DEPRECATED => true, @@ -602,6 +645,41 @@ class ApiUpload extends ApiBase { } + public function getResultProperties() { + return array( + '' => array( + 'result' => array( + ApiBase::PROP_TYPE => array( + 'Success', + 'Warning', + 'Continue', + 'Queued' + ), + ), + 'filekey' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'sessionkey' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'offset' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ), + 'statuskey' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'filename' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + public function getDescription() { return array( 'Upload a file, or get the status of pending uploads. Several methods are available:', @@ -631,6 +709,8 @@ class ApiUpload extends ApiBase { array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ), + array( 'fileexists-forbidden' ), + array( 'fileexists-shared-forbidden' ), ) ); } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index 191dd3ec..cbb66a41 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -5,7 +5,7 @@ * * Created on Mar 24, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<Firstname>.<Lastname>@gmail.com" * * 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 @@ -43,6 +43,7 @@ class ApiUserrights extends ApiBase { $form = new UserrightsPage; $r['user'] = $user->getName(); + $r['userid'] = $user->getId(); list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups( $user, (array)$params['add'], @@ -99,7 +100,10 @@ class ApiUserrights extends ApiBase { ApiBase::PARAM_TYPE => User::getAllGroups(), ApiBase::PARAM_ISMULTI => true ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'reason' => array( ApiBase::PARAM_DFLT => '' ) diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index fa382b3b..0509f1f8 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -4,7 +4,7 @@ * * Created on Jan 4, 2008 * - * Copyright © 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * Copyright © 2008 Yuri Astrakhan "<Firstname><Lastname>@gmail.com", * * 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 @@ -88,7 +88,10 @@ class ApiWatch extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'unwatch' => false, - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), ); } @@ -100,6 +103,17 @@ class ApiWatch extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'title' => 'string', + 'unwatched' => 'boolean', + 'watched' => 'boolean', + 'message' => 'string' + ) + ); + } + public function getDescription() { return 'Add or remove a page from/to the current user\'s watchlist'; } diff --git a/includes/cache/CacheDependency.php b/includes/cache/CacheDependency.php index 0df0cd89..a3c2b52a 100644 --- a/includes/cache/CacheDependency.php +++ b/includes/cache/CacheDependency.php @@ -1,11 +1,32 @@ <?php /** + * Data caching with dependencies. + * + * 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 + */ + +/** * This class stores an arbitrary value along with its dependencies. * Users should typically only use DependencyWrapper::getValueFromCache(), * rather than instantiating one of these objects directly. * @ingroup Cache */ - class DependencyWrapper { var $value; var $deps; diff --git a/includes/cache/FileCacheBase.php b/includes/cache/FileCacheBase.php index 37401655..c0c5609c 100644 --- a/includes/cache/FileCacheBase.php +++ b/includes/cache/FileCacheBase.php @@ -1,9 +1,31 @@ <?php /** - * Contain the FileCacheBase class + * Data storage in the file system. + * + * 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 */ + +/** + * Base class for data storage in the file system. + * + * @ingroup Cache + */ abstract class FileCacheBase { protected $mKey; protected $mType = 'object'; @@ -74,7 +96,7 @@ abstract class FileCacheBase { /** * Get the last-modified timestamp of the cache file - * @return string|false TS_MW timestamp + * @return string|bool TS_MW timestamp */ public function cacheTimestamp() { $timestamp = filemtime( $this->cachePath() ); @@ -116,9 +138,12 @@ abstract class FileCacheBase { * @return string */ public function fetchText() { - // gzopen can transparently read from gziped or plain text - $fh = gzopen( $this->cachePath(), 'rb' ); - return stream_get_contents( $fh ); + if( $this->useGzip() ) { + $fh = gzopen( $this->cachePath(), 'rb' ); + return stream_get_contents( $fh ); + } else { + return file_get_contents( $this->cachePath() ); + } } /** diff --git a/includes/cache/GenderCache.php b/includes/cache/GenderCache.php index 342f8dba..2a169bb3 100644 --- a/includes/cache/GenderCache.php +++ b/includes/cache/GenderCache.php @@ -1,8 +1,30 @@ <?php - /** * Caches user genders when needed to use correct namespace aliases. + * + * 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 * @author Niklas Laxström + * @ingroup Cache + */ + +/** + * Caches user genders when needed to use correct namespace aliases. + * * @since 1.18 */ class GenderCache { @@ -37,14 +59,18 @@ class GenderCache { /** * Returns the gender for given username. - * @param $username String: username + * @param $username String or User: username * @param $caller String: the calling method * @return String */ public function getGenderOf( $username, $caller = '' ) { global $wgUser; - $username = strtr( $username, '_', ' ' ); + if( $username instanceof User ) { + $username = $username->getName(); + } + + $username = self::normalizeUsername( $username ); if ( !isset( $this->cache[$username] ) ) { if ( $this->misses >= $this->missLimit && $wgUser->getName() !== $username ) { @@ -56,11 +82,7 @@ class GenderCache { } else { $this->misses++; - if ( !User::isValidUserName( $username ) ) { - $this->cache[$username] = $this->getDefault(); - } else { - $this->doQuery( $username, $caller ); - } + $this->doQuery( $username, $caller ); } } @@ -82,7 +104,6 @@ class GenderCache { foreach ( $data as $ns => $pagenames ) { if ( !MWNamespace::hasGenderDistinction( $ns ) ) continue; foreach ( array_keys( $pagenames ) as $username ) { - if ( isset( $this->cache[$username] ) ) continue; $users[$username] = true; } } @@ -91,6 +112,29 @@ class GenderCache { } /** + * Wrapper for doQuery that processes a title or string array. + * + * @since 1.20 + * @param $titles List: array of Title objects or strings + * @param $caller String: the calling method + */ + public function doTitlesArray( $titles, $caller = '' ) { + $users = array(); + foreach ( $titles as $title ) { + $titleObj = is_string( $title ) ? Title::newFromText( $title ) : $title; + if ( !$titleObj ) { + continue; + } + if ( !MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) { + continue; + } + $users[] = $titleObj->getText(); + } + + $this->doQuery( $users, $caller ); + } + + /** * Preloads genders for given list of users. * @param $users List|String: usernames * @param $caller String: the calling method @@ -98,26 +142,28 @@ class GenderCache { public function doQuery( $users, $caller = '' ) { $default = $this->getDefault(); - foreach ( (array) $users as $index => $value ) { - $name = strtr( $value, '_', ' ' ); - if ( isset( $this->cache[$name] ) ) { - // Skip users whose gender setting we already know - unset( $users[$index] ); - } else { - $users[$index] = $name; + $usersToCheck = array(); + foreach ( (array) $users as $value ) { + $name = self::normalizeUsername( $value ); + // Skip users whose gender setting we already know + if ( !isset( $this->cache[$name] ) ) { // For existing users, this value will be overwritten by the correct value $this->cache[$name] = $default; + // query only for valid names, which can be in the database + if( User::isValidUserName( $name ) ) { + $usersToCheck[] = $name; + } } } - if ( count( $users ) === 0 ) { + if ( count( $usersToCheck ) === 0 ) { return; } $dbr = wfGetDB( DB_SLAVE ); $table = array( 'user', 'user_properties' ); $fields = array( 'user_name', 'up_value' ); - $conds = array( 'user_name' => $users ); + $conds = array( 'user_name' => $usersToCheck ); $joins = array( 'user_properties' => array( 'LEFT JOIN', array( 'user_id = up_user', 'up_property' => 'gender' ) ) ); @@ -125,11 +171,20 @@ class GenderCache { if ( strval( $caller ) !== '' ) { $comment .= "/$caller"; } - $res = $dbr->select( $table, $fields, $conds, $comment, $joins, $joins ); + $res = $dbr->select( $table, $fields, $conds, $comment, array(), $joins ); foreach ( $res as $row ) { $this->cache[$row->user_name] = $row->up_value ? $row->up_value : $default; } } + private static function normalizeUsername( $username ) { + // Strip off subpages + $indexSlash = strpos( $username, '/' ); + if ( $indexSlash !== false ) { + $username = substr( $username, 0, $indexSlash ); + } + // normalize underscore/spaces + return strtr( $username, '_', ' ' ); + } } diff --git a/includes/cache/HTMLCacheUpdate.php b/includes/cache/HTMLCacheUpdate.php index 11e2ae74..0a3c0023 100644 --- a/includes/cache/HTMLCacheUpdate.php +++ b/includes/cache/HTMLCacheUpdate.php @@ -1,4 +1,25 @@ <?php +/** + * HTML cache invalidation of all pages linking to a given title. + * + * 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 + */ /** * Class to invalidate the HTML cache of all the pages linking to a given title. diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php index 92130f69..6bfeed32 100644 --- a/includes/cache/HTMLFileCache.php +++ b/includes/cache/HTMLFileCache.php @@ -1,9 +1,33 @@ <?php /** - * Contain the HTMLFileCache class + * Page view caching in the file system. + * + * 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 */ + +/** + * Page view caching in the file system. + * The only cacheable actions are "view" and "history". Also special pages + * will not be cached. + * + * @ingroup Cache + */ class HTMLFileCache extends FileCacheBase { /** * Construct an ObjectFileCache from a Title and an action @@ -105,19 +129,22 @@ class HTMLFileCache extends FileCacheBase { wfDebug( __METHOD__ . "()\n"); $filename = $this->cachePath(); + $context->getOutput()->sendCacheControl(); header( "Content-Type: $wgMimeType; charset=UTF-8" ); header( "Content-Language: $wgLanguageCode" ); if ( $this->useGzip() ) { if ( wfClientAcceptsGzip() ) { header( 'Content-Encoding: gzip' ); + readfile( $filename ); } else { /* Send uncompressed */ + wfDebug( __METHOD__ . " uncompressing cache file and sending it\n" ); readgzfile( $filename ); - return; } + } else { + readfile( $filename ); } - readfile( $filename ); $context->getOutput()->disable(); // tell $wgOut that output is taken care of } diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php index 17e8739b..372f983b 100644 --- a/includes/cache/LinkBatch.php +++ b/includes/cache/LinkBatch.php @@ -1,4 +1,25 @@ <?php +/** + * Batch query to determine page existence. + * + * 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 + */ /** * Class representing a list of titles @@ -45,6 +66,11 @@ class LinkBatch { } } + /** + * @param $ns int + * @param $dbkey string + * @return mixed + */ public function add( $ns, $dbkey ) { if ( $ns < 0 ) { return; @@ -190,7 +216,7 @@ class LinkBatch { } $genderCache = GenderCache::singleton(); - $genderCache->dolinkBatch( $this->data, $this->caller ); + $genderCache->doLinkBatch( $this->data, $this->caller ); return true; } diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index a73eaaa4..f759c020 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -1,5 +1,27 @@ <?php /** + * Page existence cache. + * + * 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 + */ + +/** * Cache for article titles (prefixed DB keys) and ids linked from one source * * @ingroup Cache diff --git a/includes/cache/MemcachedSessions.php b/includes/cache/MemcachedSessions.php deleted file mode 100644 index 36733595..00000000 --- a/includes/cache/MemcachedSessions.php +++ /dev/null @@ -1,98 +0,0 @@ -<?php -/** - * This file gets included if $wgSessionsInMemcache is set in the config. - * It redirects session handling functions to store their data in memcached - * instead of the local filesystem. Depending on circumstances, it may also - * be necessary to change the cookie settings to work across hostnames. - * See: http://www.php.net/manual/en/function.session-set-save-handler.php - * - * @file - * @ingroup Cache - */ - -/** - * Get a cache key for the given session id. - * - * @param $id String: session id - * @return String: cache key - */ -function memsess_key( $id ) { - return wfMemcKey( 'session', $id ); -} - -/** - * Callback when opening a session. - * NOP: $wgMemc should be set up already. - * - * @param $save_path String: path used to store session files, unused - * @param $session_name String: session name - * @return Boolean: success - */ -function memsess_open( $save_path, $session_name ) { - return true; -} - -/** - * Callback when closing a session. - * NOP. - * - * @return Boolean: success - */ -function memsess_close() { - return true; -} - -/** - * Callback when reading session data. - * - * @param $id String: session id - * @return Mixed: session data - */ -function memsess_read( $id ) { - global $wgMemc; - $data = $wgMemc->get( memsess_key( $id ) ); - if( ! $data ) return ''; - return $data; -} - -/** - * Callback when writing session data. - * - * @param $id String: session id - * @param $data Mixed: session data - * @return Boolean: success - */ -function memsess_write( $id, $data ) { - global $wgMemc; - $wgMemc->set( memsess_key( $id ), $data, 3600 ); - return true; -} - -/** - * Callback to destroy a session when calling session_destroy(). - * - * @param $id String: session id - * @return Boolean: success - */ -function memsess_destroy( $id ) { - global $wgMemc; - - $wgMemc->delete( memsess_key( $id ) ); - return true; -} - -/** - * Callback to execute garbage collection. - * NOP: Memcached performs garbage collection. - * - * @param $maxlifetime Integer: maximum session life time - * @return Boolean: success - */ -function memsess_gc( $maxlifetime ) { - return true; -} - -function memsess_write_close() { - session_write_close(); -} - diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 146edd28..b854a2ec 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -1,5 +1,22 @@ <?php /** + * Localisation messages cache. + * + * 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 */ @@ -115,6 +132,7 @@ class MessageCache { function getParserOptions() { if ( !$this->mParserOptions ) { $this->mParserOptions = new ParserOptions; + $this->mParserOptions->setEditSection( false ); } return $this->mParserOptions; } @@ -126,7 +144,7 @@ class MessageCache { * * @param $hash String: the hash of contents, to check validity. * @param $code Mixed: Optional language code, see documenation of load(). - * @return false on failure. + * @return bool on failure. */ function loadFromLocal( $hash, $code ) { global $wgCacheDirectory, $wgLocalMessageCacheSerialized; @@ -260,6 +278,7 @@ class MessageCache { * is disabled. * * @param $code String: language to which load messages + * @return bool */ function load( $code = false ) { global $wgUseLocalMessageCache; @@ -496,7 +515,7 @@ class MessageCache { if ( $code === 'en' ) { // Delete all sidebars, like for example on action=purge on the // sidebar messages - $codes = array_keys( Language::getLanguageNames() ); + $codes = array_keys( Language::fetchLanguageNames() ); } global $wgMemc; @@ -520,7 +539,7 @@ class MessageCache { * @param $cache Array: cached messages with a version. * @param $memc Bool: Wether to update or not memcache. * @param $code String: Language code. - * @return False on somekind of error. + * @return bool on somekind of error. */ protected function saveToCaches( $cache, $memc = true, $code = false ) { wfProfileIn( __METHOD__ ); @@ -588,7 +607,7 @@ class MessageCache { * @param $isFullKey Boolean: specifies whether $key is a two part key * "msg/lang". * - * @return string|false + * @return string|bool */ function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { global $wgLanguageCode, $wgContLang; @@ -696,7 +715,7 @@ class MessageCache { * @param $title String: Message cache key with initial uppercase letter. * @param $code String: code denoting the language to try. * - * @return string|false + * @return string|bool False on failure */ function getMsgFromNamespace( $title, $code ) { global $wgAdaptiveMessageCache; @@ -747,12 +766,14 @@ class MessageCache { } # Try loading it from the database - $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); + $revision = Revision::newFromTitle( + Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST + ); if ( $revision ) { $message = $revision->getText(); if ($message === false) { // A possibly temporary loading failure. - wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$title->getDbKey()} ($code)" ); + wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$title} ($code)" ); } else { $this->mCache[$code][$title] = ' ' . $message; $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry ); @@ -868,7 +889,7 @@ class MessageCache { * Clear all stored messages. Mainly used after a mass rebuild. */ function clear() { - $langs = Language::getLanguageNames( false ); + $langs = Language::fetchLanguageNames( null, 'mw' ); foreach ( array_keys($langs) as $code ) { # Global cache $this->mMemc->delete( wfMemcKey( 'messages', $code ) ); @@ -890,8 +911,7 @@ class MessageCache { } $lang = array_pop( $pieces ); - $validCodes = Language::getLanguageNames(); - if( !array_key_exists( $lang, $validCodes ) ) { + if( !Language::fetchLanguageName( $lang, null, 'mw' ) ) { return array( $key, $wgLanguageCode ); } diff --git a/includes/cache/ObjectFileCache.php b/includes/cache/ObjectFileCache.php index 3356f1fc..ed1e49a6 100644 --- a/includes/cache/ObjectFileCache.php +++ b/includes/cache/ObjectFileCache.php @@ -1,9 +1,31 @@ <?php /** - * Contain the ObjectFileCache class + * Object cache in the file system. + * + * 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 */ + +/** + * Object cache in the file system. + * + * @ingroup Cache + */ class ObjectFileCache extends FileCacheBase { /** * Construct an ObjectFileCache from a key and a type diff --git a/includes/cache/ProcessCacheLRU.php b/includes/cache/ProcessCacheLRU.php new file mode 100644 index 00000000..f215ebd8 --- /dev/null +++ b/includes/cache/ProcessCacheLRU.php @@ -0,0 +1,120 @@ +<?php +/** + * Per-process memory cache for storing items. + * + * 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 + */ + +/** + * Handles per process caching of items + * @ingroup Cache + */ +class ProcessCacheLRU { + /** @var Array */ + protected $cache = array(); // (key => prop => value) + + protected $maxCacheKeys; // integer; max entries + + /** + * @param $maxKeys integer Maximum number of entries allowed (min 1). + * @throws MWException When $maxCacheKeys is not an int or =< 0. + */ + public function __construct( $maxKeys ) { + if ( !is_int( $maxKeys ) || $maxKeys < 1 ) { + throw new MWException( __METHOD__ . " must be given an integer and >= 1" ); + } + $this->maxCacheKeys = $maxKeys; + } + + /** + * Set a property field for a cache entry. + * This will prune the cache if it gets too large. + * If the item is already set, it will be pushed to the top of the cache. + * + * @param $key string + * @param $prop string + * @param $value mixed + * @return void + */ + public function set( $key, $prop, $value ) { + if ( isset( $this->cache[$key] ) ) { + $this->ping( $key ); // push to top + } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) { + reset( $this->cache ); + unset( $this->cache[key( $this->cache )] ); + } + $this->cache[$key][$prop] = $value; + } + + /** + * Check if a property field exists for a cache entry. + * + * @param $key string + * @param $prop string + * @return bool + */ + public function has( $key, $prop ) { + return isset( $this->cache[$key][$prop] ); + } + + /** + * Get a property field for a cache entry. + * This returns null if the property is not set. + * If the item is already set, it will be pushed to the top of the cache. + * + * @param $key string + * @param $prop string + * @return mixed + */ + public function get( $key, $prop ) { + if ( isset( $this->cache[$key][$prop] ) ) { + $this->ping( $key ); // push to top + return $this->cache[$key][$prop]; + } else { + return null; + } + } + + /** + * Clear one or several cache entries, or all cache entries + * + * @param $keys string|Array + * @return void + */ + public function clear( $keys = null ) { + if ( $keys === null ) { + $this->cache = array(); + } else { + foreach ( (array)$keys as $key ) { + unset( $this->cache[$key] ); + } + } + } + + /** + * Push an entry to the top of the cache + * + * @param $key string + */ + protected function ping( $key ) { + $item = $this->cache[$key]; + unset( $this->cache[$key] ); + $this->cache[$key] = $item; + } +} diff --git a/includes/cache/ResourceFileCache.php b/includes/cache/ResourceFileCache.php index e73fc2d7..61f1e8c3 100644 --- a/includes/cache/ResourceFileCache.php +++ b/includes/cache/ResourceFileCache.php @@ -1,9 +1,31 @@ <?php /** - * Contain the ResourceFileCache class + * Resource loader request result caching in the file system. + * + * 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 */ + +/** + * Resource loader request result caching in the file system. + * + * @ingroup Cache + */ class ResourceFileCache extends FileCacheBase { protected $mCacheWorthy; diff --git a/includes/cache/SquidUpdate.php b/includes/cache/SquidUpdate.php index d47b5b5e..423e3884 100644 --- a/includes/cache/SquidUpdate.php +++ b/includes/cache/SquidUpdate.php @@ -1,6 +1,22 @@ <?php /** - * See deferred.txt + * Squid cache purging. + * + * 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 */ @@ -12,13 +28,18 @@ class SquidUpdate { var $urlArr, $mMaxTitles; - function __construct( $urlArr = Array(), $maxTitles = false ) { + /** + * @param $urlArr array + * @param $maxTitles bool|int + */ + function __construct( $urlArr = array(), $maxTitles = false ) { global $wgMaxSquidPurgeTitles; if ( $maxTitles === false ) { $this->mMaxTitles = $wgMaxSquidPurgeTitles; } else { $this->mMaxTitles = $maxTitles; } + $urlArr = array_unique( $urlArr ); // Remove duplicates if ( count( $urlArr ) > $this->mMaxTitles ) { $urlArr = array_slice( $urlArr, 0, $this->mMaxTitles ); } @@ -102,23 +123,19 @@ class SquidUpdate { * @return void */ static function purge( $urlArr ) { - global $wgSquidServers, $wgHTCPMulticastAddress, $wgHTCPPort; - - /*if ( (@$wgSquidServers[0]) == 'echo' ) { - echo implode("<br />\n", $urlArr) . "<br />\n"; - return; - }*/ + global $wgSquidServers, $wgHTCPMulticastRouting; if( !$urlArr ) { return; } - if ( $wgHTCPMulticastAddress && $wgHTCPPort ) { + if ( $wgHTCPMulticastRouting ) { SquidUpdate::HTCPPurge( $urlArr ); } wfProfileIn( __METHOD__ ); + $urlArr = array_unique( $urlArr ); // Remove duplicates $maxSocketsPerSquid = 8; // socket cap per Squid $urlsPerSocket = 400; // 400 seems to be a good tradeoff, opening a socket takes a while $socketsPerSquid = ceil( count( $urlArr ) / $urlsPerSocket ); @@ -147,7 +164,7 @@ class SquidUpdate { * @param $urlArr array */ static function HTCPPurge( $urlArr ) { - global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort; + global $wgHTCPMulticastRouting, $wgHTCPMulticastTTL; wfProfileIn( __METHOD__ ); $htcpOpCLR = 4; // HTCP CLR @@ -168,11 +185,20 @@ class SquidUpdate { socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_TTL, $wgHTCPMulticastTTL ); + $urlArr = array_unique( $urlArr ); // Remove duplicates foreach ( $urlArr as $url ) { if( !is_string( $url ) ) { throw new MWException( 'Bad purge URL' ); } $url = SquidUpdate::expand( $url ); + $conf = self::getRuleForURL( $url, $wgHTCPMulticastRouting ); + if ( !$conf ) { + wfDebug( "No HTCP rule configured for URL $url , skipping\n" ); + continue; + } + if ( !isset( $conf['host'] ) || !isset( $conf['port'] ) ) { + throw new MWException( "Invalid HTCP rule for URL $url\n" ); + } // Construct a minimal HTCP request diagram // as per RFC 2756 @@ -196,7 +222,7 @@ class SquidUpdate { // Send out wfDebug( "Purging URL $url via HTCP\n" ); socket_sendto( $conn, $htcpPacket, $htcpLen, 0, - $wgHTCPMulticastAddress, $wgHTCPPort ); + $conf['host'], $conf['port'] ); } } else { $errstr = socket_strerror( socket_last_error() ); @@ -223,4 +249,20 @@ class SquidUpdate { static function expand( $url ) { return wfExpandUrl( $url, PROTO_INTERNAL ); } + + /** + * Find the HTCP routing rule to use for a given URL. + * @param $url string URL to match + * @param $rules array Array of rules, see $wgHTCPMulticastRouting for format and behavior + * @return mixed Element of $rules that matched, or false if nothing matched + */ + static function getRuleForURL( $url, $rules ) { + foreach ( $rules as $regex => $routing ) { + if ( $regex === '' || preg_match( $regex, $url ) ) { + return $routing; + } + } + return false; + } + } diff --git a/includes/cache/UserCache.php b/includes/cache/UserCache.php new file mode 100644 index 00000000..6ec23669 --- /dev/null +++ b/includes/cache/UserCache.php @@ -0,0 +1,134 @@ +<?php +/** + * Caches current user names and other info based on user IDs. + * + * 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 + */ + +/** + * @since 1.20 + */ +class UserCache { + protected $cache = array(); // (uid => property => value) + protected $typesCached = array(); // (uid => cache type => 1) + + /** + * @return UserCache + */ + public static function singleton() { + static $instance = null; + if ( $instance === null ) { + $instance = new self(); + } + return $instance; + } + + protected function __construct() {} + + /** + * Get a property of a user based on their user ID + * + * @param $userId integer User ID + * @param $prop string User property + * @return mixed The property or false if the user does not exist + */ + public function getProp( $userId, $prop ) { + if ( !isset( $this->cache[$userId][$prop] ) ) { + wfDebug( __METHOD__ . ": querying DB for prop '$prop' for user ID '$userId'.\n" ); + $this->doQuery( array( $userId ) ); // cache miss + } + return isset( $this->cache[$userId][$prop] ) + ? $this->cache[$userId][$prop] + : false; // user does not exist? + } + + /** + * Preloads user names for given list of users. + * @param $userIds Array List of user IDs + * @param $options Array Option flags; include 'userpage' and 'usertalk' + * @param $caller String: the calling method + */ + public function doQuery( array $userIds, $options = array(), $caller = '' ) { + wfProfileIn( __METHOD__ ); + + $usersToCheck = array(); + $usersToQuery = array(); + + foreach ( $userIds as $userId ) { + $userId = (int)$userId; + if ( $userId <= 0 ) { + continue; // skip anons + } + if ( isset( $this->cache[$userId]['name'] ) ) { + $usersToCheck[$userId] = $this->cache[$userId]['name']; // already have name + } else { + $usersToQuery[] = $userId; // we need to get the name + } + } + + // Lookup basic info for users not yet loaded... + if ( count( $usersToQuery ) ) { + $dbr = wfGetDB( DB_SLAVE ); + $table = array( 'user' ); + $conds = array( 'user_id' => $usersToQuery ); + $fields = array( 'user_name', 'user_real_name', 'user_registration', 'user_id' ); + + $comment = __METHOD__; + if ( strval( $caller ) !== '' ) { + $comment .= "/$caller"; + } + + $res = $dbr->select( $table, $fields, $conds, $comment ); + foreach ( $res as $row ) { // load each user into cache + $userId = (int)$row->user_id; + $this->cache[$userId]['name'] = $row->user_name; + $this->cache[$userId]['real_name'] = $row->user_real_name; + $this->cache[$userId]['registration'] = $row->user_registration; + $usersToCheck[$userId] = $row->user_name; + } + } + + $lb = new LinkBatch(); + foreach ( $usersToCheck as $userId => $name ) { + if ( $this->queryNeeded( $userId, 'userpage', $options ) ) { + $lb->add( NS_USER, str_replace( ' ', '_', $row->user_name ) ); + $this->typesCached[$userId]['userpage'] = 1; + } + if ( $this->queryNeeded( $userId, 'usertalk', $options ) ) { + $lb->add( NS_USER_TALK, str_replace( ' ', '_', $row->user_name ) ); + $this->typesCached[$userId]['usertalk'] = 1; + } + } + $lb->execute(); + + wfProfileOut( __METHOD__ ); + } + + /** + * Check if a cache type is in $options and was not loaded for this user + * + * @param $uid integer user ID + * @param $type string Cache type + * @param $options Array Requested cache types + * @return bool + */ + protected function queryNeeded( $uid, $type, array $options ) { + return ( in_array( $type, $options ) && !isset( $this->typesCached[$uid][$type] ) ); + } +} diff --git a/includes/dao/IDBAccessObject.php b/includes/dao/IDBAccessObject.php new file mode 100644 index 00000000..e30522a5 --- /dev/null +++ b/includes/dao/IDBAccessObject.php @@ -0,0 +1,55 @@ +<?php +/** + * This file contains database access object related constants. + * + * 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 Database + */ + +/** + * Interface for database access objects. + * + * Classes using this support a set of constants in a bitfield argument to their data loading + * functions. In general, objects should assume READ_NORMAL if no flags are explicitly given, + * though certain objects may assume READ_LATEST for common use case or legacy reasons. + * + * There are three types of reads: + * - READ_NORMAL : Potentially cached read of data (e.g. from a slave or stale replica) + * - READ_LATEST : Up-to-date read as of transaction start (e.g. from master or a quorum read) + * - READ_LOCKING : Up-to-date read as of now, that locks the records for the transaction + * + * Callers should use READ_NORMAL (or pass in no flags) unless the read determines a write. + * In theory, such cases may require READ_LOCKING, though to avoid contention, READ_LATEST is + * often good enough. If UPDATE race condition checks are required on a row and expensive code + * must run after the row is fetched to determine the UPDATE, it may help to do something like: + * - a) Read the current row + * - b) Determine the new row (expensive, so we don't want to hold locks now) + * - c) Re-read the current row with READ_LOCKING; if it changed then bail out + * - d) otherwise, do the updates + */ +interface IDBAccessObject { + // Constants for object loading bitfield flags (higher => higher QoS) + const READ_LATEST = 1; // read from the master + const READ_LOCKING = 3; // READ_LATEST and "FOR UPDATE" + + // Convenience constant for callers to explicitly request slave data + const READ_NORMAL = 0; // read from the slave + + // Convenience constant for tracking how data was loaded (higher => higher QoS) + const READ_NONE = -1; // not loaded yet (or the object was cleared) +} diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index bd0895cf..4e43642f 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -20,6 +20,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @file * @ingroup Database */ diff --git a/includes/db/Database.php b/includes/db/Database.php index f3e84675..5f10b97d 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -2,10 +2,26 @@ /** * @defgroup Database Database * + * This file deals with database interface functions + * and query specifics/optimisations. + * + * 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 Database - * This file deals with database interface functions - * and query specifics/optimisations */ /** Number of times to re-try an operation in case of deadlock */ @@ -209,12 +225,15 @@ abstract class DatabaseBase implements DatabaseType { protected $mServer, $mUser, $mPassword, $mDBname; - /** - * @var DatabaseBase - */ protected $mConn = null; protected $mOpened = false; + /** + * @since 1.20 + * @var array of Closure + */ + protected $mTrxIdleCallbacks = array(); + protected $mTablePrefix; protected $mFlags; protected $mTrxLevel = 0; @@ -253,9 +272,9 @@ abstract class DatabaseBase implements DatabaseType { * - false to disable debugging * - omitted or null to do nothing * - * @return The previous value of the flag + * @return bool|null previous value of the flag */ - function debug( $debug = null ) { + public function debug( $debug = null ) { return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); } @@ -279,9 +298,9 @@ abstract class DatabaseBase implements DatabaseType { * * @param $buffer null|bool * - * @return The previous value of the flag + * @return null|bool The previous value of the flag */ - function bufferResults( $buffer = null ) { + public function bufferResults( $buffer = null ) { if ( is_null( $buffer ) ) { return !(bool)( $this->mFlags & DBO_NOBUFFER ); } else { @@ -300,7 +319,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool The previous value of the flag. */ - function ignoreErrors( $ignoreErrors = null ) { + public function ignoreErrors( $ignoreErrors = null ) { return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); } @@ -310,28 +329,28 @@ abstract class DatabaseBase implements DatabaseType { * Historically, transactions were allowed to be "nested". This is no * longer supported, so this function really only returns a boolean. * - * @param $level An integer (0 or 1), or omitted to leave it unchanged. - * @return The previous value + * @param $level int An integer (0 or 1), or omitted to leave it unchanged. + * @return int The previous value */ - function trxLevel( $level = null ) { + public function trxLevel( $level = null ) { return wfSetVar( $this->mTrxLevel, $level ); } /** * Get/set the number of errors logged. Only useful when errors are ignored - * @param $count The count to set, or omitted to leave it unchanged. - * @return The error count + * @param $count int The count to set, or omitted to leave it unchanged. + * @return int The error count */ - function errorCount( $count = null ) { + public function errorCount( $count = null ) { return wfSetVar( $this->mErrorCount, $count ); } /** * Get/set the table prefix. - * @param $prefix The table prefix to set, or omitted to leave it unchanged. - * @return The previous table prefix. + * @param $prefix string The table prefix to set, or omitted to leave it unchanged. + * @return string The previous table prefix. */ - function tablePrefix( $prefix = null ) { + public function tablePrefix( $prefix = null ) { return wfSetVar( $this->mTablePrefix, $prefix ); } @@ -344,7 +363,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return LoadBalancer|null */ - function getLBInfo( $name = null ) { + public function getLBInfo( $name = null ) { if ( is_null( $name ) ) { return $this->mLBInfo; } else { @@ -364,7 +383,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $name * @param $value */ - function setLBInfo( $name, $value = null ) { + public function setLBInfo( $name, $value = null ) { if ( is_null( $value ) ) { $this->mLBInfo = $name; } else { @@ -377,7 +396,7 @@ abstract class DatabaseBase implements DatabaseType { * * @param $lag int */ - function setFakeSlaveLag( $lag ) { + public function setFakeSlaveLag( $lag ) { $this->mFakeSlaveLag = $lag; } @@ -386,7 +405,7 @@ abstract class DatabaseBase implements DatabaseType { * * @param $enabled bool */ - function setFakeMaster( $enabled = true ) { + public function setFakeMaster( $enabled = true ) { $this->mFakeMaster = $enabled; } @@ -395,7 +414,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function cascadingDeletes() { + public function cascadingDeletes() { return false; } @@ -404,7 +423,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function cleanupTriggers() { + public function cleanupTriggers() { return false; } @@ -414,7 +433,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function strictIPs() { + public function strictIPs() { return false; } @@ -423,7 +442,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function realTimestamps() { + public function realTimestamps() { return false; } @@ -432,7 +451,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function implicitGroupby() { + public function implicitGroupby() { return true; } @@ -442,17 +461,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function implicitOrderby() { - return true; - } - - /** - * Returns true if this database requires that SELECT DISTINCT queries require that all - ORDER BY expressions occur in the SELECT list per the SQL92 standard - * - * @return bool - */ - function standardSelectDistinct() { + public function implicitOrderby() { return true; } @@ -462,7 +471,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function searchableIPs() { + public function searchableIPs() { return false; } @@ -471,7 +480,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function functionalIndexes() { + public function functionalIndexes() { return false; } @@ -479,7 +488,7 @@ abstract class DatabaseBase implements DatabaseType { * Return the last query that went through DatabaseBase::query() * @return String */ - function lastQuery() { + public function lastQuery() { return $this->mLastQuery; } @@ -489,15 +498,25 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function doneWrites() { + public function doneWrites() { return $this->mDoneWrites; } /** + * Returns true if there is a transaction open with possible write + * queries or transaction idle callbacks waiting on it to finish. + * + * @return bool + */ + public function writesOrCallbacksPending() { + return $this->mTrxLevel && ( $this->mDoneWrites || $this->mTrxIdleCallbacks ); + } + + /** * Is a connection to the database open? * @return Boolean */ - function isOpen() { + public function isOpen() { return $this->mOpened; } @@ -513,8 +532,12 @@ abstract class DatabaseBase implements DatabaseType { * and removes it in command line mode * - DBO_PERSISTENT: use persistant database connection */ - function setFlag( $flag ) { + public function setFlag( $flag ) { + global $wgDebugDBTransactions; $this->mFlags |= $flag; + if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) { + wfDebug("Implicit transactions are now disabled.\n"); + } } /** @@ -522,8 +545,12 @@ abstract class DatabaseBase implements DatabaseType { * * @param $flag: same as setFlag()'s $flag param */ - function clearFlag( $flag ) { + public function clearFlag( $flag ) { + global $wgDebugDBTransactions; $this->mFlags &= ~$flag; + if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) { + wfDebug("Implicit transactions are now disabled.\n"); + } } /** @@ -532,7 +559,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $flag: same as setFlag()'s $flag param * @return Boolean */ - function getFlag( $flag ) { + public function getFlag( $flag ) { return !!( $this->mFlags & $flag ); } @@ -543,14 +570,14 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function getProperty( $name ) { + public function getProperty( $name ) { return $this->$name; } /** * @return string */ - function getWikiID() { + public function getWikiID() { if ( $this->mTablePrefix ) { return "{$this->mDBname}-{$this->mTablePrefix}"; } else { @@ -588,15 +615,21 @@ abstract class DatabaseBase implements DatabaseType { function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0, $tablePrefix = 'get from global' ) { - global $wgDBprefix, $wgCommandLineMode; + global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions; $this->mFlags = $flags; if ( $this->mFlags & DBO_DEFAULT ) { if ( $wgCommandLineMode ) { $this->mFlags &= ~DBO_TRX; + if ( $wgDebugDBTransactions ) { + wfDebug("Implicit transaction open disabled.\n"); + } } else { $this->mFlags |= DBO_TRX; + if ( $wgDebugDBTransactions ) { + wfDebug("Implicit transaction open enabled.\n"); + } } } @@ -622,35 +655,6 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Same as new DatabaseMysql( ... ), kept for backward compatibility - * @deprecated since 1.17 - * - * @param $server - * @param $user - * @param $password - * @param $dbName - * @param $flags int - * @return DatabaseMysql - */ - static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { - wfDeprecated( __METHOD__, '1.17' ); - return new DatabaseMysql( $server, $user, $password, $dbName, $flags ); - } - - /** - * Same as new factory( ... ), kept for backward compatibility - * @deprecated since 1.18 - * @see Database::factory() - */ - public final static function newFromType( $dbType, $p = array() ) { - wfDeprecated( __METHOD__, '1.18' ); - if ( isset( $p['tableprefix'] ) ) { - $p['tablePrefix'] = $p['tableprefix']; - } - return self::factory( $dbType, $p ); - } - - /** * Given a DB type, construct the name of the appropriate child class of * DatabaseBase. This is designed to replace all of the manual stuff like: * $class = 'Database' . ucfirst( strtolower( $type ) ); @@ -665,6 +669,8 @@ abstract class DatabaseBase implements DatabaseType { * @see ForeignDBRepo::getMasterDB() * @see WebInstaller_DBConnect::execute() * + * @since 1.18 + * * @param $dbType String A possible DB type * @param $p Array An array of options to pass to the constructor. * Valid options are: host, user, password, dbname, flags, tablePrefix @@ -707,7 +713,7 @@ abstract class DatabaseBase implements DatabaseType { } if ( $this->mPHPError ) { $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError ); - $error = preg_replace( '!^.*?:(.*)$!', '$1', $error ); + $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error ); return $error; } else { return false; @@ -728,12 +734,31 @@ abstract class DatabaseBase implements DatabaseType { * * @return Bool operation success. true if already closed. */ - function close() { - # Stub, should probably be overridden - return true; + public function close() { + if ( count( $this->mTrxIdleCallbacks ) ) { // sanity + throw new MWException( "Transaction idle callbacks still pending." ); + } + $this->mOpened = false; + if ( $this->mConn ) { + if ( $this->trxLevel() ) { + $this->commit( __METHOD__ ); + } + $ret = $this->closeConnection(); + $this->mConn = false; + return $ret; + } else { + return true; + } } /** + * Closes underlying database connection + * @since 1.20 + * @return bool: Whether connection was closed successfully + */ + protected abstract function closeConnection(); + + /** * @param $error String: fallback error message, used if none is given by DB */ function reportConnectionError( $error = 'Unknown error' ) { @@ -762,8 +787,8 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function isWriteQuery( $sql ) { - return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW|\(SELECT)\b/i', $sql ); + public function isWriteQuery( $sql ) { + return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql ); } /** @@ -833,8 +858,13 @@ abstract class DatabaseBase implements DatabaseType { # that would delay transaction initializations to once connection # is really used by application $sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm) - if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) + if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) { + global $wgDebugDBTransactions; + if ( $wgDebugDBTransactions ) { + wfDebug("Implicit transaction start.\n"); + } $this->begin( __METHOD__ . " ($fname)" ); + } } if ( $this->debug() ) { @@ -863,6 +893,7 @@ abstract class DatabaseBase implements DatabaseType { if ( false === $ret && $this->wasErrorReissuable() ) { # Transaction is gone, like it or not $this->mTrxLevel = 0; + $this->mTrxIdleCallbacks = array(); // cancel wfDebug( "Connection lost, reconnecting...\n" ); if ( $this->ping() ) { @@ -903,7 +934,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String * @param $tempIgnore Boolean */ - function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { # Ignore errors during error handling to avoid infinite recursion $ignore = $this->ignoreErrors( true ); ++$this->mErrorCount; @@ -928,16 +959,12 @@ abstract class DatabaseBase implements DatabaseType { * & = filename; reads the file and inserts as a blob * (we don't use this though...) * - * This function should not be used directly by new code outside of the - * database classes. The query wrapper functions (select() etc.) should be - * used instead. - * * @param $sql string * @param $func string * * @return array */ - function prepare( $sql, $func = 'DatabaseBase::prepare' ) { + protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) { /* MySQL doesn't support prepared statements (yet), so just pack up the query for reference. We'll manually replace the bits later. */ @@ -948,7 +975,7 @@ abstract class DatabaseBase implements DatabaseType { * Free a prepared query, generated by prepare(). * @param $prepared */ - function freePrepared( $prepared ) { + protected function freePrepared( $prepared ) { /* No-op by default */ } @@ -959,7 +986,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return ResultWrapper */ - function execute( $prepared, $args = null ) { + public function execute( $prepared, $args = null ) { if ( !is_array( $args ) ) { # Pull the var args $args = func_get_args(); @@ -972,41 +999,13 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Prepare & execute an SQL statement, quoting and inserting arguments - * in the appropriate places. + * For faking prepared SQL statements on DBs that don't support it directly. * - * This function should not be used directly by new code outside of the - * database classes. The query wrapper functions (select() etc.) should be - * used instead. - * - * @param $query String - * @param $args ... - * - * @return ResultWrapper - */ - function safeQuery( $query, $args = null ) { - $prepared = $this->prepare( $query, 'DatabaseBase::safeQuery' ); - - if ( !is_array( $args ) ) { - # Pull the var args - $args = func_get_args(); - array_shift( $args ); - } - - $retval = $this->execute( $prepared, $args ); - $this->freePrepared( $prepared ); - - return $retval; - } - - /** - * For faking prepared SQL statements on DBs that don't support - * it directly. * @param $preparedQuery String: a 'preparable' SQL statement * @param $args Array of arguments to fill it with * @return string executable SQL */ - function fillPrepared( $preparedQuery, $args ) { + public function fillPrepared( $preparedQuery, $args ) { reset( $args ); $this->preparedArgs =& $args; @@ -1022,7 +1021,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $matches Array * @return String */ - function fillPreparedArg( $matches ) { + protected function fillPreparedArg( $matches ) { switch( $matches[1] ) { case '\\?': return '?'; case '\\!': return '!'; @@ -1049,32 +1048,7 @@ abstract class DatabaseBase implements DatabaseType { * * @param $res Mixed: A SQL result */ - function freeResult( $res ) { - } - - /** - * Simple UPDATE wrapper. - * Usually throws a DBQueryError on failure. - * If errors are explicitly ignored, returns success - * - * This function exists for historical reasons, DatabaseBase::update() has a more standard - * calling convention and feature set - * - * @param $table string - * @param $var - * @param $value - * @param $cond - * @param $fname string - * - * @return bool - */ - function set( $table, $var, $value, $cond, $fname = 'DatabaseBase::set' ) { - $table = $this->tableName( $table ); - $sql = "UPDATE $table SET $var = '" . - $this->strencode( $value ) . "' WHERE ($cond)"; - - return (bool)$this->query( $sql, $fname ); - } + public function freeResult( $res ) {} /** * A SELECT wrapper which returns a single field from a single result row. @@ -1091,9 +1065,9 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname string The function name of the caller. * @param $options string|array The query options. See DatabaseBase::select() for details. * - * @return false|mixed The value from the field, or false on failure. + * @return bool|mixed The value from the field, or false on failure. */ - function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', + public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', $options = array() ) { if ( !is_array( $options ) ) { @@ -1126,7 +1100,7 @@ abstract class DatabaseBase implements DatabaseType { * @return Array * @see DatabaseBase::select() */ - function makeSelectOptions( $options ) { + public function makeSelectOptions( $options ) { $preLimitTail = $postLimitTail = ''; $startOpts = ''; @@ -1146,7 +1120,10 @@ abstract class DatabaseBase implements DatabaseType { } if ( isset( $options['HAVING'] ) ) { - $preLimitTail .= " HAVING {$options['HAVING']}"; + $having = is_array( $options['HAVING'] ) + ? $this->makeList( $options['HAVING'], LIST_AND ) + : $options['HAVING']; + $preLimitTail .= " HAVING {$having}"; } if ( isset( $options['ORDER BY'] ) ) { @@ -1245,10 +1222,12 @@ abstract class DatabaseBase implements DatabaseType { * @param $vars string|array * * May be either a field name or an array of field names. The field names - * here are complete fragments of SQL, for direct inclusion into the SELECT - * query. Expressions and aliases may be specified as in SQL, for example: + * can be complete fragments of SQL, for direct inclusion into the SELECT + * query. If an array is given, field aliases can be specified, for example: + * + * array( 'maxrev' => 'MAX(rev_id)' ) * - * array( 'MAX(rev_id) AS maxrev' ) + * This includes an expression with the alias "maxrev" in the query. * * If an expression is given, care must be taken to ensure that it is * DBMS-independent. @@ -1305,7 +1284,9 @@ abstract class DatabaseBase implements DatabaseType { * - GROUP BY: May be either an SQL fragment string naming a field or * expression to group by, or an array of such SQL fragments. * - * - HAVING: A string containing a HAVING clause. + * - HAVING: May be either an string containing a HAVING clause or an array of + * conditions building the HAVING clause. If an array is given, the conditions + * constructed from each element are combined with AND. * * - ORDER BY: May be either an SQL fragment giving a field name or * expression to order by, or an array of such SQL fragments. @@ -1351,7 +1332,7 @@ abstract class DatabaseBase implements DatabaseType { * DBQueryError exception will be thrown, except if the "ignore errors" * option was set, in which case false will be returned. */ - function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', + public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); @@ -1360,7 +1341,9 @@ abstract class DatabaseBase implements DatabaseType { /** * The equivalent of DatabaseBase::select() except that the constructed SQL - * is returned, instead of being immediately executed. + * is returned, instead of being immediately executed. This can be useful for + * doing UNION queries, where the SQL text of each query is needed. In general, + * however, callers outside of Database classes should just use select(). * * @param $table string|array Table name * @param $vars string|array Field names @@ -1369,12 +1352,14 @@ abstract class DatabaseBase implements DatabaseType { * @param $options string|array Query options * @param $join_conds string|array Join conditions * - * @return SQL query string. + * @return string SQL query string. * @see DatabaseBase::select() */ - function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { + public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', + $options = array(), $join_conds = array() ) + { if ( is_array( $vars ) ) { - $vars = implode( ',', $vars ); + $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) ); } $options = (array)$options; @@ -1435,9 +1420,9 @@ abstract class DatabaseBase implements DatabaseType { * @param $options string|array Query options * @param $join_conds array|string Join conditions * - * @return ResultWrapper|bool + * @return object|bool */ - function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', + public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', $options = array(), $join_conds = array() ) { $options = (array)$options; @@ -1481,7 +1466,7 @@ abstract class DatabaseBase implements DatabaseType { $fname = 'DatabaseBase::estimateRowCount', $options = array() ) { $rows = 0; - $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options ); + $res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options ); if ( $res ) { $row = $this->fetchRow( $res ); @@ -1527,7 +1512,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: calling function name (optional) * @return Boolean: whether $table has filed $field */ - function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) { + public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) { $info = $this->fieldInfo( $table, $field ); return (bool)$info; @@ -1544,7 +1529,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool|null */ - function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) { + public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) { $info = $this->indexInfo( $table, $index, $fname ); if ( is_null( $info ) ) { return null; @@ -1561,7 +1546,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function tableExists( $table, $fname = __METHOD__ ) { + public function tableExists( $table, $fname = __METHOD__ ) { $table = $this->tableName( $table ); $old = $this->ignoreErrors( true ); $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname ); @@ -1576,7 +1561,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $index * @return string */ - function fieldType( $res, $index ) { + public function fieldType( $res, $index ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@ -1592,7 +1577,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function indexUnique( $table, $index ) { + public function indexUnique( $table, $index ) { $indexInfo = $this->indexInfo( $table, $index ); if ( !$indexInfo ) { @@ -1608,7 +1593,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $options array * @return string */ - function makeInsertOptions( $options ) { + protected function makeInsertOptions( $options ) { return implode( ' ', $options ); } @@ -1645,7 +1630,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) { + public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) { # No rows to insert, easy just return now if ( !count( $a ) ) { return true; @@ -1693,7 +1678,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $options Array: The options passed to DatabaseBase::update * @return string */ - function makeUpdateOptions( $options ) { + protected function makeUpdateOptions( $options ) { if ( !is_array( $options ) ) { $options = array( $options ); } @@ -1759,7 +1744,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function makeList( $a, $mode = LIST_COMMA ) { + public function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' ); } @@ -1819,12 +1804,12 @@ abstract class DatabaseBase implements DatabaseType { * The keys on each level may be either integers or strings. * * @param $data Array: organized as 2-d - * array(baseKeyVal => array(subKeyVal => <ignored>, ...), ...) + * array(baseKeyVal => array(subKeyVal => [ignored], ...), ...) * @param $baseKey String: field name to match the base-level keys to (eg 'pl_namespace') * @param $subKey String: field name to match the sub-level keys to (eg 'pl_title') * @return Mixed: string SQL fragment, or false if no items in array. */ - function makeWhereFrom2d( $data, $baseKey, $subKey ) { + public function makeWhereFrom2d( $data, $baseKey, $subKey ) { $conds = array(); foreach ( $data as $base => $sub ) { @@ -1844,14 +1829,22 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Bitwise operations + * Return aggregated value alias + * + * @param $valuedata + * @param $valuename string + * + * @return string */ + public function aggregateValue( $valuedata, $valuename = 'value' ) { + return $valuename; + } /** * @param $field * @return string */ - function bitNot( $field ) { + public function bitNot( $field ) { return "(~$field)"; } @@ -1860,7 +1853,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fieldRight * @return string */ - function bitAnd( $fieldLeft, $fieldRight ) { + public function bitAnd( $fieldLeft, $fieldRight ) { return "($fieldLeft & $fieldRight)"; } @@ -1869,11 +1862,20 @@ abstract class DatabaseBase implements DatabaseType { * @param $fieldRight * @return string */ - function bitOr( $fieldLeft, $fieldRight ) { + public function bitOr( $fieldLeft, $fieldRight ) { return "($fieldLeft | $fieldRight)"; } /** + * Build a concatenation list to feed into a SQL query + * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting + * @return String + */ + public function buildConcat( $stringList ) { + return 'CONCAT(' . implode( ',', $stringList ) . ')'; + } + + /** * Change the current database * * @todo Explain what exactly will fail if this is not overridden. @@ -1882,7 +1884,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool Success or failure */ - function selectDB( $db ) { + public function selectDB( $db ) { # Stub. Shouldn't cause serious problems if it's not overridden, but # if your database engine supports a concept similar to MySQL's # databases you may as well. @@ -1893,14 +1895,14 @@ abstract class DatabaseBase implements DatabaseType { /** * Get the current DB name */ - function getDBname() { + public function getDBname() { return $this->mDBname; } /** * Get the server hostname or IP address */ - function getServer() { + public function getServer() { return $this->mServer; } @@ -1921,7 +1923,7 @@ abstract class DatabaseBase implements DatabaseType { * raw - Do not add identifier quotes to the table name * @return String: full database name */ - function tableName( $name, $format = 'quoted' ) { + public function tableName( $name, $format = 'quoted' ) { global $wgSharedDB, $wgSharedPrefix, $wgSharedTables; # Skip the entire process when we have a string quoted on both ends. # Note that we check the end so that we will still quote any use of @@ -2067,6 +2069,39 @@ abstract class DatabaseBase implements DatabaseType { } /** + * Get an aliased field name + * e.g. fieldName AS newFieldName + * + * @param $name string Field name + * @param $alias string|bool Alias (optional) + * @return string SQL name for aliased field. Will not alias a field to its own name + */ + public function fieldNameWithAlias( $name, $alias = false ) { + if ( !$alias || (string)$alias === (string)$name ) { + return $name; + } else { + return $name . ' AS ' . $alias; //PostgreSQL needs AS + } + } + + /** + * Gets an array of aliased field names + * + * @param $fields array( [alias] => field ) + * @return array of strings, see fieldNameWithAlias() + */ + public function fieldNamesWithAlias( $fields ) { + $retval = array(); + foreach ( $fields as $alias => $field ) { + if ( is_numeric( $alias ) ) { + $alias = $field; + } + $retval[] = $this->fieldNameWithAlias( $field, $alias ); + } + return $retval; + } + + /** * Get the aliased table name clause for a FROM clause * which might have a JOIN and/or USE INDEX clause * @@ -2134,7 +2169,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function indexName( $index ) { + protected function indexName( $index ) { // Backwards-compatibility hack $renamed = array( 'ar_usertext_timestamp' => 'usertext_timestamp', @@ -2157,7 +2192,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function addQuotes( $s ) { + public function addQuotes( $s ) { if ( $s === null ) { return 'NULL'; } else { @@ -2196,36 +2231,6 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Backwards compatibility, identifier quoting originated in DatabasePostgres - * which used quote_ident which does not follow our naming conventions - * was renamed to addIdentifierQuotes. - * @deprecated since 1.18 use addIdentifierQuotes - * - * @param $s string - * - * @return string - */ - function quote_ident( $s ) { - wfDeprecated( __METHOD__, '1.18' ); - return $this->addIdentifierQuotes( $s ); - } - - /** - * Escape string for safe LIKE usage. - * WARNING: you should almost never use this function directly, - * instead use buildLike() that escapes everything automatically - * @deprecated since 1.17, warnings in 1.17, removed in ??? - * - * @param $s string - * - * @return string - */ - public function escapeLike( $s ) { - wfDeprecated( __METHOD__, '1.17' ); - return $this->escapeLikeInternal( $s ); - } - - /** * @param $s string * @return string */ @@ -2249,7 +2254,7 @@ abstract class DatabaseBase implements DatabaseType { * @since 1.16 * @return String: fully built LIKE statement */ - function buildLike() { + public function buildLike() { $params = func_get_args(); if ( count( $params ) > 0 && is_array( $params[0] ) ) { @@ -2274,7 +2279,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return LikeMatch */ - function anyChar() { + public function anyChar() { return new LikeMatch( '_' ); } @@ -2283,7 +2288,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return LikeMatch */ - function anyString() { + public function anyString() { return new LikeMatch( '%' ); } @@ -2298,7 +2303,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $seqName string * @return null */ - function nextSequenceValue( $seqName ) { + public function nextSequenceValue( $seqName ) { return null; } @@ -2312,7 +2317,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $index * @return string */ - function useIndexClause( $index ) { + public function useIndexClause( $index ) { return ''; } @@ -2338,7 +2343,7 @@ abstract class DatabaseBase implements DatabaseType { * a field name or an array of field names * @param $fname String: Calling function name (use __METHOD__) for logs/profiling */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) { + public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) { $quotedTable = $this->tableName( $table ); if ( count( $rows ) == 0 ) { @@ -2439,7 +2444,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: Calling function name (use __METHOD__) for * logs/profiling */ - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, + public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) { if ( !$conds ) { @@ -2466,7 +2471,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return int */ - function textFieldSize( $table, $field ) { + public function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";"; $res = $this->query( $sql, 'DatabaseBase::textFieldSize' ); @@ -2491,7 +2496,7 @@ abstract class DatabaseBase implements DatabaseType { * @return string Returns the text of the low priority option if it is * supported, or a blank string otherwise */ - function lowPriorityOption() { + public function lowPriorityOption() { return ''; } @@ -2505,7 +2510,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { + public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { if ( !$conds ) { throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' ); } @@ -2546,7 +2551,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return ResultWrapper */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, + public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseBase::insertSelect', $insertOptions = array(), $selectOptions = array() ) { @@ -2592,35 +2597,24 @@ abstract class DatabaseBase implements DatabaseType { * If the result of the query is not ordered, then the rows to be returned * are theoretically arbitrary. * - * $sql is expected to be a SELECT, if that makes a difference. For - * UPDATE, limitResultForUpdate should be used. + * $sql is expected to be a SELECT, if that makes a difference. * * The version provided by default works in MySQL and SQLite. It will very * likely need to be overridden for most other DBMSes. * * @param $sql String SQL query we will append the limit too * @param $limit Integer the SQL limit - * @param $offset Integer|false the SQL offset (default false) + * @param $offset Integer|bool the SQL offset (default false) * * @return string */ - function limitResult( $sql, $limit, $offset = false ) { + public function limitResult( $sql, $limit, $offset = false ) { if ( !is_numeric( $limit ) ) { throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); } - return "$sql LIMIT " - . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" ) - . "{$limit} "; - } - - /** - * @param $sql - * @param $num - * @return string - */ - function limitResultForUpdate( $sql, $num ) { - return $this->limitResult( $sql, $num, 0 ); + . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" ) + . "{$limit} "; } /** @@ -2628,7 +2622,7 @@ abstract class DatabaseBase implements DatabaseType { * within the UNION construct. * @return Boolean */ - function unionSupportsOrderAndLimit() { + public function unionSupportsOrderAndLimit() { return true; // True for almost every DB supported } @@ -2640,7 +2634,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $all Boolean: use UNION ALL * @return String: SQL fragment */ - function unionQueries( $sqls, $all ) { + public function unionQueries( $sqls, $all ) { $glue = $all ? ') UNION ALL (' : ') UNION ('; return '(' . implode( $glue, $sqls ) . ')'; } @@ -2649,12 +2643,15 @@ abstract class DatabaseBase implements DatabaseType { * Returns an SQL expression for a simple conditional. This doesn't need * to be overridden unless CASE isn't supported in your DBMS. * - * @param $cond String: SQL expression which will result in a boolean value + * @param $cond string|array SQL expression which will result in a boolean value * @param $trueVal String: SQL expression to return if true * @param $falseVal String: SQL expression to return if false * @return String: SQL fragment */ - function conditional( $cond, $trueVal, $falseVal ) { + public function conditional( $cond, $trueVal, $falseVal ) { + if ( is_array( $cond ) ) { + $cond = $this->makeList( $cond, LIST_AND ); + } return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; } @@ -2668,7 +2665,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function strreplace( $orig, $old, $new ) { + public function strreplace( $orig, $old, $new ) { return "REPLACE({$orig}, {$old}, {$new})"; } @@ -2678,7 +2675,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return int */ - function getServerUptime() { + public function getServerUptime() { return 0; } @@ -2688,7 +2685,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasDeadlock() { + public function wasDeadlock() { return false; } @@ -2698,7 +2695,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasLockTimeout() { + public function wasLockTimeout() { return false; } @@ -2709,7 +2706,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasErrorReissuable() { + public function wasErrorReissuable() { return false; } @@ -2719,7 +2716,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasReadOnlyError() { + public function wasReadOnlyError() { return false; } @@ -2741,8 +2738,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function deadlockLoop() { - + public function deadlockLoop() { $this->begin( __METHOD__ ); $args = func_get_args(); $function = array_shift( $args ); @@ -2790,11 +2786,11 @@ abstract class DatabaseBase implements DatabaseType { * @param $timeout Integer: the maximum number of seconds to wait for * synchronisation * - * @return An integer: zero if the slave was past that position already, + * @return integer: zero if the slave was past that position already, * greater than zero if we waited for some period of time, less than * zero if we timed out. */ - function masterPosWait( DBMasterPos $pos, $timeout ) { + public function masterPosWait( DBMasterPos $pos, $timeout ) { wfProfileIn( __METHOD__ ); if ( !is_null( $this->mFakeSlaveLag ) ) { @@ -2827,7 +2823,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return DBMasterPos, or false if this is not a slave. */ - function getSlavePos() { + public function getSlavePos() { if ( !is_null( $this->mFakeSlaveLag ) ) { $pos = new MySQLMasterPos( 'fake', microtime( true ) - $this->mFakeSlaveLag ); wfDebug( __METHOD__ . ": fake slave pos = $pos\n" ); @@ -2843,7 +2839,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return DBMasterPos, or false if this is not a master */ - function getMasterPos() { + public function getMasterPos() { if ( $this->mFakeMaster ) { return new MySQLMasterPos( 'fake', microtime( true ) ); } else { @@ -2852,11 +2848,65 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Begin a transaction, committing any previously open transaction + * Run an anonymous function as soon as there is no transaction pending. + * If there is a transaction and it is rolled back, then the callback is cancelled. + * Callbacks must commit any transactions that they begin. + * + * This is useful for updates to different systems or separate transactions are needed. + * + * @param Closure $callback + * @return void + */ + final public function onTransactionIdle( Closure $callback ) { + if ( $this->mTrxLevel ) { + $this->mTrxIdleCallbacks[] = $callback; + } else { + $callback(); + } + } + + /** + * Actually run the "on transaction idle" callbacks + */ + protected function runOnTransactionIdleCallbacks() { + $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled? + + $e = null; // last exception + do { // callbacks may add callbacks :) + $callbacks = $this->mTrxIdleCallbacks; + $this->mTrxIdleCallbacks = array(); // recursion guard + foreach ( $callbacks as $callback ) { + try { + $this->clearFlag( DBO_TRX ); // make each query its own transaction + $callback(); + $this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + } catch ( Exception $e ) {} + } + } while ( count( $this->mTrxIdleCallbacks ) ); + + if ( $e instanceof Exception ) { + throw $e; // re-throw any last exception + } + } + + /** + * Begin a transaction * * @param $fname string */ - function begin( $fname = 'DatabaseBase::begin' ) { + final public function begin( $fname = 'DatabaseBase::begin' ) { + if ( $this->mTrxLevel ) { // implicit commit + $this->doCommit( $fname ); + $this->runOnTransactionIdleCallbacks(); + } + $this->doBegin( $fname ); + } + + /** + * @see DatabaseBase::begin() + * @param type $fname + */ + protected function doBegin( $fname ) { $this->query( 'BEGIN', $fname ); $this->mTrxLevel = 1; } @@ -2866,7 +2916,16 @@ abstract class DatabaseBase implements DatabaseType { * * @param $fname string */ - function commit( $fname = 'DatabaseBase::commit' ) { + final public function commit( $fname = 'DatabaseBase::commit' ) { + $this->doCommit( $fname ); + $this->runOnTransactionIdleCallbacks(); + } + + /** + * @see DatabaseBase::commit() + * @param type $fname + */ + protected function doCommit( $fname ) { if ( $this->mTrxLevel ) { $this->query( 'COMMIT', $fname ); $this->mTrxLevel = 0; @@ -2879,7 +2938,16 @@ abstract class DatabaseBase implements DatabaseType { * * @param $fname string */ - function rollback( $fname = 'DatabaseBase::rollback' ) { + final public function rollback( $fname = 'DatabaseBase::rollback' ) { + $this->doRollback( $fname ); + $this->mTrxIdleCallbacks = array(); // cancel + } + + /** + * @see DatabaseBase::rollback() + * @param type $fname + */ + protected function doRollback( $fname ) { if ( $this->mTrxLevel ) { $this->query( 'ROLLBACK', $fname, true ); $this->mTrxLevel = 0; @@ -2900,7 +2968,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: calling function name * @return Boolean: true if operation was successful */ - function duplicateTableStructure( $oldName, $newName, $temporary = false, + public function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseBase::duplicateTableStructure' ) { throw new MWException( @@ -2910,7 +2978,7 @@ abstract class DatabaseBase implements DatabaseType { /** * List all tables on the database * - * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $prefix string Only show tables with this prefix, e.g. mw_ * @param $fname String: calling function name */ function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) { @@ -2928,7 +2996,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function timestamp( $ts = 0 ) { + public function timestamp( $ts = 0 ) { return wfTimestamp( TS_MW, $ts ); } @@ -2945,7 +3013,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function timestampOrNull( $ts = null ) { + public function timestampOrNull( $ts = null ) { if ( is_null( $ts ) ) { return null; } else { @@ -2968,7 +3036,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool|ResultWrapper */ - function resultObject( $result ) { + public function resultObject( $result ) { if ( empty( $result ) ) { return false; } elseif ( $result instanceof ResultWrapper ) { @@ -2982,23 +3050,11 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Return aggregated value alias - * - * @param $valuedata - * @param $valuename string - * - * @return string - */ - function aggregateValue ( $valuedata, $valuename = 'value' ) { - return $valuename; - } - - /** * Ping the server and try to reconnect if it there is no connection * * @return bool Success or failure */ - function ping() { + public function ping() { # Stub. Not essential to override. return true; } @@ -3010,9 +3066,9 @@ abstract class DatabaseBase implements DatabaseType { * installations. Most callers should use LoadBalancer::safeGetLag() * instead. * - * @return Database replication lag in seconds + * @return int Database replication lag in seconds */ - function getLag() { + public function getLag() { return intval( $this->mFakeSlaveLag ); } @@ -3033,7 +3089,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $b string * @return string */ - function encodeBlob( $b ) { + public function encodeBlob( $b ) { return $b; } @@ -3044,23 +3100,11 @@ abstract class DatabaseBase implements DatabaseType { * @param $b string * @return string */ - function decodeBlob( $b ) { + public function decodeBlob( $b ) { return $b; } /** - * Override database's default connection timeout - * - * @param $timeout Integer in seconds - * @return void - * @deprecated since 1.19; use setSessionOptions() - */ - public function setTimeout( $timeout ) { - wfDeprecated( __METHOD__, '1.19' ); - $this->setSessionOptions( array( 'connTimeout' => $timeout ) ); - } - - /** * Override database's default behavior. $options include: * 'connTimeout' : Set the connection timeout value in seconds. * May be useful for very long batch queries such as @@ -3085,7 +3129,9 @@ abstract class DatabaseBase implements DatabaseType { * generated dynamically using $filename * @return bool|string */ - function sourceFile( $filename, $lineCallback = false, $resultCallback = false, $fname = false ) { + public function sourceFile( + $filename, $lineCallback = false, $resultCallback = false, $fname = false + ) { wfSuppressWarnings(); $fp = fopen( $filename, 'r' ); wfRestoreWarnings(); @@ -3135,9 +3181,9 @@ abstract class DatabaseBase implements DatabaseType { * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at * all. If it's set to false, $GLOBALS will be used. * - * @param $vars False, or array mapping variable name to value. + * @param $vars bool|array mapping variable name to value. */ - function setSchemaVars( $vars ) { + public function setSchemaVars( $vars ) { $this->mSchemaVars = $vars; } @@ -3323,12 +3369,15 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Build a concatenation list to feed into a SQL query - * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting - * @return String + * Check to see if a named lock is available. This is non-blocking. + * + * @param $lockName String: name of lock to poll + * @param $method String: name of method calling us + * @return Boolean + * @since 1.20 */ - function buildConcat( $stringList ) { - return 'CONCAT(' . implode( ',', $stringList ) . ')'; + public function lockIsFree( $lockName, $method ) { + return true; } /** @@ -3352,7 +3401,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $lockName String: Name of lock to release * @param $method String: Name of method calling us * - * @return Returns 1 if the lock was released, 0 if the lock was not established + * @return int Returns 1 if the lock was released, 0 if the lock was not established * by this thread (in which case the lock is not released), and NULL if the named * lock did not exist */ @@ -3425,17 +3474,28 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Encode an expiry time + * Encode an expiry time into the DBMS dependent format * * @param $expiry String: timestamp for expiry, or the 'infinity' string * @return String */ public function encodeExpiry( $expiry ) { - if ( $expiry == '' || $expiry == $this->getInfinity() ) { - return $this->getInfinity(); - } else { - return $this->timestamp( $expiry ); - } + return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) + ? $this->getInfinity() + : $this->timestamp( $expiry ); + } + + /** + * Decode an expiry time into a DBMS independent format + * + * @param $expiry String: DB timestamp field value for expiry + * @param $format integer: TS_* constant, defaults to TS_MW + * @return String + */ + public function decodeExpiry( $expiry, $format = TS_MW ) { + return ( $expiry == '' || $expiry == $this->getInfinity() ) + ? 'infinity' + : wfTimestamp( $format, $expiry ); } /** @@ -3450,4 +3510,17 @@ abstract class DatabaseBase implements DatabaseType { public function setBigSelects( $value = true ) { // no-op } + + /** + * @since 1.19 + */ + public function __toString() { + return (string)$this->mConn; + } + + public function __destruct() { + if ( count( $this->mTrxIdleCallbacks ) ) { // sanity + trigger_error( "Transaction idle callbacks still pending." ); + } + } } diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index 836d7814..a53a6747 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -1,4 +1,25 @@ <?php +/** + * This file contains database error classes. + * + * 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 Database + */ /** * Database error base class @@ -189,7 +210,7 @@ class DBConnectionError extends DBError { * @return string */ function searchForm() { - global $wgSitename, $wgServer, $wgRequest; + global $wgSitename, $wgCanonicalServer, $wgRequest; $usegoogle = htmlspecialchars( $this->msg( 'dberr-usegoogle', 'You can try searching via Google in the meantime.' ) ); $outofdate = htmlspecialchars( $this->msg( 'dberr-outofdate', 'Note that their indexes of our content may be out of date.' ) ); @@ -197,7 +218,7 @@ class DBConnectionError extends DBError { $search = htmlspecialchars( $wgRequest->getVal( 'search' ) ); - $server = htmlspecialchars( $wgServer ); + $server = htmlspecialchars( $wgCanonicalServer ); $sitename = htmlspecialchars( $wgSitename ); $trygoogle = <<<EOT @@ -297,7 +318,7 @@ class DBQueryError extends DBError { $fname = $this->fname; $error = $this->error; } - return wfMsg( $msg, $sql, $fname, $this->errno, $error ); + return wfMessage( $msg )->rawParams( $sql, $fname, $this->errno, $error )->text(); } else { return parent::getContentMessage( $html ); } diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php index fed3b12e..f1f6dfca 100644 --- a/includes/db/DatabaseIbm_db2.php +++ b/includes/db/DatabaseIbm_db2.php @@ -2,7 +2,22 @@ /** * This is the IBM DB2 database abstraction layer. * See maintenance/ibm_db2/README for development notes - * and other specific information + * and other specific information. + * + * 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 Database @@ -122,7 +137,7 @@ class IBM_DB2Result{ /** * Construct and initialize a wrapper for DB2 query results - * @param $db Database + * @param $db DatabaseBase * @param $result Object * @param $num_rows Integer * @param $sql String @@ -130,21 +145,21 @@ class IBM_DB2Result{ */ public function __construct( $db, $result, $num_rows, $sql, $columns ){ $this->db = $db; - + if( $result instanceof ResultWrapper ){ $this->result = $result->result; } else{ $this->result = $result; } - + $this->num_rows = $num_rows; $this->current_pos = 0; if ( $this->num_rows > 0 ) { // Make a lower-case list of the column names // By default, DB2 column names are capitalized // while MySQL column names are lowercase - + // Is there a reasonable maximum value for $i? // Setting to 2048 to prevent an infinite loop for( $i = 0; $i < 2048; $i++ ) { @@ -155,11 +170,11 @@ class IBM_DB2Result{ else { return false; } - + $this->columns[$i] = strtolower( $name ); } } - + $this->sql = $sql; } @@ -187,14 +202,14 @@ class IBM_DB2Result{ * @return mixed Object on success, false on failure. */ public function fetchObject() { - if ( $this->result - && $this->num_rows > 0 - && $this->current_pos >= 0 - && $this->current_pos < $this->num_rows ) + if ( $this->result + && $this->num_rows > 0 + && $this->current_pos >= 0 + && $this->current_pos < $this->num_rows ) { $row = $this->fetchRow(); $ret = new stdClass(); - + foreach ( $row as $k => $v ) { $lc = $this->columns[$k]; $ret->$lc = $v; @@ -210,9 +225,9 @@ class IBM_DB2Result{ * @throws DBUnexpectedError */ public function fetchRow(){ - if ( $this->result - && $this->num_rows > 0 - && $this->current_pos >= 0 + if ( $this->result + && $this->num_rows > 0 + && $this->current_pos >= 0 && $this->current_pos < $this->num_rows ) { if ( $this->loadedLines <= $this->current_pos ) { @@ -227,7 +242,7 @@ class IBM_DB2Result{ if ( $this->loadedLines > $this->current_pos ){ return $this->resultSet[$this->current_pos++]; } - + } return false; } @@ -313,6 +328,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database supports (and uses) cascading deletes + * @return bool */ function cascadingDeletes() { return true; @@ -321,6 +337,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database supports (and uses) triggers (e.g. on the * page table) + * @return bool */ function cleanupTriggers() { return true; @@ -330,6 +347,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Returns true if this database is strict about what can be put into an * IP field. * Specifically, it uses a NULL value instead of an empty string. + * @return bool */ function strictIPs() { return true; @@ -337,13 +355,15 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database uses timestamps rather than integers - */ + * @return bool + */ function realTimestamps() { return true; } /** * Returns true if this database does an implicit sort when doing GROUP BY + * @return bool */ function implicitGroupby() { return false; @@ -353,6 +373,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Returns true if this database does an implicit order by when the column * has an index * For example: SELECT page_title FROM page LIMIT 1 + * @return bool */ function implicitOrderby() { return false; @@ -361,6 +382,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database can do a native search on IP columns * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32'; + * @return bool */ function searchableIPs() { return true; @@ -368,6 +390,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database can use functional indexes + * @return bool */ function functionalIndexes() { return true; @@ -375,6 +398,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns a unique string representing the wiki on the server + * @return string */ public function getWikiID() { if( $this->mSchema ) { @@ -392,7 +416,7 @@ class DatabaseIbm_db2 extends DatabaseBase { return 'ibm_db2'; } - /** + /** * Returns the database connection object * @return Object */ @@ -472,7 +496,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * @param $user String * @param $password String * @param $dbName String: database name - * @return a fresh connection + * @return DatabaseBase a fresh connection */ public function open( $server, $user, $password, $dbName ) { wfProfileIn( __METHOD__ ); @@ -546,32 +570,26 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - public function close() { - $this->mOpened = false; - if ( $this->mConn ) { - if ( $this->trxLevel() > 0 ) { - $this->commit(); - } - return db2_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return db2_close( $this->mConn ); } /** * Retrieves the most current database error * Forces a database rollback + * @return bool|string */ public function lastError() { $connerr = db2_conn_errormsg(); if ( $connerr ) { - //$this->rollback(); + //$this->rollback( __METHOD__ ); return $connerr; } $stmterr = db2_stmt_errormsg(); if ( $stmterr ) { - //$this->rollback(); + //$this->rollback( __METHOD__ ); return $stmterr; } @@ -667,7 +685,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Fields can be retrieved with $row->fieldname, with fields acting like * member variables. * - * @param $res SQL result object as returned from Database::query(), etc. + * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc. * @return DB2 row object * @throws DBUnexpectedError Thrown if the database returns an error */ @@ -689,8 +707,8 @@ class DatabaseIbm_db2 extends DatabaseBase { * Fetch the next row from the given result object, in associative array * form. Fields are retrieved with $row['fieldname']. * - * @param $res SQL result object as returned from Database::query(), etc. - * @return DB2 row object + * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc. + * @return ResultWrapper row object * @throws DBUnexpectedError Thrown if the database returns an error */ public function fetchRow( $res ) { @@ -715,7 +733,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Doesn't escape numbers * * @param $s String: string to escape - * @return escaped string + * @return string escaped string */ public function addQuotes( $s ) { //$this->installPrint( "DB2::addQuotes( $s )\n" ); @@ -758,7 +776,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Alias for addQuotes() * @param $s String: string to escape - * @return escaped string + * @return string escaped string */ public function strencode( $s ) { // Bloody useless function @@ -780,16 +798,16 @@ class DatabaseIbm_db2 extends DatabaseBase { protected function applySchema() { if ( !( $this->mSchemaSet ) ) { $this->mSchemaSet = true; - $this->begin(); + $this->begin( __METHOD__ ); $this->doQuery( "SET SCHEMA = $this->mSchema" ); - $this->commit(); + $this->commit( __METHOD__ ); } } /** * Start a transaction (mandatory) */ - public function begin( $fname = 'DatabaseIbm_db2::begin' ) { + protected function doBegin( $fname = 'DatabaseIbm_db2::begin' ) { // BEGIN is implicit for DB2 // However, it requires that AutoCommit be off. @@ -805,7 +823,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * End a transaction * Must have a preceding begin() */ - public function commit( $fname = 'DatabaseIbm_db2::commit' ) { + protected function doCommit( $fname = 'DatabaseIbm_db2::commit' ) { db2_commit( $this->mConn ); // Some MediaWiki code is still transaction-less (?). @@ -819,7 +837,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Cancel a transaction */ - public function rollback( $fname = 'DatabaseIbm_db2::rollback' ) { + protected function doRollback( $fname = 'DatabaseIbm_db2::rollback' ) { db2_rollback( $this->mConn ); // turn auto-commit back on // not sure if this is appropriate @@ -836,6 +854,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * LIST_SET - comma separated with field names, like a SET clause * LIST_NAMES - comma separated field names * LIST_SET_PREPARED - like LIST_SET, except with ? tokens as values + * @return string */ function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { @@ -873,6 +892,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * @param $sql string SQL query we will append the limit too * @param $limit integer the SQL limit * @param $offset integer the SQL offset (default false) + * @return string */ public function limitResult( $sql, $limit, $offset=false ) { if( !is_numeric( $limit ) ) { @@ -904,7 +924,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Generates a timestamp in an insertable format * - * @param $ts timestamp + * @param $ts string timestamp * @return String: timestamp value */ public function timestamp( $ts = 0 ) { @@ -915,7 +935,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Return the next in a sequence, save the value for retrieval via insertId() * @param $seqName String: name of a defined sequence in the database - * @return next value in that sequence + * @return int next value in that sequence */ public function nextSequenceValue( $seqName ) { // Not using sequences in the primary schema to allow for easier migration @@ -934,7 +954,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * This must be called after nextSequenceVal - * @return Last sequence value used as a primary key + * @return int Last sequence value used as a primary key */ public function insertId() { return $this->mInsertId; @@ -1003,7 +1023,7 @@ class DatabaseIbm_db2 extends DatabaseBase { $res = true; // If we are not in a transaction, we need to be for savepoint trickery if ( !$this->mTrxLevel ) { - $this->begin(); + $this->begin( __METHOD__ ); } $sql = "INSERT INTO $table ( " . implode( ',', $keys ) . ' ) VALUES '; @@ -1018,7 +1038,7 @@ class DatabaseIbm_db2 extends DatabaseBase { $stmt = $this->prepare( $sql ); // start a transaction/enter transaction mode - $this->begin(); + $this->begin( __METHOD__ ); if ( !$ignore ) { //$first = true; @@ -1071,7 +1091,7 @@ class DatabaseIbm_db2 extends DatabaseBase { $this->mAffectedRows = $numrowsinserted; } // commit either way - $this->commit(); + $this->commit( __METHOD__ ); $this->freePrepared( $stmt ); return $res; @@ -1121,11 +1141,11 @@ class DatabaseIbm_db2 extends DatabaseBase { * UPDATE wrapper, takes a condition array and a SET array * * @param $table String: The table to UPDATE - * @param $values An array of values to SET - * @param $conds An array of conditions ( WHERE ). Use '*' to update all rows. + * @param $values array An array of values to SET + * @param $conds array An array of conditions ( WHERE ). Use '*' to update all rows. * @param $fname String: The Class::Function calling this function * ( for the log ) - * @param $options An array of UPDATE options, can be one or + * @param $options array An array of UPDATE options, can be one or * more of IGNORE, LOW_PRIORITY * @return Boolean */ @@ -1153,6 +1173,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * DELETE query wrapper * * Use $conds == "*" to delete all rows + * @return bool|\ResultWrapper */ public function delete( $table, $conds, $fname = 'DatabaseIbm_db2::delete' ) { if ( !$conds ) { @@ -1206,7 +1227,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Moves the row pointer of the result set * @param $res Object: result set * @param $row Integer: row number - * @return success or failure + * @return bool success or failure */ public function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { @@ -1279,11 +1300,11 @@ class DatabaseIbm_db2 extends DatabaseBase { * @param $conds Array or string, condition(s) for WHERE * @param $fname String: calling function name (use __METHOD__) * for logs/profiling - * @param $options Associative array of options + * @param $options array Associative array of options * (e.g. array( 'GROUP BY' => 'page_title' )), * see Database::makeSelectOptions code for list of * supported stuff - * @param $join_conds Associative array of table join conditions (optional) + * @param $join_conds array Associative array of table join conditions (optional) * (e.g. array( 'page' => array('LEFT JOIN', * 'page_latest=rev_id') ) * @return Mixed: database result resource for fetch functions or false @@ -1320,10 +1341,10 @@ class DatabaseIbm_db2 extends DatabaseBase { $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, $join_conds ); - + $obj = $this->fetchObject( $res2 ); $this->mNumRows = $obj->num_rows; - + return new ResultWrapper( $this, new IBM_DB2Result( $this, $res, $obj->num_rows, $vars, $sql ) ); } @@ -1333,7 +1354,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * * @private * - * @param $options Associative array of options to be turned into + * @param $options array Associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return Array */ @@ -1412,7 +1433,7 @@ class DatabaseIbm_db2 extends DatabaseBase { // db2_ping() doesn't exist // Emulate $this->close(); - $this->mConn = $this->openUncataloged( $this->mDBName, $this->mUser, + $this->openUncataloged( $this->mDBName, $this->mUser, $this->mPassword, $this->mServer, $this->mPort ); return false; @@ -1420,14 +1441,6 @@ class DatabaseIbm_db2 extends DatabaseBase { ###################################### # Unimplemented and not applicable ###################################### - /** - * Not implemented - * @return string $sql - */ - public function limitResultForUpdate( $sql, $num ) { - $this->installPrint( 'Not implemented for DB2: limitResultForUpdate()' ); - return $sql; - } /** * Only useful with fake prepare like in base Database class @@ -1502,7 +1515,7 @@ SQL; * Verifies that an index was created as unique * @param $table String: table name * @param $index String: index name - * @param $fname function name for profiling + * @param $fname string function name for profiling * @return Bool */ public function indexUnique ( $table, $index, @@ -1636,25 +1649,6 @@ SQL; } /** - * Prepare & execute an SQL statement, quoting and inserting arguments - * in the appropriate places. - * @param $query String - * @param $args ... - */ - public function safeQuery( $query, $args = null ) { - // copied verbatim from Database.php - $prepared = $this->prepare( $query, 'DB2::safeQuery' ); - if( !is_array( $args ) ) { - # Pull the var args - $args = func_get_args(); - array_shift( $args ); - } - $retval = $this->execute( $prepared, $args ); - $this->freePrepared( $prepared ); - return $retval; - } - - /** * For faking prepared SQL statements on DBs that don't support * it directly. * @param $preparedQuery String: a 'preparable' SQL statement @@ -1674,6 +1668,7 @@ SQL; /** * Switches module between regular and install modes + * @return string */ public function setMode( $mode ) { $old = $this->mMode; diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 38be4cbb..914ab408 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -2,6 +2,21 @@ /** * This is the MS SQL Server Native database abstraction layer. * + * 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 Database * @author Joel Penner <a-joelpe at microsoft dot com> @@ -46,6 +61,7 @@ class DatabaseMssql extends DatabaseBase { /** * Usually aborts on failure + * @return bool|DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { # Test for driver support, to avoid suppressed fatal error @@ -107,14 +123,10 @@ class DatabaseMssql extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - return sqlsrv_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return sqlsrv_close( $this->mConn ); } protected function doQuery( $sql ) { @@ -226,6 +238,7 @@ class DatabaseMssql extends DatabaseBase { /** * This must be called after nextSequenceVal + * @return null */ function insertId() { return $this->mInsertId; @@ -310,6 +323,7 @@ class DatabaseMssql extends DatabaseBase { * This is not necessarily an accurate estimate, so use sparingly * Returns -1 if count cannot be found * Takes same arguments as Database::select() + * @return int */ function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) { $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx @@ -326,6 +340,7 @@ class DatabaseMssql extends DatabaseBase { /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @return array|bool|null */ function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) { # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the @@ -365,6 +380,7 @@ class DatabaseMssql extends DatabaseBase { * * Usually aborts on failure * If errors are explicitly ignored, returns success + * @return bool */ function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) { # No rows to insert, easy just return now @@ -494,6 +510,7 @@ class DatabaseMssql extends DatabaseBase { * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() * $conds may be "*" to copy the whole table * srcTable may be an array of tables. + * @return null|\ResultWrapper */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect', $insertOptions = array(), $selectOptions = array() ) { @@ -511,6 +528,7 @@ class DatabaseMssql extends DatabaseBase { /** * Return the next in a sequence, save the value for retrieval via insertId() + * @return */ function nextSequenceValue( $seqName ) { if ( !$this->tableExists( 'sequence_' . $seqName ) ) { @@ -527,6 +545,7 @@ class DatabaseMssql extends DatabaseBase { /** * Return the current value of a sequence. Assumes it has ben nextval'ed in this session. + * @return */ function currentSequenceValue( $seqName ) { $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); @@ -559,6 +578,7 @@ class DatabaseMssql extends DatabaseBase { * $sql string SQL query we will append the limit too * $limit integer the SQL limit * $offset integer the SQL offset (default false) + * @return mixed|string */ function limitResult( $sql, $limit, $offset = false ) { if ( $offset === false || $offset == 0 ) { @@ -600,14 +620,6 @@ class DatabaseMssql extends DatabaseBase { return $sql; } - // MSSQL does support this, but documentation is too thin to make a generalized - // function for this. Apparently UPDATE TOP (N) works, but the sort order - // may not be what we're expecting so the top n results may be a random selection. - // TODO: Implement properly. - function limitResultForUpdate( $sql, $num ) { - return $sql; - } - function timestamp( $ts = 0 ) { return wfTimestamp( TS_ISO_8601, $ts ); } @@ -647,6 +659,7 @@ class DatabaseMssql extends DatabaseBase { /** * Query whether a given column exists in the mediawiki schema + * @return bool */ function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) { $table = $this->tableName( $table ); @@ -681,7 +694,7 @@ class DatabaseMssql extends DatabaseBase { /** * Begin a transaction, committing any previously open transaction */ - function begin( $fname = 'DatabaseMssql::begin' ) { + protected function doBegin( $fname = 'DatabaseMssql::begin' ) { sqlsrv_begin_transaction( $this->mConn ); $this->mTrxLevel = 1; } @@ -689,7 +702,7 @@ class DatabaseMssql extends DatabaseBase { /** * End a transaction */ - function commit( $fname = 'DatabaseMssql::commit' ) { + protected function doCommit( $fname = 'DatabaseMssql::commit' ) { sqlsrv_commit( $this->mConn ); $this->mTrxLevel = 0; } @@ -698,7 +711,7 @@ class DatabaseMssql extends DatabaseBase { * Rollback a transaction. * No-op on non-transactional databases. */ - function rollback( $fname = 'DatabaseMssql::rollback' ) { + protected function doRollback( $fname = 'DatabaseMssql::rollback' ) { sqlsrv_rollback( $this->mConn ); $this->mTrxLevel = 0; } @@ -707,6 +720,7 @@ class DatabaseMssql extends DatabaseBase { * Escapes a identifier for use inm SQL. * Throws an exception if it is invalid. * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx + * @return string */ private function escapeIdentifier( $identifier ) { if ( strlen( $identifier ) == 0 ) { @@ -795,6 +809,7 @@ class DatabaseMssql extends DatabaseBase { /** * @private + * @return string */ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) { $ret = array(); @@ -893,6 +908,7 @@ class DatabaseMssql extends DatabaseBase { /** * Get the type of the DBMS, as it appears in $wgDBtype. + * @return string */ function getType(){ return 'mssql'; @@ -909,6 +925,7 @@ class DatabaseMssql extends DatabaseBase { /** * Since MSSQL doesn't recognize the infinity keyword, set date manually. * @todo Remove magic date + * @return string */ public function getInfinity() { return '3000-01-31 00:00:00.000'; diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index c179b724..7f389da9 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -2,6 +2,21 @@ /** * This is the MySQL database abstraction layer. * + * 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 Database */ @@ -44,7 +59,7 @@ class DatabaseMysql extends DatabaseBase { * @throws DBConnectionError */ function open( $server, $user, $password, $dbName ) { - global $wgAllDBsAreLocalhost; + global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode; wfProfileIn( __METHOD__ ); # Load mysql.so if we don't have it @@ -68,7 +83,15 @@ class DatabaseMysql extends DatabaseBase { $this->mPassword = $password; $this->mDBname = $dbName; - wfProfileIn("dbconnect-$server"); + $connFlags = 0; + if ( $this->mFlags & DBO_SSL ) { + $connFlags |= MYSQL_CLIENT_SSL; + } + if ( $this->mFlags & DBO_COMPRESS ) { + $connFlags |= MYSQL_CLIENT_COMPRESS; + } + + wfProfileIn( "dbconnect-$server" ); # The kernel's default SYN retransmission period is far too slow for us, # so we use a short timeout plus a manual retry. Retrying means that a small @@ -85,85 +108,71 @@ class DatabaseMysql extends DatabaseBase { usleep( 1000 ); } if ( $this->mFlags & DBO_PERSISTENT ) { - $this->mConn = mysql_pconnect( $realServer, $user, $password ); + $this->mConn = mysql_pconnect( $realServer, $user, $password, $connFlags ); } else { # Create a new connection... - $this->mConn = mysql_connect( $realServer, $user, $password, true ); + $this->mConn = mysql_connect( $realServer, $user, $password, true, $connFlags ); } #if ( $this->mConn === false ) { #$iplus = $i + 1; #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); #} } - $phpError = $this->restoreErrorHandler(); + $error = $this->restoreErrorHandler(); + + wfProfileOut( "dbconnect-$server" ); + # Always log connection errors if ( !$this->mConn ) { - $error = $this->lastError(); if ( !$error ) { - $error = $phpError; + $error = $this->lastError(); } wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); - wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, User: $user, Password: " . - substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); - } + wfDebug( "DB connection error\n" . + "Server: $server, User: $user, Password: " . + substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); - wfProfileOut("dbconnect-$server"); + wfProfileOut( __METHOD__ ); + $this->reportConnectionError( $error ); + } - if ( $dbName != '' && $this->mConn !== false ) { + if ( $dbName != '' ) { wfSuppressWarnings(); $success = mysql_select_db( $dbName, $this->mConn ); wfRestoreWarnings(); if ( !$success ) { - $error = "Error selecting database $dbName on server {$this->mServer} " . - "from client host " . wfHostname() . "\n"; - wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); - wfDebug( $error ); - } - } else { - # Delay USE query - $success = (bool)$this->mConn; - } + wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" ); + wfDebug( "Error selecting database $dbName on server {$this->mServer} " . + "from client host " . wfHostname() . "\n" ); - if ( $success ) { - // Tell the server we're communicating with it in UTF-8. - // This may engage various charset conversions. - global $wgDBmysql5; - if( $wgDBmysql5 ) { - $this->query( 'SET NAMES utf8', __METHOD__ ); - } else { - $this->query( 'SET NAMES binary', __METHOD__ ); - } - // Set SQL mode, default is turning them all off, can be overridden or skipped with null - global $wgSQLMode; - if ( is_string( $wgSQLMode ) ) { - $mode = $this->addQuotes( $wgSQLMode ); - $this->query( "SET sql_mode = $mode", __METHOD__ ); + wfProfileOut( __METHOD__ ); + $this->reportConnectionError( "Error selecting database $dbName" ); } + } - // Turn off strict mode if it is on + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + if( $wgDBmysql5 ) { + $this->query( 'SET NAMES utf8', __METHOD__ ); } else { - $this->reportConnectionError( $phpError ); + $this->query( 'SET NAMES binary', __METHOD__ ); + } + // Set SQL mode, default is turning them all off, can be overridden or skipped with null + if ( is_string( $wgSQLMode ) ) { + $mode = $this->addQuotes( $wgSQLMode ); + $this->query( "SET sql_mode = $mode", __METHOD__ ); } - $this->mOpened = $success; + $this->mOpened = true; wfProfileOut( __METHOD__ ); - return $success; + return true; } /** * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - if ( $this->trxLevel() ) { - $this->commit(); - } - return mysql_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return mysql_close( $this->mConn ); } /** @@ -194,7 +203,13 @@ class DatabaseMysql extends DatabaseBase { wfSuppressWarnings(); $row = mysql_fetch_object( $res ); wfRestoreWarnings(); - if( $this->lastErrno() ) { + + $errno = $this->lastErrno(); + // Unfortunately, mysql_fetch_object does not reset the last errno. + // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as + // these are the only errors mysql_fetch_object can cause. + // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html. + if( $errno == 2000 || $errno == 2013 ) { throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; @@ -212,7 +227,13 @@ class DatabaseMysql extends DatabaseBase { wfSuppressWarnings(); $row = mysql_fetch_array( $res ); wfRestoreWarnings(); - if ( $this->lastErrno() ) { + + $errno = $this->lastErrno(); + // Unfortunately, mysql_fetch_array does not reset the last errno. + // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as + // these are the only errors mysql_fetch_object can cause. + // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html. + if( $errno == 2000 || $errno == 2013 ) { throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; @@ -385,7 +406,7 @@ class DatabaseMysql extends DatabaseBase { * @param $table string * @param $index string * @param $fname string - * @return false|array + * @return bool|array|null False or null on failure */ function indexInfo( $table, $index, $fname = 'DatabaseMysql::indexInfo' ) { # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. @@ -558,7 +579,7 @@ class DatabaseMysql extends DatabaseBase { # Commit any open transactions if ( $this->mTrxLevel ) { - $this->commit(); + $this->commit( __METHOD__ ); } if ( !is_null( $this->mFakeSlaveLag ) ) { @@ -585,7 +606,7 @@ class DatabaseMysql extends DatabaseBase { /** * Get the position of the master from SHOW SLAVE STATUS * - * @return MySQLMasterPos|false + * @return MySQLMasterPos|bool */ function getSlavePos() { if ( !is_null( $this->mFakeSlaveLag ) ) { @@ -606,7 +627,7 @@ class DatabaseMysql extends DatabaseBase { /** * Get the position of the master from SHOW MASTER STATUS * - * @return MySQLMasterPos|false + * @return MySQLMasterPos|bool */ function getMasterPos() { if ( $this->mFakeMaster ) { @@ -653,13 +674,6 @@ class DatabaseMysql extends DatabaseBase { } /** - * @return bool - */ - function standardSelectDistinct() { - return false; - } - - /** * @param $options array */ public function setSessionOptions( array $options ) { @@ -680,6 +694,21 @@ class DatabaseMysql extends DatabaseBase { } /** + * Check to see if a named lock is available. This is non-blocking. + * + * @param $lockName String: name of lock to poll + * @param $method String: name of method calling us + * @return Boolean + * @since 1.20 + */ + public function lockIsFree( $lockName, $method ) { + $lockName = $this->addQuotes( $lockName ); + $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method ); + $row = $this->fetchObject( $result ); + return ( $row->lockstatus == 1 ); + } + + /** * @param $lockName string * @param $method string * @param $timeout int @@ -708,7 +737,7 @@ class DatabaseMysql extends DatabaseBase { $lockName = $this->addQuotes( $lockName ); $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method ); $row = $this->fetchObject( $result ); - return $row->lockstatus; + return ( $row->lockstatus == 1 ); } /** @@ -860,7 +889,7 @@ class DatabaseMysql extends DatabaseBase { /** * List all tables on the database * - * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $prefix string Only show tables with this prefix, e.g. mw_ * @param $fname String: calling function name * @return array */ diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 855fc831..7d8884fb 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -2,6 +2,21 @@ /** * This is the Oracle database abstraction layer. * + * 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 Database */ @@ -226,6 +241,7 @@ class DatabaseOracle extends DatabaseBase { /** * Usually aborts on failure + * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { if ( !function_exists( 'oci_connect' ) ) { @@ -285,17 +301,10 @@ class DatabaseOracle extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - if ( $this->mTrxLevel ) { - $this->commit(); - } - return oci_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return oci_close( $this->mConn ); } function execFlags() { @@ -401,6 +410,7 @@ class DatabaseOracle extends DatabaseBase { /** * This must be called after nextSequenceVal + * @return null */ function insertId() { return $this->mInsertId; @@ -439,6 +449,7 @@ class DatabaseOracle extends DatabaseBase { /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @return bool */ function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { return false; @@ -679,6 +690,7 @@ class DatabaseOracle extends DatabaseBase { } /** * Return the next in a sequence, save the value for retrieval via insertId() + * @return null */ function nextSequenceValue( $seqName ) { $res = $this->query( "SELECT $seqName.nextval FROM dual" ); @@ -689,6 +701,7 @@ class DatabaseOracle extends DatabaseBase { /** * Return sequence_name if table has a sequence + * @return bool */ private function getSequenceData( $table ) { if ( $this->sequenceData == null ) { @@ -797,7 +810,7 @@ class DatabaseOracle extends DatabaseBase { /** * Return aggregated value function call */ - function aggregateValue ( $valuedata, $valuename = 'value' ) { + public function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; } @@ -836,6 +849,7 @@ class DatabaseOracle extends DatabaseBase { /** * Query whether a given index exists + * @return bool */ function indexExists( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { $table = $this->tableName( $table ); @@ -855,6 +869,7 @@ class DatabaseOracle extends DatabaseBase { /** * Query whether a given table exists (in the given schema, or the default mw one if not given) + * @return int */ function tableExists( $table, $fname = __METHOD__ ) { $table = $this->tableName( $table ); @@ -940,12 +955,12 @@ class DatabaseOracle extends DatabaseBase { return $this->fieldInfoMulti ($table, $field); } - function begin( $fname = 'DatabaseOracle::begin' ) { + protected function doBegin( $fname = 'DatabaseOracle::begin' ) { $this->mTrxLevel = 1; $this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' ); } - function commit( $fname = 'DatabaseOracle::commit' ) { + protected function doCommit( $fname = 'DatabaseOracle::commit' ) { if ( $this->mTrxLevel ) { $ret = oci_commit( $this->mConn ); if ( !$ret ) { @@ -956,7 +971,7 @@ class DatabaseOracle extends DatabaseBase { } } - function rollback( $fname = 'DatabaseOracle::rollback' ) { + protected function doRollback( $fname = 'DatabaseOracle::rollback' ) { if ( $this->mTrxLevel ) { oci_rollback( $this->mConn ); $this->mTrxLevel = 0; @@ -964,11 +979,6 @@ class DatabaseOracle extends DatabaseBase { } } - /* Not even sure why this is used in the main codebase... */ - function limitResultForUpdate( $sql, $num ) { - return $sql; - } - /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */ function sourceStream( $fp, $lineCallback = false, $resultCallback = false, $fname = 'DatabaseOracle::sourceStream', $inputCallback = false ) { diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 98cf3c75..457bf384 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -2,12 +2,28 @@ /** * This is the Postgres database abstraction layer. * + * 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 Database */ class PostgresField implements Field { - private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname; + private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname, + $has_default, $default; /** * @param $db DatabaseBase @@ -16,11 +32,11 @@ class PostgresField implements Field { * @return null|PostgresField */ static function fromText( $db, $table, $field ) { - global $wgDBmwschema; - $q = <<<SQL SELECT - attnotnull, attlen, COALESCE(conname, '') AS conname, + attnotnull, attlen, conname AS conname, + atthasdef, + adsrc, COALESCE(condeferred, 'f') AS deferred, COALESCE(condeferrable, 'f') AS deferrable, CASE WHEN typname = 'int2' THEN 'smallint' @@ -33,6 +49,7 @@ JOIN pg_namespace n ON (n.oid = c.relnamespace) JOIN pg_attribute a ON (a.attrelid = c.oid) JOIN pg_type t ON (t.oid = a.atttypid) LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f') +LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum WHERE relkind = 'r' AND nspname=%s AND relname=%s @@ -42,7 +59,7 @@ SQL; $table = $db->tableName( $table, 'raw' ); $res = $db->query( sprintf( $q, - $db->addQuotes( $wgDBmwschema ), + $db->addQuotes( $db->getCoreSchema() ), $db->addQuotes( $table ), $db->addQuotes( $field ) ) @@ -60,6 +77,8 @@ SQL; $n->deferrable = ( $row->deferrable == 't' ); $n->deferred = ( $row->deferred == 't' ); $n->conname = $row->conname; + $n->has_default = ( $row->atthasdef === 't' ); + $n->default = $row->adsrc; return $n; } @@ -94,7 +113,169 @@ SQL; function conname() { return $this->conname; } + /** + * @since 1.19 + */ + function defaultValue() { + if( $this->has_default ) { + return $this->default; + } else { + return false; + } + } + +} + +/** + * Used to debug transaction processing + * Only used if $wgDebugDBTransactions is true + * + * @since 1.19 + * @ingroup Database + */ +class PostgresTransactionState { + + static $WATCHED = array( + array( + "desc" => "%s: Connection state changed from %s -> %s\n", + "states" => array( + PGSQL_CONNECTION_OK => "OK", + PGSQL_CONNECTION_BAD => "BAD" + ) + ), + array( + "desc" => "%s: Transaction state changed from %s -> %s\n", + "states" => array( + PGSQL_TRANSACTION_IDLE => "IDLE", + PGSQL_TRANSACTION_ACTIVE => "ACTIVE", + PGSQL_TRANSACTION_INTRANS => "TRANS", + PGSQL_TRANSACTION_INERROR => "ERROR", + PGSQL_TRANSACTION_UNKNOWN => "UNKNOWN" + ) + ) + ); + + public function __construct( $conn ) { + $this->mConn = $conn; + $this->update(); + $this->mCurrentState = $this->mNewState; + } + + public function update() { + $this->mNewState = array( + pg_connection_status( $this->mConn ), + pg_transaction_status( $this->mConn ) + ); + } + + public function check() { + global $wgDebugDBTransactions; + $this->update(); + if ( $wgDebugDBTransactions ) { + if ( $this->mCurrentState !== $this->mNewState ) { + $old = reset( $this->mCurrentState ); + $new = reset( $this->mNewState ); + foreach ( self::$WATCHED as $watched ) { + if ($old !== $new) { + $this->log_changed($old, $new, $watched); + } + $old = next( $this->mCurrentState ); + $new = next( $this->mNewState ); + + } + } + } + $this->mCurrentState = $this->mNewState; + } + + protected function describe_changed( $status, $desc_table ) { + if( isset( $desc_table[$status] ) ) { + return $desc_table[$status]; + } else { + return "STATUS " . $status; + } + } + protected function log_changed( $old, $new, $watched ) { + wfDebug(sprintf($watched["desc"], + $this->mConn, + $this->describe_changed( $old, $watched["states"] ), + $this->describe_changed( $new, $watched["states"] )) + ); + } +} + +/** + * Manage savepoints within a transaction + * @ingroup Database + * @since 1.19 + */ +class SavepointPostgres { + /** + * Establish a savepoint within a transaction + */ + protected $dbw; + protected $id; + protected $didbegin; + + public function __construct ($dbw, $id) { + $this->dbw = $dbw; + $this->id = $id; + $this->didbegin = false; + /* If we are not in a transaction, we need to be for savepoint trickery */ + if ( !$dbw->trxLevel() ) { + $dbw->begin( "FOR SAVEPOINT" ); + $this->didbegin = true; + } + } + + public function __destruct() { + if ( $this->didbegin ) { + $this->dbw->rollback(); + } + } + + public function commit() { + if ( $this->didbegin ) { + $this->dbw->commit(); + } + } + + protected function query( $keyword, $msg_ok, $msg_failed ) { + global $wgDebugDBTransactions; + if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) { + if ( $wgDebugDBTransactions ) { + wfDebug( sprintf ($msg_ok, $this->id ) ); + } + } else { + wfDebug( sprintf ($msg_failed, $this->id ) ); + } + } + + public function savepoint() { + $this->query("SAVEPOINT", + "Transaction state: savepoint \"%s\" established.\n", + "Transaction state: establishment of savepoint \"%s\" FAILED.\n" + ); + } + + public function release() { + $this->query("RELEASE", + "Transaction state: savepoint \"%s\" released.\n", + "Transaction state: release of savepoint \"%s\" FAILED.\n" + ); + } + + public function rollback() { + $this->query("ROLLBACK TO", + "Transaction state: savepoint \"%s\" rolled back.\n", + "Transaction state: rollback of savepoint \"%s\" FAILED.\n" + ); + } + + public function __toString() { + return (string)$this->id; + } } /** @@ -136,15 +317,15 @@ class DatabasePostgres extends DatabaseBase { } function hasConstraint( $name ) { - global $wgDBmwschema; $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . - pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $wgDBmwschema ) ."'"; + pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) ."'"; $res = $this->doQuery( $SQL ); return $this->numRows( $res ); } /** * Usually aborts on failure + * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { # Test for Postgres support, to avoid suppressed fatal error @@ -158,7 +339,6 @@ class DatabasePostgres extends DatabaseBase { return; } - $this->close(); $this->mServer = $server; $port = $wgDBport; $this->mUser = $user; @@ -176,10 +356,14 @@ class DatabasePostgres extends DatabaseBase { if ( $port != false && $port != '' ) { $connectVars['port'] = $port; } - $connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); + if ( $this->mFlags & DBO_SSL ) { + $connectVars['sslmode'] = 1; + } + $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); + $this->close(); $this->installErrorHandler(); - $this->mConn = pg_connect( $connectString ); + $this->mConn = pg_connect( $this->connectString ); $phpError = $this->restoreErrorHandler(); if ( !$this->mConn ) { @@ -190,6 +374,7 @@ class DatabasePostgres extends DatabaseBase { } $this->mOpened = true; + $this->mTransactionState = new PostgresTransactionState( $this->mConn ); global $wgCommandLineMode; # If called from the command-line (e.g. importDump), only show errors @@ -203,12 +388,7 @@ class DatabasePostgres extends DatabaseBase { $this->query( "SET standard_conforming_strings = on", __METHOD__ ); global $wgDBmwschema; - if ( $this->schemaExists( $wgDBmwschema ) ) { - $safeschema = $this->addIdentifierQuotes( $wgDBmwschema ); - $this->doQuery( "SET search_path = $safeschema" ); - } else { - $this->doQuery( "SET search_path = public" ); - } + $this->determineCoreSchema( $wgDBmwschema ); return $this->mConn; } @@ -237,25 +417,62 @@ class DatabasePostgres extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - return pg_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return pg_close( $this->mConn ); } - protected function doQuery( $sql ) { + public function doQuery( $sql ) { if ( function_exists( 'mb_convert_encoding' ) ) { $sql = mb_convert_encoding( $sql, 'UTF-8' ); } - $this->mLastResult = pg_query( $this->mConn, $sql ); - $this->mAffectedRows = null; // use pg_affected_rows(mLastResult) + $this->mTransactionState->check(); + if( pg_send_query( $this->mConn, $sql ) === false ) { + throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" ); + } + $this->mLastResult = pg_get_result( $this->mConn ); + $this->mTransactionState->check(); + $this->mAffectedRows = null; + if ( pg_result_error( $this->mLastResult ) ) { + return false; + } return $this->mLastResult; } + protected function dumpError () { + $diags = array( PGSQL_DIAG_SEVERITY, + PGSQL_DIAG_SQLSTATE, + PGSQL_DIAG_MESSAGE_PRIMARY, + PGSQL_DIAG_MESSAGE_DETAIL, + PGSQL_DIAG_MESSAGE_HINT, + PGSQL_DIAG_STATEMENT_POSITION, + PGSQL_DIAG_INTERNAL_POSITION, + PGSQL_DIAG_INTERNAL_QUERY, + PGSQL_DIAG_CONTEXT, + PGSQL_DIAG_SOURCE_FILE, + PGSQL_DIAG_SOURCE_LINE, + PGSQL_DIAG_SOURCE_FUNCTION ); + foreach ( $diags as $d ) { + wfDebug( sprintf("PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) ); + } + } + + function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + /* Transaction stays in the ERROR state until rolledback */ + if ( $tempIgnore ) { + /* Check for constraint violation */ + if ( $errno === '23505' ) { + parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore ); + return; + } + } + /* Don't ignore serious errors */ + $this->rollback( __METHOD__ ); + parent::reportQueryError( $error, $errno, $sql, $fname, false ); + } + + function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) { return $this->query( $sql, $fname, true ); } @@ -331,6 +548,7 @@ class DatabasePostgres extends DatabaseBase { /** * This must be called after nextSequenceVal + * @return null */ function insertId() { return $this->mInsertId; @@ -345,13 +563,21 @@ class DatabasePostgres extends DatabaseBase { function lastError() { if ( $this->mConn ) { - return pg_last_error(); + if ( $this->mLastResult ) { + return pg_result_error( $this->mLastResult ); + } else { + return pg_last_error(); + } } else { return 'No database connection'; } } function lastErrno() { - return pg_last_error() ? 1 : 0; + if ( $this->mLastResult ) { + return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE ); + } else { + return false; + } } function affectedRows() { @@ -371,6 +597,7 @@ class DatabasePostgres extends DatabaseBase { * This is not necessarily an accurate estimate, so use sparingly * Returns -1 if count cannot be found * Takes same arguments as Database::select() + * @return int */ function estimateRowCount( $table, $vars = '*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) { $options['EXPLAIN'] = true; @@ -389,6 +616,7 @@ class DatabasePostgres extends DatabaseBase { /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @return bool|null */ function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) { $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'"; @@ -404,6 +632,68 @@ class DatabasePostgres extends DatabaseBase { return false; } + /** + * Returns is of attributes used in index + * + * @since 1.19 + * @return Array + */ + function indexAttributes ( $index, $schema = false ) { + if ( $schema === false ) + $schema = $this->getCoreSchema(); + /* + * A subquery would be not needed if we didn't care about the order + * of attributes, but we do + */ + $sql = <<<__INDEXATTR__ + + SELECT opcname, + attname, + i.indoption[s.g] as option, + pg_am.amname + FROM + (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g + FROM + pg_index isub + JOIN pg_class cis + ON cis.oid=isub.indexrelid + JOIN pg_namespace ns + ON cis.relnamespace = ns.oid + WHERE cis.relname='$index' AND ns.nspname='$schema') AS s, + pg_attribute, + pg_opclass opcls, + pg_am, + pg_class ci + JOIN pg_index i + ON ci.oid=i.indexrelid + JOIN pg_class ct + ON ct.oid = i.indrelid + JOIN pg_namespace n + ON ci.relnamespace = n.oid + WHERE + ci.relname='$index' AND n.nspname='$schema' + AND attrelid = ct.oid + AND i.indkey[s.g] = attnum + AND i.indclass[s.g] = opcls.oid + AND pg_am.oid = opcls.opcmethod +__INDEXATTR__; + $res = $this->query($sql, __METHOD__); + $a = array(); + if ( $res ) { + foreach ( $res as $row ) { + $a[] = array( + $row->attname, + $row->opcname, + $row->amname, + $row->option); + } + } else { + return null; + } + return $a; + } + + function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) { $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'". " AND indexdef LIKE 'CREATE UNIQUE%(" . @@ -455,15 +745,9 @@ class DatabasePostgres extends DatabaseBase { } // If IGNORE is set, we use savepoints to emulate mysql's behavior - $ignore = in_array( 'IGNORE', $options ) ? 'mw' : ''; - - // If we are not in a transaction, we need to be for savepoint trickery - $didbegin = 0; - if ( $ignore ) { - if ( !$this->mTrxLevel ) { - $this->begin(); - $didbegin = 1; - } + $savepoint = null; + if ( in_array( 'IGNORE', $options ) ) { + $savepoint = new SavepointPostgres( $this, 'mw' ); $olde = error_reporting( 0 ); // For future use, we may want to track the number of actual inserts // Right now, insert (all writes) simply return true/false @@ -473,7 +757,7 @@ class DatabasePostgres extends DatabaseBase { $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES '; if ( $multi ) { - if ( $this->numeric_version >= 8.2 && !$ignore ) { + if ( $this->numeric_version >= 8.2 && !$savepoint ) { $first = true; foreach ( $args as $row ) { if ( $first ) { @@ -483,7 +767,7 @@ class DatabasePostgres extends DatabaseBase { } $sql .= '(' . $this->makeList( $row ) . ')'; } - $res = (bool)$this->query( $sql, $fname, $ignore ); + $res = (bool)$this->query( $sql, $fname, $savepoint ); } else { $res = true; $origsql = $sql; @@ -491,18 +775,18 @@ class DatabasePostgres extends DatabaseBase { $tempsql = $origsql; $tempsql .= '(' . $this->makeList( $row ) . ')'; - if ( $ignore ) { - pg_query( $this->mConn, "SAVEPOINT $ignore" ); + if ( $savepoint ) { + $savepoint->savepoint(); } - $tempres = (bool)$this->query( $tempsql, $fname, $ignore ); + $tempres = (bool)$this->query( $tempsql, $fname, $savepoint ); - if ( $ignore ) { + if ( $savepoint ) { $bar = pg_last_error(); if ( $bar != false ) { - pg_query( $this->mConn, "ROLLBACK TO $ignore" ); + $savepoint->rollback(); } else { - pg_query( $this->mConn, "RELEASE $ignore" ); + $savepoint->release(); $numrowsinserted++; } } @@ -516,27 +800,25 @@ class DatabasePostgres extends DatabaseBase { } } else { // Not multi, just a lone insert - if ( $ignore ) { - pg_query($this->mConn, "SAVEPOINT $ignore"); + if ( $savepoint ) { + $savepoint->savepoint(); } $sql .= '(' . $this->makeList( $args ) . ')'; - $res = (bool)$this->query( $sql, $fname, $ignore ); - if ( $ignore ) { + $res = (bool)$this->query( $sql, $fname, $savepoint ); + if ( $savepoint ) { $bar = pg_last_error(); if ( $bar != false ) { - pg_query( $this->mConn, "ROLLBACK TO $ignore" ); + $savepoint->rollback(); } else { - pg_query( $this->mConn, "RELEASE $ignore" ); + $savepoint->release(); $numrowsinserted++; } } } - if ( $ignore ) { + if ( $savepoint ) { $olde = error_reporting( $olde ); - if ( $didbegin ) { - $this->commit(); - } + $savepoint->commit(); // Set the affected row count for the whole operation $this->mAffectedRows = $numrowsinserted; @@ -555,18 +837,29 @@ class DatabasePostgres extends DatabaseBase { * $conds may be "*" to copy the whole table * srcTable may be an array of tables. * @todo FIXME: Implement this a little better (seperate select/insert)? + * @return bool */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect', $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); - // If IGNORE is set, we use savepoints to emulate mysql's behavior - $ignore = in_array( 'IGNORE', $insertOptions ) ? 'mw' : ''; + if( !is_array( $insertOptions ) ) { + $insertOptions = array( $insertOptions ); + } - if( is_array( $insertOptions ) ) { - $insertOptions = implode( ' ', $insertOptions ); // FIXME: This is unused + /* + * If IGNORE is set, we use savepoints to emulate mysql's behavior + * Ignore LOW PRIORITY option, since it is MySQL-specific + */ + $savepoint = null; + if ( in_array( 'IGNORE', $insertOptions ) ) { + $savepoint = new SavepointPostgres( $this, 'mw' ); + $olde = error_reporting( 0 ); + $numrowsinserted = 0; + $savepoint->savepoint(); } + if( !is_array( $selectOptions ) ) { $selectOptions = array( $selectOptions ); } @@ -577,18 +870,6 @@ class DatabasePostgres extends DatabaseBase { $srcTable = $this->tableName( $srcTable ); } - // If we are not in a transaction, we need to be for savepoint trickery - $didbegin = 0; - if ( $ignore ) { - if( !$this->mTrxLevel ) { - $this->begin(); - $didbegin = 1; - } - $olde = error_reporting( 0 ); - $numrowsinserted = 0; - pg_query( $this->mConn, "SAVEPOINT $ignore"); - } - $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . " SELECT $startOpts " . implode( ',', $varMap ) . " FROM $srcTable $useIndex"; @@ -599,19 +880,17 @@ class DatabasePostgres extends DatabaseBase { $sql .= " $tailOpts"; - $res = (bool)$this->query( $sql, $fname, $ignore ); - if( $ignore ) { + $res = (bool)$this->query( $sql, $fname, $savepoint ); + if( $savepoint ) { $bar = pg_last_error(); if( $bar != false ) { - pg_query( $this->mConn, "ROLLBACK TO $ignore" ); + $savepoint->rollback(); } else { - pg_query( $this->mConn, "RELEASE $ignore" ); + $savepoint->release(); $numrowsinserted++; } $olde = error_reporting( $olde ); - if( $didbegin ) { - $this->commit(); - } + $savepoint->commit(); // Set the affected row count for the whole operation $this->mAffectedRows = $numrowsinserted; @@ -642,6 +921,7 @@ class DatabasePostgres extends DatabaseBase { /** * Return the next in a sequence, save the value for retrieval via insertId() + * @return null */ function nextSequenceValue( $seqName ) { $safeseq = str_replace( "'", "''", $seqName ); @@ -653,6 +933,7 @@ class DatabasePostgres extends DatabaseBase { /** * Return the current value of a sequence. Assumes it has been nextval'ed in this session. + * @return */ function currentSequenceValue( $seqName ) { $safeseq = str_replace( "'", "''", $seqName ); @@ -694,10 +975,8 @@ class DatabasePostgres extends DatabaseBase { } function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) { - global $wgDBmwschema; - $eschema = $this->addQuotes( $wgDBmwschema ); + $eschema = $this->addQuotes( $this->getCoreSchema() ); $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname ); - $endArray = array(); foreach( $result as $table ) { @@ -715,10 +994,54 @@ class DatabasePostgres extends DatabaseBase { return wfTimestamp( TS_POSTGRES, $ts ); } + /* + * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12 + * to http://www.php.net/manual/en/ref.pgsql.php + * + * Parsing a postgres array can be a tricky problem, he's my + * take on this, it handles multi-dimensional arrays plus + * escaping using a nasty regexp to determine the limits of each + * data-item. + * + * This should really be handled by PHP PostgreSQL module + * + * @since 1.19 + * @param $text string: postgreql array returned in a text form like {a,b} + * @param $output string + * @param $limit int + * @param $offset int + * @return string + */ + function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) { + if( false === $limit ) { + $limit = strlen( $text )-1; + $output = array(); + } + if( '{}' == $text ) { + return $output; + } + do { + if ( '{' != $text{$offset} ) { + preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/", + $text, $match, 0, $offset ); + $offset += strlen( $match[0] ); + $output[] = ( '"' != $match[1]{0} + ? $match[1] + : stripcslashes( substr( $match[1], 1, -1 ) ) ); + if ( '},' == $match[3] ) { + return $output; + } + } else { + $offset = $this->pg_array_parse( $text, $output, $limit, $offset+1 ); + } + } while ( $limit > $offset ); + return $output; + } + /** * Return aggregated value function call */ - function aggregateValue( $valuedata, $valuename = 'value' ) { + public function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; } @@ -729,6 +1052,115 @@ class DatabasePostgres extends DatabaseBase { return '[http://www.postgresql.org/ PostgreSQL]'; } + + /** + * Return current schema (executes SELECT current_schema()) + * Needs transaction + * + * @since 1.19 + * @return string return default schema for the current session + */ + function getCurrentSchema() { + $res = $this->query( "SELECT current_schema()", __METHOD__); + $row = $this->fetchRow( $res ); + return $row[0]; + } + + /** + * Return list of schemas which are accessible without schema name + * This is list does not contain magic keywords like "$user" + * Needs transaction + * + * @seealso getSearchPath() + * @seealso setSearchPath() + * @since 1.19 + * @return array list of actual schemas for the current sesson + */ + function getSchemas() { + $res = $this->query( "SELECT current_schemas(false)", __METHOD__); + $row = $this->fetchRow( $res ); + $schemas = array(); + /* PHP pgsql support does not support array type, "{a,b}" string is returned */ + return $this->pg_array_parse($row[0], $schemas); + } + + /** + * Return search patch for schemas + * This is different from getSchemas() since it contain magic keywords + * (like "$user"). + * Needs transaction + * + * @since 1.19 + * @return array how to search for table names schemas for the current user + */ + function getSearchPath() { + $res = $this->query( "SHOW search_path", __METHOD__); + $row = $this->fetchRow( $res ); + /* PostgreSQL returns SHOW values as strings */ + return explode(",", $row[0]); + } + + /** + * Update search_path, values should already be sanitized + * Values may contain magic keywords like "$user" + * @since 1.19 + * + * @param $search_path array list of schemas to be searched by default + */ + function setSearchPath( $search_path ) { + $this->query( "SET search_path = " . implode(", ", $search_path) ); + } + + /** + * Determine default schema for MediaWiki core + * Adjust this session schema search path if desired schema exists + * and is not alread there. + * + * We need to have name of the core schema stored to be able + * to query database metadata. + * + * This will be also called by the installer after the schema is created + * + * @since 1.19 + * @param $desired_schema string + */ + function determineCoreSchema( $desired_schema ) { + $this->begin( __METHOD__ ); + if ( $this->schemaExists( $desired_schema ) ) { + if ( in_array( $desired_schema, $this->getSchemas() ) ) { + $this->mCoreSchema = $desired_schema; + wfDebug("Schema \"" . $desired_schema . "\" already in the search path\n"); + } else { + /** + * Prepend our schema (e.g. 'mediawiki') in front + * of the search path + * Fixes bug 15816 + */ + $search_path = $this->getSearchPath(); + array_unshift( $search_path, + $this->addIdentifierQuotes( $desired_schema )); + $this->setSearchPath( $search_path ); + $this->mCoreSchema = $desired_schema; + wfDebug("Schema \"" . $desired_schema . "\" added to the search path\n"); + } + } else { + $this->mCoreSchema = $this->getCurrentSchema(); + wfDebug("Schema \"" . $desired_schema . "\" not found, using current \"". $this->mCoreSchema ."\"\n"); + } + /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */ + $this->commit( __METHOD__ ); + } + + /** + * Return schema name fore core MediaWiki tables + * + * @since 1.19 + * @return string core schema name + */ + function getCoreSchema() { + return $this->mCoreSchema; + } + /** * @return string Version information from the database */ @@ -752,14 +1184,14 @@ class DatabasePostgres extends DatabaseBase { /** * Query whether a given relation exists (in the given schema, or the * default mw one if not given) + * @return bool */ function relationExists( $table, $types, $schema = false ) { - global $wgDBmwschema; if ( !is_array( $types ) ) { $types = array( $types ); } if ( !$schema ) { - $schema = $wgDBmwschema; + $schema = $this->getCoreSchema(); } $table = $this->realTableName( $table, 'raw' ); $etable = $this->addQuotes( $table ); @@ -775,6 +1207,7 @@ class DatabasePostgres extends DatabaseBase { /** * For backward compatibility, this function checks both tables and * views. + * @return bool */ function tableExists( $table, $fname = __METHOD__, $schema = false ) { return $this->relationExists( $table, array( 'r', 'v' ), $schema ); @@ -785,8 +1218,6 @@ class DatabasePostgres extends DatabaseBase { } function triggerExists( $table, $trigger ) { - global $wgDBmwschema; - $q = <<<SQL SELECT 1 FROM pg_class, pg_namespace, pg_trigger WHERE relnamespace=pg_namespace.oid AND relkind='r' @@ -796,7 +1227,7 @@ SQL; $res = $this->query( sprintf( $q, - $this->addQuotes( $wgDBmwschema ), + $this->addQuotes( $this->getCoreSchema() ), $this->addQuotes( $table ), $this->addQuotes( $trigger ) ) @@ -809,22 +1240,20 @@ SQL; } function ruleExists( $table, $rule ) { - global $wgDBmwschema; $exists = $this->selectField( 'pg_rules', 'rulename', array( 'rulename' => $rule, 'tablename' => $table, - 'schemaname' => $wgDBmwschema + 'schemaname' => $this->getCoreSchema() ) ); return $exists === $rule; } function constraintExists( $table, $constraint ) { - global $wgDBmwschema; $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints ". "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", - $this->addQuotes( $wgDBmwschema ), + $this->addQuotes( $this->getCoreSchema() ), $this->addQuotes( $table ), $this->addQuotes( $constraint ) ); @@ -838,6 +1267,7 @@ SQL; /** * Query whether a given schema exists. Returns true if it does, false if it doesn't. + * @return bool */ function schemaExists( $schema ) { $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1, @@ -847,6 +1277,7 @@ SQL; /** * Returns true if a given role (i.e. user) exists, false otherwise. + * @return bool */ function roleExists( $roleName ) { $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1, @@ -860,6 +1291,7 @@ SQL; /** * pg_field_type() wrapper + * @return string */ function fieldType( $res, $index ) { if ( $res instanceof ResultWrapper ) { @@ -868,11 +1300,6 @@ SQL; return pg_field_type( $res, $index ); } - /* Not even sure why this is used in the main codebase... */ - function limitResultForUpdate( $sql, $num ) { - return $sql; - } - /** * @param $b * @return Blob @@ -979,9 +1406,6 @@ SQL; if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { $postLimitTail .= ' FOR UPDATE'; } - if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) { - $postLimitTail .= ' LOCK IN SHARE MODE'; - } if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { $startOpts .= 'DISTINCT'; } diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index b2eb1c6b..f1e553d7 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -3,6 +3,21 @@ * This is the SQLite database abstraction layer. * See maintenance/sqlite/README for development notes and other specific information * + * 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 Database */ @@ -88,7 +103,7 @@ class DatabaseSqlite extends DatabaseBase { * * @param $fileName string * - * @return PDO|false SQL connection or false if failed + * @return PDO|bool SQL connection or false if failed */ function openFile( $fileName ) { $this->mDatabaseFile = $fileName; @@ -115,16 +130,11 @@ class DatabaseSqlite extends DatabaseBase { } /** - * Close an SQLite database - * + * Does not actually close the connection, just destroys the reference for GC to do its work * @return bool */ - function close() { - $this->mOpened = false; - if ( is_object( $this->mConn ) ) { - if ( $this->trxLevel() ) $this->commit(); - $this->mConn = null; - } + protected function closeConnection() { + $this->mConn = null; return true; } @@ -140,7 +150,7 @@ class DatabaseSqlite extends DatabaseBase { /** * Check if the searchindext table is FTS enabled. - * @return false if not enabled. + * @return bool False if not enabled. */ function checkForEnabledSearch() { if ( self::$fulltextEnabled === null ) { @@ -166,7 +176,7 @@ class DatabaseSqlite extends DatabaseBase { } $cachedResult = false; $table = 'dummy_search_test'; - + $db = new DatabaseSqliteStandalone( ':memory:' ); if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { @@ -303,7 +313,7 @@ class DatabaseSqlite extends DatabaseBase { /** * @param $res ResultWrapper - * @param $n + * @param $n * @return bool */ function fieldName( $res, $n ) { @@ -347,7 +357,8 @@ class DatabaseSqlite extends DatabaseBase { * @return int */ function insertId() { - return $this->mConn->lastInsertId(); + // PDO::lastInsertId yields a string :( + return intval( $this->mConn->lastInsertId() ); } /** @@ -497,6 +508,7 @@ class DatabaseSqlite extends DatabaseBase { /** * Based on generic method (parent) with some prior SQLite-sepcific adjustments + * @return bool */ function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) { if ( !count( $a ) ) { @@ -610,14 +622,16 @@ class DatabaseSqlite extends DatabaseBase { * @return string User-friendly database information */ public function getServerInfo() { - return wfMsg( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() ); + return wfMessage( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() )->text(); } /** * Get information about a given field * Returns false if the field does not exist. * - * @return SQLiteField|false + * @param $table string + * @param $field string + * @return SQLiteField|bool False on failure */ function fieldInfo( $table, $field ) { $tableName = $this->tableName( $table ); @@ -631,15 +645,15 @@ class DatabaseSqlite extends DatabaseBase { return false; } - function begin( $fname = '' ) { + protected function doBegin( $fname = '' ) { if ( $this->mTrxLevel == 1 ) { - $this->commit(); + $this->commit( __METHOD__ ); } $this->mConn->beginTransaction(); $this->mTrxLevel = 1; } - function commit( $fname = '' ) { + protected function doCommit( $fname = '' ) { if ( $this->mTrxLevel == 0 ) { return; } @@ -647,7 +661,7 @@ class DatabaseSqlite extends DatabaseBase { $this->mTrxLevel = 0; } - function rollback( $fname = '' ) { + protected function doRollback( $fname = '' ) { if ( $this->mTrxLevel == 0 ) { return; } @@ -656,15 +670,6 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @param $sql - * @param $num - * @return string - */ - function limitResultForUpdate( $sql, $num ) { - return $this->limitResult( $sql, $num ); - } - - /** * @param $s string * @return string */ @@ -723,6 +728,7 @@ class DatabaseSqlite extends DatabaseBase { /** * No-op version of deadlockLoop + * @return mixed */ public function deadlockLoop( /*...*/ ) { $args = func_get_args(); @@ -812,12 +818,12 @@ class DatabaseSqlite extends DatabaseBase { } return $this->query( $sql, $fname ); } - - + + /** * List all tables on the database * - * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $prefix string Only show tables with this prefix, e.g. mw_ * @param $fname String: calling function name * * @return array @@ -828,21 +834,21 @@ class DatabaseSqlite extends DatabaseBase { 'name', "type='table'" ); - + $endArray = array(); - - foreach( $result as $table ) { + + foreach( $result as $table ) { $vars = get_object_vars($table); $table = array_pop( $vars ); - + if( !$prefix || strpos( $table, $prefix ) === 0 ) { if ( strpos( $table, 'sqlite_' ) !== 0 ) { $endArray[] = $table; } - + } } - + return $endArray; } diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php index 0ea713c8..c846788d 100644 --- a/includes/db/DatabaseUtility.php +++ b/includes/db/DatabaseUtility.php @@ -1,5 +1,27 @@ <?php /** + * This file contains database-related utiliy classes. + * + * 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 Database + */ + +/** * Utility class. * @ingroup Database */ @@ -220,7 +242,11 @@ class FakeResultWrapper extends ResultWrapper { $this->currentRow = false; } $this->pos++; - return $this->currentRow; + if ( is_object( $this->currentRow ) ) { + return get_object_vars( $this->currentRow ); + } else { + return $this->currentRow; + } } function seek( $row ) { diff --git a/includes/db/IORMRow.php b/includes/db/IORMRow.php new file mode 100644 index 00000000..e99ba6cc --- /dev/null +++ b/includes/db/IORMRow.php @@ -0,0 +1,275 @@ +<?php +/** + * Interface for representing objects that are stored in some DB table. + * This is basically an ORM-like wrapper around rows in database tables that + * aims to be both simple and very flexible. It is centered around an associative + * array of fields and various methods to do common interaction with the database. + * + * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable + * + * 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 + * + * @since 1.20 + * + * @file + * @ingroup ORM + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +interface IORMRow { + + + /** + * Constructor. + * + * @since 1.20 + * + * @param IORMTable $table + * @param array|null $fields + * @param boolean $loadDefaults + */ + public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ); + + /** + * Load the specified fields from the database. + * + * @since 1.20 + * + * @param array|null $fields + * @param boolean $override + * @param boolean $skipLoaded + * + * @return bool Success indicator + */ + public function loadFields( $fields = null, $override = true, $skipLoaded = false ); + + /** + * Gets the value of a field. + * + * @since 1.20 + * + * @param string $name + * @param mixed $default + * + * @throws MWException + * @return mixed + */ + public function getField( $name, $default = null ); + + /** + * Gets the value of a field but first loads it if not done so already. + * + * @since 1.20 + * + * @param string$name + * + * @return mixed + */ + public function loadAndGetField( $name ); + + /** + * Remove a field. + * + * @since 1.20 + * + * @param string $name + */ + public function removeField( $name ); + + /** + * Returns the objects database id. + * + * @since 1.20 + * + * @return integer|null + */ + public function getId(); + + /** + * Sets the objects database id. + * + * @since 1.20 + * + * @param integer|null $id + */ + public function setId( $id ); + + /** + * Gets if a certain field is set. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function hasField( $name ); + + /** + * Gets if the id field is set. + * + * @since 1.20 + * + * @return boolean + */ + public function hasIdField(); + + /** + * Sets multiple fields. + * + * @since 1.20 + * + * @param array $fields The fields to set + * @param boolean $override Override already set fields with the provided values? + */ + public function setFields( array $fields, $override = true ); + + /** + * Serializes the object to an associative array which + * can then easily be converted into JSON or similar. + * + * @since 1.20 + * + * @param null|array $fields + * @param boolean $incNullId + * + * @return array + */ + public function toArray( $fields = null, $incNullId = false ); + + /** + * Load the default values, via getDefaults. + * + * @since 1.20 + * + * @param boolean $override + */ + public function loadDefaults( $override = true ); + + /** + * Writes the answer to the database, either updating it + * when it already exists, or inserting it when it doesn't. + * + * @since 1.20 + * + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function save( $functionName = null ); + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @return boolean Success indicator + */ + public function remove(); + + /** + * Return the names and values of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFields(); + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getSetFieldNames(); + + /** + * Sets the value of a field. + * Strings can be provided for other types, + * so this method can be called from unserialization handlers. + * + * @since 1.20 + * + * @param string $name + * @param mixed $value + * + * @throws MWException + */ + public function setField( $name, $value ); + + /** + * Add an amount (can be negative) to the specified field (needs to be numeric). + * TODO: most off this stuff makes more sense in the table class + * + * @since 1.20 + * + * @param string $field + * @param integer $amount + * + * @return boolean Success indicator + */ + public function addToField( $field, $amount ); + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames(); + + /** + * Computes and updates the values of the summary fields. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + */ + public function loadSummaryFields( $summaryFields = null ); + + /** + * Sets the value for the @see $updateSummaries field. + * + * @since 1.20 + * + * @param boolean $update + */ + public function setUpdateSummaries( $update ); + + /** + * Sets the value for the @see $inSummaryMode field. + * + * @since 1.20 + * + * @param boolean $summaryMode + */ + public function setSummaryMode( $summaryMode ); + + /** + * Returns the table this IORMRow is a row in. + * + * @since 1.20 + * + * @return IORMTable + */ + public function getTable(); + +}
\ No newline at end of file diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php new file mode 100644 index 00000000..99413f99 --- /dev/null +++ b/includes/db/IORMTable.php @@ -0,0 +1,448 @@ +<?php +/** + * Interface for objects representing a single database table. + * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable + * + * 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 + * + * @since 1.20 + * + * @file + * @ingroup ORM + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +interface IORMTable { + + /** + * Returns the name of the database table objects of this type are stored in. + * + * @since 1.20 + * + * @return string + */ + public function getName(); + + /** + * Returns the name of a IORMRow implementing class that + * represents single rows in this table. + * + * @since 1.20 + * + * @return string + */ + public function getRowClass(); + + /** + * Returns an array with the fields and their types this object contains. + * This corresponds directly to the fields in the database, without prefix. + * + * field name => type + * + * Allowed types: + * * id + * * str + * * int + * * float + * * bool + * * array + * * blob + * + * TODO: get rid of the id field. Every row instance needs to have + * one so this is just causing hassle at various locations by requiring an extra check for field name. + * + * @since 1.20 + * + * @return array + */ + public function getFields(); + + /** + * Returns a list of default field values. + * field name => field value + * + * @since 1.20 + * + * @return array + */ + public function getDefaults(); + + /** + * Returns a list of the summary fields. + * These are fields that cache computed values, such as the amount of linked objects of $type. + * This is relevant as one might not want to do actions such as log changes when these get updated. + * + * @since 1.20 + * + * @return array + */ + public function getSummaryFields(); + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ORMResult + */ + public function select( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return array of self + */ + public function selectObjects( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Do the actual select. + * + * @since 1.20 + * + * @param null|string|array $fields + * @param array $conditions + * @param array $options + * @param null|string $functionName + * + * @return ResultWrapper + */ + public function rawSelect( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as associative arrays. + * Provided field names get prefixed. + * Returned field names will not have a prefix. + * + * When $collapse is true: + * If one field is selected, each item in the result array will be this field. + * If two fields are selected, each item in the result array will have as key + * the first field and as value the second field. + * If more then two fields are selected, each item will be an associative array. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return array of array + */ + public function selectFields( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ); + + /** + * Selects the the specified fields of the first matching record. + * Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return IORMRow|bool False on failure + */ + public function selectRow( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the records matching the provided + * conditions. Field names do NOT get prefixed. + * + * @since 1.20 + * + * @param array $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ResultWrapper + */ + public function rawSelectRow( array $fields, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the first record matching the provided + * conditions and returns it as an associative array, or false when nothing matches. + * This method makes use of selectFields and expects the same parameters and + * returns the same results (if there are any, if there are none, this method returns false). + * @see IORMTable::selectFields + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return mixed|array|bool False on failure + */ + public function selectFieldsRow( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ); + + /** + * Returns if there is at least one record matching the provided conditions. + * Condition field names get prefixed. + * + * @since 1.20 + * + * @param array $conditions + * + * @return boolean + */ + public function has( array $conditions = array() ); + + /** + * Returns the amount of matching records. + * Condition field names get prefixed. + * + * Note that this can be expensive on large tables. + * In such cases you might want to use DatabaseBase::estimateRowCount instead. + * + * @since 1.20 + * + * @param array $conditions + * @param array $options + * + * @return integer + */ + public function count( array $conditions = array(), array $options = array() ); + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @param array $conditions + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function delete( array $conditions, $functionName = null ); + + /** + * Get API parameters for the fields supported by this object. + * + * @since 1.20 + * + * @param boolean $requireParams + * @param boolean $setDefaults + * + * @return array + */ + public function getAPIParams( $requireParams = false, $setDefaults = false ); + + /** + * Returns an array with the fields and their descriptions. + * + * field name => field description + * + * @since 1.20 + * + * @return array + */ + public function getFieldDescriptions(); + + /** + * Get the database type used for read operations. + * + * @since 1.20 + * + * @return integer DB_ enum + */ + public function getReadDb(); + + /** + * Set the database type to use for read operations. + * + * @param integer $db + * + * @since 1.20 + */ + public function setReadDb( $db ); + + /** + * Update the records matching the provided conditions by + * setting the fields that are keys in the $values param to + * their corresponding values. + * + * @since 1.20 + * + * @param array $values + * @param array $conditions + * + * @return boolean Success indicator + */ + public function update( array $values, array $conditions = array() ); + + /** + * Computes the values of the summary fields of the objects matching the provided conditions. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + * @param array $conditions + */ + public function updateSummaryFields( $summaryFields = null, array $conditions = array() ); + + /** + * Takes in an associative array with field names as keys and + * their values as value. The field names are prefixed with the + * db field prefix. + * + * @since 1.20 + * + * @param array $values + * + * @return array + */ + public function getPrefixedValues( array $values ); + + /** + * Takes in a field or array of fields and returns an + * array with their prefixed versions, ready for db usage. + * + * @since 1.20 + * + * @param array|string $fields + * + * @return array + */ + public function getPrefixedFields( array $fields ); + + /** + * Takes in a field and returns an it's prefixed version, ready for db usage. + * + * @since 1.20 + * + * @param string|array $field + * + * @return string + */ + public function getPrefixedField( $field ); + + /** + * Takes an array of field names with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param array $fieldNames + * + * @return array + */ + public function unprefixFieldNames( array $fieldNames ); + + /** + * Takes a field name with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param string $fieldName + * + * @return string + */ + public function unprefixFieldName( $fieldName ); + + /** + * Get an instance of this class. + * + * @since 1.20 + * + * @return IORMTable + */ + public static function singleton(); + + /** + * Get an array with fields from a database result, + * that can be fed directly to the constructor or + * to setFields. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return array + */ + public function getFieldsFromDBResult( stdClass $result ); + + /** + * Get a new instance of the class from a database result. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return IORMRow + */ + public function newRowFromDBResult( stdClass $result ); + + /** + * Get a new instance of the class from an array. + * + * @since 1.20 + * + * @param array $data + * @param boolean $loadDefaults + * + * @return IORMRow + */ + public function newRow( array $data, $loadDefaults = false ); + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames(); + + /** + * Gets if the object can take a certain field. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function canHaveField( $name ); + +} diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index dec6ae16..e82c54ba 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -1,6 +1,21 @@ <?php /** - * Generator of database load balancing objects + * Generator of database load balancing objects. + * + * 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 Database @@ -176,6 +191,16 @@ class LBFactory_Simple extends LBFactory { $servers = $wgDBservers; } else { global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql; + global $wgDBssl, $wgDBcompress; + + $flags = ( $wgDebugDumpSql ? DBO_DEBUG : 0 ) | DBO_DEFAULT; + if ( $wgDBssl ) { + $flags |= DBO_SSL; + } + if ( $wgDBcompress ) { + $flags |= DBO_COMPRESS; + } + $servers = array(array( 'host' => $wgDBserver, 'user' => $wgDBuser, @@ -183,7 +208,7 @@ class LBFactory_Simple extends LBFactory { 'dbname' => $wgDBname, 'type' => $wgDBtype, 'load' => 1, - 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT + 'flags' => $flags )); } diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php index b7977a21..6008813b 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactory_Multi.php @@ -1,6 +1,21 @@ <?php /** - * Advanced generator of database load balancing objects for wiki farms + * Advanced generator of database load balancing objects for wiki farms. + * + * 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 Database diff --git a/includes/db/LBFactory_Single.php b/includes/db/LBFactory_Single.php index f80aa4bc..4b165b2a 100644 --- a/includes/db/LBFactory_Single.php +++ b/includes/db/LBFactory_Single.php @@ -1,4 +1,25 @@ <?php +/** + * Simple generator of database connections that always returns the same object. + * + * 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 Database + */ /** * An LBFactory class that always returns a single database object. diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index e96c6720..0e455e0c 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -1,6 +1,21 @@ <?php /** - * Database load balancing + * Database load balancing. + * + * 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 Database @@ -91,7 +106,7 @@ class LoadBalancer { /** * Get or set arbitrary data used by the parent object, usually an LBFactory * @param $x - * @return \Mixed + * @return Mixed */ function parentInfo( $x = null ) { return wfSetVar( $this->mParentInfo, $x ); @@ -385,7 +400,7 @@ class LoadBalancer { * Returns false if there is no connection open * * @param $i int - * @return DatabaseBase|false + * @return DatabaseBase|bool False on failure */ function getAnyOpenConnection( $i ) { foreach ( $this->mConns as $conns ) { @@ -894,7 +909,7 @@ class LoadBalancer { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { foreach ( $conns3 as $conn ) { - $conn->commit(); + $conn->commit( __METHOD__ ); } } } @@ -911,7 +926,7 @@ class LoadBalancer { continue; } foreach ( $conns2[$masterIndex] as $conn ) { - if ( $conn->doneWrites() ) { + if ( $conn->writesOrCallbacksPending() ) { $conn->commit( __METHOD__ ); } } diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 16a0343f..146ac61e 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -1,6 +1,21 @@ <?php /** - * Database load monitoring + * Database load monitoring. + * + * 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 Database diff --git a/includes/db/ORMIterator.php b/includes/db/ORMIterator.php new file mode 100644 index 00000000..090b8932 --- /dev/null +++ b/includes/db/ORMIterator.php @@ -0,0 +1,31 @@ +<?php + +/** + * Interface for Iterators containing IORMRows. + * + * 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 + * + * @since 1.20 + * + * @file + * @ingroup ORM + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +interface ORMIterator extends Iterator { + +}
\ No newline at end of file diff --git a/includes/db/ORMResult.php b/includes/db/ORMResult.php new file mode 100644 index 00000000..2a5837a1 --- /dev/null +++ b/includes/db/ORMResult.php @@ -0,0 +1,123 @@ +<?php +/** + * ORMIterator that takes a ResultWrapper object returned from + * a select operation returning IORMRow objects (ie IORMTable::select). + * + * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable + * + * 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 + * + * @since 1.20 + * + * @file ORMResult.php + * @ingroup ORM + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +class ORMResult implements ORMIterator { + + /** + * @var ResultWrapper + */ + protected $res; + + /** + * @var integer + */ + protected $key; + + /** + * @var IORMRow + */ + protected $current; + + /** + * @var IORMTable + */ + protected $table; + + /** + * @param IORMTable $table + * @param ResultWrapper $res + */ + public function __construct( IORMTable $table, ResultWrapper $res ) { + $this->table = $table; + $this->res = $res; + $this->key = 0; + $this->setCurrent( $this->res->current() ); + } + + /** + * @param $row + */ + protected function setCurrent( $row ) { + if ( $row === false ) { + $this->current = false; + } else { + $this->current = $this->table->newRowFromDBResult( $row ); + } + } + + /** + * @return integer + */ + public function count() { + return $this->res->numRows(); + } + + /** + * @return boolean + */ + public function isEmpty() { + return $this->res->numRows() === 0; + } + + /** + * @return IORMRow + */ + public function current() { + return $this->current; + } + + /** + * @return integer + */ + public function key() { + return $this->key; + } + + public function next() { + $row = $this->res->next(); + $this->setCurrent( $row ); + $this->key++; + } + + public function rewind() { + $this->res->rewind(); + $this->key = 0; + $this->setCurrent( $this->res->current() ); + } + + /** + * @return boolean + */ + public function valid() { + return $this->current !== false; + } + +} diff --git a/includes/db/ORMRow.php b/includes/db/ORMRow.php new file mode 100644 index 00000000..303f3a20 --- /dev/null +++ b/includes/db/ORMRow.php @@ -0,0 +1,663 @@ +<?php +/** + * Abstract base class for representing objects that are stored in some DB table. + * This is basically an ORM-like wrapper around rows in database tables that + * aims to be both simple and very flexible. It is centered around an associative + * array of fields and various methods to do common interaction with the database. + * + * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable + * + * 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 + * + * @since 1.20 + * + * @file ORMRow.php + * @ingroup ORM + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +abstract class ORMRow implements IORMRow { + + /** + * The fields of the object. + * field name (w/o prefix) => value + * + * @since 1.20 + * @var array + */ + protected $fields = array( 'id' => null ); + + /** + * @since 1.20 + * @var ORMTable + */ + protected $table; + + /** + * If the object should update summaries of linked items when changed. + * For example, update the course_count field in universities when a course in courses is deleted. + * Settings this to false can prevent needless updating work in situations + * such as deleting a university, which will then delete all it's courses. + * + * @since 1.20 + * @var bool + */ + protected $updateSummaries = true; + + /** + * Indicates if the object is in summary mode. + * This mode indicates that only summary fields got updated, + * which allows for optimizations. + * + * @since 1.20 + * @var bool + */ + protected $inSummaryMode = false; + + /** + * Constructor. + * + * @since 1.20 + * + * @param IORMTable $table + * @param array|null $fields + * @param boolean $loadDefaults + */ + public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ) { + $this->table = $table; + + if ( !is_array( $fields ) ) { + $fields = array(); + } + + if ( $loadDefaults ) { + $fields = array_merge( $this->table->getDefaults(), $fields ); + } + + $this->setFields( $fields ); + } + + /** + * Load the specified fields from the database. + * + * @since 1.20 + * + * @param array|null $fields + * @param boolean $override + * @param boolean $skipLoaded + * + * @return bool Success indicator + */ + public function loadFields( $fields = null, $override = true, $skipLoaded = false ) { + if ( is_null( $this->getId() ) ) { + return false; + } + + if ( is_null( $fields ) ) { + $fields = array_keys( $this->table->getFields() ); + } + + if ( $skipLoaded ) { + $fields = array_diff( $fields, array_keys( $this->fields ) ); + } + + if ( !empty( $fields ) ) { + $result = $this->table->rawSelectRow( + $this->table->getPrefixedFields( $fields ), + array( $this->table->getPrefixedField( 'id' ) => $this->getId() ), + array( 'LIMIT' => 1 ) + ); + + if ( $result !== false ) { + $this->setFields( $this->table->getFieldsFromDBResult( $result ), $override ); + return true; + } + return false; + } + + return true; + } + + /** + * Gets the value of a field. + * + * @since 1.20 + * + * @param string $name + * @param mixed $default + * + * @throws MWException + * @return mixed + */ + public function getField( $name, $default = null ) { + if ( $this->hasField( $name ) ) { + return $this->fields[$name]; + } elseif ( !is_null( $default ) ) { + return $default; + } else { + throw new MWException( 'Attempted to get not-set field ' . $name ); + } + } + + /** + * Gets the value of a field but first loads it if not done so already. + * + * @since 1.20 + * + * @param string$name + * + * @return mixed + */ + public function loadAndGetField( $name ) { + if ( !$this->hasField( $name ) ) { + $this->loadFields( array( $name ) ); + } + + return $this->getField( $name ); + } + + /** + * Remove a field. + * + * @since 1.20 + * + * @param string $name + */ + public function removeField( $name ) { + unset( $this->fields[$name] ); + } + + /** + * Returns the objects database id. + * + * @since 1.20 + * + * @return integer|null + */ + public function getId() { + return $this->getField( 'id' ); + } + + /** + * Sets the objects database id. + * + * @since 1.20 + * + * @param integer|null $id + */ + public function setId( $id ) { + $this->setField( 'id', $id ); + } + + /** + * Gets if a certain field is set. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function hasField( $name ) { + return array_key_exists( $name, $this->fields ); + } + + /** + * Gets if the id field is set. + * + * @since 1.20 + * + * @return boolean + */ + public function hasIdField() { + return $this->hasField( 'id' ) + && !is_null( $this->getField( 'id' ) ); + } + + /** + * Sets multiple fields. + * + * @since 1.20 + * + * @param array $fields The fields to set + * @param boolean $override Override already set fields with the provided values? + */ + public function setFields( array $fields, $override = true ) { + foreach ( $fields as $name => $value ) { + if ( $override || !$this->hasField( $name ) ) { + $this->setField( $name, $value ); + } + } + } + + /** + * Gets the fields => values to write to the table. + * + * @since 1.20 + * + * @return array + */ + protected function getWriteValues() { + $values = array(); + + foreach ( $this->table->getFields() as $name => $type ) { + if ( array_key_exists( $name, $this->fields ) ) { + $value = $this->fields[$name]; + + switch ( $type ) { + case 'array': + $value = (array)$value; + case 'blob': + $value = serialize( $value ); + } + + $values[$this->table->getPrefixedField( $name )] = $value; + } + } + + return $values; + } + + /** + * Serializes the object to an associative array which + * can then easily be converted into JSON or similar. + * + * @since 1.20 + * + * @param null|array $fields + * @param boolean $incNullId + * + * @return array + */ + public function toArray( $fields = null, $incNullId = false ) { + $data = array(); + $setFields = array(); + + if ( !is_array( $fields ) ) { + $setFields = $this->getSetFieldNames(); + } else { + foreach ( $fields as $field ) { + if ( $this->hasField( $field ) ) { + $setFields[] = $field; + } + } + } + + foreach ( $setFields as $field ) { + if ( $incNullId || $field != 'id' || $this->hasIdField() ) { + $data[$field] = $this->getField( $field ); + } + } + + return $data; + } + + /** + * Load the default values, via getDefaults. + * + * @since 1.20 + * + * @param boolean $override + */ + public function loadDefaults( $override = true ) { + $this->setFields( $this->table->getDefaults(), $override ); + } + + /** + * Writes the answer to the database, either updating it + * when it already exists, or inserting it when it doesn't. + * + * @since 1.20 + * + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function save( $functionName = null ) { + if ( $this->hasIdField() ) { + return $this->saveExisting( $functionName ); + } else { + return $this->insert( $functionName ); + } + } + + /** + * Updates the object in the database. + * + * @since 1.20 + * + * @param string|null $functionName + * + * @return boolean Success indicator + */ + protected function saveExisting( $functionName = null ) { + $dbw = wfGetDB( DB_MASTER ); + + $success = $dbw->update( + $this->table->getName(), + $this->getWriteValues(), + $this->table->getPrefixedValues( $this->getUpdateConditions() ), + is_null( $functionName ) ? __METHOD__ : $functionName + ); + + // DatabaseBase::update does not always return true for success as documented... + return $success !== false; + } + + /** + * Returns the WHERE considtions needed to identify this object so + * it can be updated. + * + * @since 1.20 + * + * @return array + */ + protected function getUpdateConditions() { + return array( 'id' => $this->getId() ); + } + + /** + * Inserts the object into the database. + * + * @since 1.20 + * + * @param string|null $functionName + * @param array|null $options + * + * @return boolean Success indicator + */ + protected function insert( $functionName = null, array $options = null ) { + $dbw = wfGetDB( DB_MASTER ); + + $success = $dbw->insert( + $this->table->getName(), + $this->getWriteValues(), + is_null( $functionName ) ? __METHOD__ : $functionName, + is_null( $options ) ? array( 'IGNORE' ) : $options + ); + + // DatabaseBase::insert does not always return true for success as documented... + $success = $success !== false; + + if ( $success ) { + $this->setField( 'id', $dbw->insertId() ); + } + + return $success; + } + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @return boolean Success indicator + */ + public function remove() { + $this->beforeRemove(); + + $success = $this->table->delete( array( 'id' => $this->getId() ) ); + + // DatabaseBase::delete does not always return true for success as documented... + $success = $success !== false; + + if ( $success ) { + $this->onRemoved(); + } + + return $success; + } + + /** + * Gets called before an object is removed from the database. + * + * @since 1.20 + */ + protected function beforeRemove() { + $this->loadFields( $this->getBeforeRemoveFields(), false, true ); + } + + /** + * Before removal of an object happens, @see beforeRemove gets called. + * This method loads the fields of which the names have been returned by this one (or all fields if null is returned). + * This allows for loading info needed after removal to get rid of linked data and the like. + * + * @since 1.20 + * + * @return array|null + */ + protected function getBeforeRemoveFields() { + return array(); + } + + /** + * Gets called after successfull removal. + * Can be overriden to get rid of linked data. + * + * @since 1.20 + */ + protected function onRemoved() { + $this->setField( 'id', null ); + } + + /** + * Return the names and values of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFields() { + return $this->fields; + } + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getSetFieldNames() { + return array_keys( $this->fields ); + } + + /** + * Sets the value of a field. + * Strings can be provided for other types, + * so this method can be called from unserialization handlers. + * + * @since 1.20 + * + * @param string $name + * @param mixed $value + * + * @throws MWException + */ + public function setField( $name, $value ) { + $fields = $this->table->getFields(); + + if ( array_key_exists( $name, $fields ) ) { + switch ( $fields[$name] ) { + case 'int': + $value = (int)$value; + break; + case 'float': + $value = (float)$value; + break; + case 'bool': + if ( is_string( $value ) ) { + $value = $value !== '0'; + } elseif ( is_int( $value ) ) { + $value = $value !== 0; + } + break; + case 'array': + if ( is_string( $value ) ) { + $value = unserialize( $value ); + } + + if ( !is_array( $value ) ) { + $value = array(); + } + break; + case 'blob': + if ( is_string( $value ) ) { + $value = unserialize( $value ); + } + break; + case 'id': + if ( is_string( $value ) ) { + $value = (int)$value; + } + break; + } + + $this->fields[$name] = $value; + } else { + throw new MWException( 'Attempted to set unknown field ' . $name ); + } + } + + /** + * Add an amount (can be negative) to the specified field (needs to be numeric). + * TODO: most off this stuff makes more sense in the table class + * + * @since 1.20 + * + * @param string $field + * @param integer $amount + * + * @return boolean Success indicator + */ + public function addToField( $field, $amount ) { + if ( $amount == 0 ) { + return true; + } + + if ( !$this->hasIdField() ) { + return false; + } + + $absoluteAmount = abs( $amount ); + $isNegative = $amount < 0; + + $dbw = wfGetDB( DB_MASTER ); + + $fullField = $this->table->getPrefixedField( $field ); + + $success = $dbw->update( + $this->table->getName(), + array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ), + array( $this->table->getPrefixedField( 'id' ) => $this->getId() ), + __METHOD__ + ); + + if ( $success && $this->hasField( $field ) ) { + $this->setField( $field, $this->getField( $field ) + $amount ); + } + + return $success; + } + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames() { + return array_keys( $this->table->getFields() ); + } + + /** + * Computes and updates the values of the summary fields. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + */ + public function loadSummaryFields( $summaryFields = null ) { + + } + + /** + * Sets the value for the @see $updateSummaries field. + * + * @since 1.20 + * + * @param boolean $update + */ + public function setUpdateSummaries( $update ) { + $this->updateSummaries = $update; + } + + /** + * Sets the value for the @see $inSummaryMode field. + * + * @since 1.20 + * + * @param boolean $summaryMode + */ + public function setSummaryMode( $summaryMode ) { + $this->inSummaryMode = $summaryMode; + } + + /** + * Return if any fields got changed. + * + * @since 1.20 + * + * @param IORMRow $object + * @param boolean|array $excludeSummaryFields + * When set to true, summary field changes are ignored. + * Can also be an array of fields to ignore. + * + * @return boolean + */ + protected function fieldsChanged( IORMRow $object, $excludeSummaryFields = false ) { + $exclusionFields = array(); + + if ( $excludeSummaryFields !== false ) { + $exclusionFields = is_array( $excludeSummaryFields ) ? $excludeSummaryFields : $this->table->getSummaryFields(); + } + + foreach ( $this->fields as $name => $value ) { + $excluded = $excludeSummaryFields && in_array( $name, $exclusionFields ); + + if ( !$excluded && $object->getField( $name ) !== $value ) { + return true; + } + } + + return false; + } + + /** + * Returns the table this IORMRow is a row in. + * + * @since 1.20 + * + * @return IORMTable + */ + public function getTable() { + return $this->table; + } + +} diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php new file mode 100644 index 00000000..a77074ff --- /dev/null +++ b/includes/db/ORMTable.php @@ -0,0 +1,675 @@ +<?php +/** + * Abstract base class for representing a single database table. + * Documentation inline and at https://www.mediawiki.org/wiki/Manual:ORMTable + * + * 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 + * + * @since 1.20 + * + * @file ORMTable.php + * @ingroup ORM + * + * @licence GNU GPL v2 or later + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +abstract class ORMTable implements IORMTable { + + /** + * Gets the db field prefix. + * + * @since 1.20 + * + * @return string + */ + protected abstract function getFieldPrefix(); + + /** + * Cache for instances, used by the singleton method. + * + * @since 1.20 + * @var array of DBTable + */ + protected static $instanceCache = array(); + + /** + * The database connection to use for read operations. + * Can be changed via @see setReadDb. + * + * @since 1.20 + * @var integer DB_ enum + */ + protected $readDb = DB_SLAVE; + + /** + * Returns a list of default field values. + * field name => field value + * + * @since 1.20 + * + * @return array + */ + public function getDefaults() { + return array(); + } + + /** + * Returns a list of the summary fields. + * These are fields that cache computed values, such as the amount of linked objects of $type. + * This is relevant as one might not want to do actions such as log changes when these get updated. + * + * @since 1.20 + * + * @return array + */ + public function getSummaryFields() { + return array(); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ORMResult + */ + public function select( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + return new ORMResult( $this, $this->rawSelect( $fields, $conditions, $options, $functionName ) ); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return array of self + */ + public function selectObjects( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + $result = $this->selectFields( $fields, $conditions, $options, false, $functionName ); + + $objects = array(); + + foreach ( $result as $record ) { + $objects[] = $this->newRow( $record ); + } + + return $objects; + } + + /** + * Do the actual select. + * + * @since 1.20 + * + * @param null|string|array $fields + * @param array $conditions + * @param array $options + * @param null|string $functionName + * + * @return ResultWrapper + */ + public function rawSelect( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + if ( is_null( $fields ) ) { + $fields = array_keys( $this->getFields() ); + } + else { + $fields = (array)$fields; + } + + return wfGetDB( $this->getReadDb() )->select( + $this->getName(), + $this->getPrefixedFields( $fields ), + $this->getPrefixedValues( $conditions ), + is_null( $functionName ) ? __METHOD__ : $functionName, + $options + ); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as associative arrays. + * Provided field names get prefixed. + * Returned field names will not have a prefix. + * + * When $collapse is true: + * If one field is selected, each item in the result array will be this field. + * If two fields are selected, each item in the result array will have as key + * the first field and as value the second field. + * If more then two fields are selected, each item will be an associative array. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return array of array + */ + public function selectFields( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ) { + $objects = array(); + + $result = $this->rawSelect( $fields, $conditions, $options, $functionName ); + + foreach ( $result as $record ) { + $objects[] = $this->getFieldsFromDBResult( $record ); + } + + if ( $collapse ) { + if ( count( $fields ) === 1 ) { + $objects = array_map( 'array_shift', $objects ); + } + elseif ( count( $fields ) === 2 ) { + $o = array(); + + foreach ( $objects as $object ) { + $o[array_shift( $object )] = array_shift( $object ); + } + + $objects = $o; + } + } + + return $objects; + } + + /** + * Selects the the specified fields of the first matching record. + * Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return IORMRow|bool False on failure + */ + public function selectRow( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + $options['LIMIT'] = 1; + + $objects = $this->select( $fields, $conditions, $options, $functionName ); + + return $objects->isEmpty() ? false : $objects->current(); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions. Field names do NOT get prefixed. + * + * @since 1.20 + * + * @param array $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ResultWrapper + */ + public function rawSelectRow( array $fields, array $conditions = array(), + array $options = array(), $functionName = null ) { + $dbr = wfGetDB( $this->getReadDb() ); + + return $dbr->selectRow( + $this->getName(), + $fields, + $conditions, + is_null( $functionName ) ? __METHOD__ : $functionName, + $options + ); + } + + /** + * Selects the the specified fields of the first record matching the provided + * conditions and returns it as an associative array, or false when nothing matches. + * This method makes use of selectFields and expects the same parameters and + * returns the same results (if there are any, if there are none, this method returns false). + * @see ORMTable::selectFields + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return mixed|array|bool False on failure + */ + public function selectFieldsRow( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ) { + $options['LIMIT'] = 1; + + $objects = $this->selectFields( $fields, $conditions, $options, $collapse, $functionName ); + + return empty( $objects ) ? false : $objects[0]; + } + + /** + * Returns if there is at least one record matching the provided conditions. + * Condition field names get prefixed. + * + * @since 1.20 + * + * @param array $conditions + * + * @return boolean + */ + public function has( array $conditions = array() ) { + return $this->selectRow( array( 'id' ), $conditions ) !== false; + } + + /** + * Returns the amount of matching records. + * Condition field names get prefixed. + * + * Note that this can be expensive on large tables. + * In such cases you might want to use DatabaseBase::estimateRowCount instead. + * + * @since 1.20 + * + * @param array $conditions + * @param array $options + * + * @return integer + */ + public function count( array $conditions = array(), array $options = array() ) { + $res = $this->rawSelectRow( + array( 'rowcount' => 'COUNT(*)' ), + $this->getPrefixedValues( $conditions ), + $options + ); + + return $res->rowcount; + } + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @param array $conditions + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function delete( array $conditions, $functionName = null ) { + return wfGetDB( DB_MASTER )->delete( + $this->getName(), + $conditions === array() ? '*' : $this->getPrefixedValues( $conditions ), + $functionName + ) !== false; // DatabaseBase::delete does not always return true for success as documented... + } + + /** + * Get API parameters for the fields supported by this object. + * + * @since 1.20 + * + * @param boolean $requireParams + * @param boolean $setDefaults + * + * @return array + */ + public function getAPIParams( $requireParams = false, $setDefaults = false ) { + $typeMap = array( + 'id' => 'integer', + 'int' => 'integer', + 'float' => 'NULL', + 'str' => 'string', + 'bool' => 'integer', + 'array' => 'string', + 'blob' => 'string', + ); + + $params = array(); + $defaults = $this->getDefaults(); + + foreach ( $this->getFields() as $field => $type ) { + if ( $field == 'id' ) { + continue; + } + + $hasDefault = array_key_exists( $field, $defaults ); + + $params[$field] = array( + ApiBase::PARAM_TYPE => $typeMap[$type], + ApiBase::PARAM_REQUIRED => $requireParams && !$hasDefault + ); + + if ( $type == 'array' ) { + $params[$field][ApiBase::PARAM_ISMULTI] = true; + } + + if ( $setDefaults && $hasDefault ) { + $default = is_array( $defaults[$field] ) ? implode( '|', $defaults[$field] ) : $defaults[$field]; + $params[$field][ApiBase::PARAM_DFLT] = $default; + } + } + + return $params; + } + + /** + * Returns an array with the fields and their descriptions. + * + * field name => field description + * + * @since 1.20 + * + * @return array + */ + public function getFieldDescriptions() { + return array(); + } + + /** + * Get the database type used for read operations. + * + * @since 1.20 + * + * @return integer DB_ enum + */ + public function getReadDb() { + return $this->readDb; + } + + /** + * Set the database type to use for read operations. + * + * @param integer $db + * + * @since 1.20 + */ + public function setReadDb( $db ) { + $this->readDb = $db; + } + + /** + * Update the records matching the provided conditions by + * setting the fields that are keys in the $values param to + * their corresponding values. + * + * @since 1.20 + * + * @param array $values + * @param array $conditions + * + * @return boolean Success indicator + */ + public function update( array $values, array $conditions = array() ) { + $dbw = wfGetDB( DB_MASTER ); + + return $dbw->update( + $this->getName(), + $this->getPrefixedValues( $values ), + $this->getPrefixedValues( $conditions ), + __METHOD__ + ) !== false; // DatabaseBase::update does not always return true for success as documented... + } + + /** + * Computes the values of the summary fields of the objects matching the provided conditions. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + * @param array $conditions + */ + public function updateSummaryFields( $summaryFields = null, array $conditions = array() ) { + $this->setReadDb( DB_MASTER ); + + /** + * @var IORMRow $item + */ + foreach ( $this->select( null, $conditions ) as $item ) { + $item->loadSummaryFields( $summaryFields ); + $item->setSummaryMode( true ); + $item->save(); + } + + $this->setReadDb( DB_SLAVE ); + } + + /** + * Takes in an associative array with field names as keys and + * their values as value. The field names are prefixed with the + * db field prefix. + * + * @since 1.20 + * + * @param array $values + * + * @return array + */ + public function getPrefixedValues( array $values ) { + $prefixedValues = array(); + + foreach ( $values as $field => $value ) { + if ( is_integer( $field ) ) { + if ( is_array( $value ) ) { + $field = $value[0]; + $value = $value[1]; + } + else { + $value = explode( ' ', $value, 2 ); + $value[0] = $this->getPrefixedField( $value[0] ); + $prefixedValues[] = implode( ' ', $value ); + continue; + } + } + + $prefixedValues[$this->getPrefixedField( $field )] = $value; + } + + return $prefixedValues; + } + + /** + * Takes in a field or array of fields and returns an + * array with their prefixed versions, ready for db usage. + * + * @since 1.20 + * + * @param array|string $fields + * + * @return array + */ + public function getPrefixedFields( array $fields ) { + foreach ( $fields as &$field ) { + $field = $this->getPrefixedField( $field ); + } + + return $fields; + } + + /** + * Takes in a field and returns an it's prefixed version, ready for db usage. + * + * @since 1.20 + * + * @param string|array $field + * + * @return string + */ + public function getPrefixedField( $field ) { + return $this->getFieldPrefix() . $field; + } + + /** + * Takes an array of field names with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param array $fieldNames + * + * @return array + */ + public function unprefixFieldNames( array $fieldNames ) { + return array_map( array( $this, 'unprefixFieldName' ), $fieldNames ); + } + + /** + * Takes a field name with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param string $fieldName + * + * @return string + */ + public function unprefixFieldName( $fieldName ) { + return substr( $fieldName, strlen( $this->getFieldPrefix() ) ); + } + + /** + * Get an instance of this class. + * + * @since 1.20 + * + * @return IORMTable + */ + public static function singleton() { + $class = get_called_class(); + + if ( !array_key_exists( $class, self::$instanceCache ) ) { + self::$instanceCache[$class] = new $class; + } + + return self::$instanceCache[$class]; + } + + /** + * Get an array with fields from a database result, + * that can be fed directly to the constructor or + * to setFields. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return array + */ + public function getFieldsFromDBResult( stdClass $result ) { + $result = (array)$result; + return array_combine( + $this->unprefixFieldNames( array_keys( $result ) ), + array_values( $result ) + ); + } + + /** + * @see ORMTable::newRowFromFromDBResult + * + * @deprecated use newRowFromDBResult instead + * @since 1.20 + * + * @param stdClass $result + * + * @return IORMRow + */ + public function newFromDBResult( stdClass $result ) { + return self::newRowFromDBResult( $result ); + } + + /** + * Get a new instance of the class from a database result. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return IORMRow + */ + public function newRowFromDBResult( stdClass $result ) { + return $this->newRow( $this->getFieldsFromDBResult( $result ) ); + } + + /** + * @see ORMTable::newRow + * + * @deprecated use newRow instead + * @since 1.20 + * + * @param array $data + * @param boolean $loadDefaults + * + * @return IORMRow + */ + public function newFromArray( array $data, $loadDefaults = false ) { + return static::newRow( $data, $loadDefaults ); + } + + /** + * Get a new instance of the class from an array. + * + * @since 1.20 + * + * @param array $data + * @param boolean $loadDefaults + * + * @return IORMRow + */ + public function newRow( array $data, $loadDefaults = false ) { + $class = $this->getRowClass(); + return new $class( $this, $data, $loadDefaults ); + } + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames() { + return array_keys( $this->getFields() ); + } + + /** + * Gets if the object can take a certain field. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function canHaveField( $name ) { + return array_key_exists( $name, $this->getFields() ); + } + +} diff --git a/includes/debug/Debug.php b/includes/debug/Debug.php index de50ccac..d02bcf53 100644 --- a/includes/debug/Debug.php +++ b/includes/debug/Debug.php @@ -1,4 +1,24 @@ <?php +/** + * Debug toolbar related code + * + * 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 + */ /** * New debugger system that outputs a toolbar on page view @@ -7,6 +27,8 @@ * to explicitly call MWDebug::init() to enabled them. * * @todo Profiler support + * + * @since 1.19 */ class MWDebug { @@ -49,6 +71,8 @@ class MWDebug { /** * Enabled the debugger and load resource module. * This is called by Setup.php when $wgDebugToolbar is true. + * + * @since 1.19 */ public static function init() { self::$enabled = true; @@ -58,6 +82,7 @@ class MWDebug { * Add ResourceLoader modules to the OutputPage object if debugging is * enabled. * + * @since 1.19 * @param $out OutputPage */ public static function addModules( OutputPage $out ) { @@ -71,6 +96,7 @@ class MWDebug { * * @todo Add support for passing objects * + * @since 1.19 * @param $str string */ public static function log( $str ) { @@ -87,6 +113,8 @@ class MWDebug { /** * Returns internal log array + * @since 1.19 + * @return array */ public static function getLog() { return self::$log; @@ -94,6 +122,7 @@ class MWDebug { /** * Clears internal log array and deprecation tracking + * @since 1.19 */ public static function clearLog() { self::$log = array(); @@ -103,87 +132,178 @@ class MWDebug { /** * Adds a warning entry to the log * - * @param $msg - * @param int $callerOffset + * @since 1.19 + * @param $msg string + * @param $callerOffset int * @return mixed */ - public static function warning( $msg, $callerOffset = 1 ) { - if ( !self::$enabled ) { - return; + public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { + $callerDescription = self::getCallerDescription( $callerOffset ); + + self::sendWarning( $msg, $callerDescription, $level ); + + if ( self::$enabled ) { + self::$log[] = array( + 'msg' => htmlspecialchars( $msg ), + 'type' => 'warn', + 'caller' => $callerDescription['func'], + ); } + } + + /** + * Show a warning that $function is deprecated. + * This will send it to the following locations: + * - Debug toolbar, with one item per function and caller, if $wgDebugToolbar + * is set to true. + * - PHP's error log, with level E_USER_DEPRECATED, if $wgDevelopmentWarnings + * is set to true. + * - MediaWiki's debug log, if $wgDevelopmentWarnings is set to false. + * + * @since 1.19 + * @param $function string: Function that is deprecated. + * @param $version string|bool: Version in which the function was deprecated. + * @param $component string|bool: Component to which the function belongs. + * If false, it is assumbed the function is in MediaWiki core. + * @param $callerOffset integer: How far up the callstack is the original + * caller. 2 = function that called the function that called + * MWDebug::deprecated() (Added in 1.20). + * @return mixed + */ + public static function deprecated( $function, $version = false, $component = false, $callerOffset = 2 ) { + $callerDescription = self::getCallerDescription( $callerOffset ); + $callerFunc = $callerDescription['func']; + + $sendToLog = true; - // Check to see if there was already a deprecation notice, so not to - // get a duplicate warning - $logCount = count( self::$log ); - if ( $logCount ) { - $lastLog = self::$log[ $logCount - 1 ]; - if ( $lastLog['type'] == 'deprecated' && $lastLog['caller'] == wfGetCaller( $callerOffset + 1 ) ) { + // Check to see if there already was a warning about this function + if ( isset( self::$deprecationWarnings[$function][$callerFunc] ) ) { + return; + } elseif ( isset( self::$deprecationWarnings[$function] ) ) { + if ( self::$enabled ) { + $sendToLog = false; + } else { return; } } - self::$log[] = array( - 'msg' => htmlspecialchars( $msg ), - 'type' => 'warn', - 'caller' => wfGetCaller( $callerOffset ), - ); + self::$deprecationWarnings[$function][$callerFunc] = true; + + if ( $version ) { + global $wgDeprecationReleaseLimit; + if ( $wgDeprecationReleaseLimit && $component === false ) { + # Strip -* off the end of $version so that branches can use the + # format #.##-branchname to avoid issues if the branch is merged into + # a version of MediaWiki later than what it was branched from + $comparableVersion = preg_replace( '/-.*$/', '', $version ); + + # If the comparableVersion is larger than our release limit then + # skip the warning message for the deprecation + if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) { + $sendToLog = false; + } + } + + $component = $component === false ? 'MediaWiki' : $component; + $msg = "Use of $function was deprecated in $component $version."; + } else { + $msg = "Use of $function is deprecated."; + } + + if ( $sendToLog ) { + self::sendWarning( $msg, $callerDescription, E_USER_DEPRECATED ); + } + + if ( self::$enabled ) { + $logMsg = htmlspecialchars( $msg ) . + Html::rawElement( 'div', array( 'class' => 'mw-debug-backtrace' ), + Html::element( 'span', array(), 'Backtrace:' ) . wfBacktrace() + ); + + self::$log[] = array( + 'msg' => $logMsg, + 'type' => 'deprecated', + 'caller' => $callerFunc, + ); + } } /** - * Adds a depreciation entry to the log, along with a backtrace + * Get an array describing the calling function at a specified offset. * - * @param $function - * @param $version - * @param $component - * @return mixed + * @param $callerOffset integer: How far up the callstack is the original + * caller. 0 = function that called getCallerDescription() + * @return array with two keys: 'file' and 'func' */ - public static function deprecated( $function, $version, $component ) { - if ( !self::$enabled ) { - return; + private static function getCallerDescription( $callerOffset ) { + $callers = wfDebugBacktrace(); + + if ( isset( $callers[$callerOffset] ) ) { + $callerfile = $callers[$callerOffset]; + if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) { + $file = $callerfile['file'] . ' at line ' . $callerfile['line']; + } else { + $file = '(internal function)'; + } + } else { + $file = '(unknown location)'; } - // Chain: This function -> wfDeprecated -> deprecatedFunction -> caller - $caller = wfGetCaller( 4 ); - - // Check to see if there already was a warning about this function - $functionString = "$function-$caller"; - if ( in_array( $functionString, self::$deprecationWarnings ) ) { - return; + if ( isset( $callers[$callerOffset + 1] ) ) { + $callerfunc = $callers[$callerOffset + 1]; + $func = ''; + if ( isset( $callerfunc['class'] ) ) { + $func .= $callerfunc['class'] . '::'; + } + if ( isset( $callerfunc['function'] ) ) { + $func .= $callerfunc['function']; + } + } else { + $func = 'unknown'; } - $version = $version === false ? '(unknown version)' : $version; - $component = $component === false ? 'MediaWiki' : $component; - $msg = htmlspecialchars( "Use of function $function was deprecated in $component $version" ); - $msg .= Html::rawElement( 'div', array( 'class' => 'mw-debug-backtrace' ), - Html::element( 'span', array(), 'Backtrace:' ) - . wfBacktrace() - ); + return array( 'file' => $file, 'func' => $func ); + } - self::$deprecationWarnings[] = $functionString; - self::$log[] = array( - 'msg' => $msg, - 'type' => 'deprecated', - 'caller' => $caller, - ); + /** + * Send a warning either to the debug log or by triggering an user PHP + * error depending on $wgDevelopmentWarnings. + * + * @param $msg string Message to send + * @param $caller array caller description get from getCallerDescription() + * @param $level error level to use if $wgDevelopmentWarnings is true + */ + private static function sendWarning( $msg, $caller, $level ) { + global $wgDevelopmentWarnings; + + $msg .= ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']'; + + if ( $wgDevelopmentWarnings ) { + trigger_error( $msg, $level ); + } else { + wfDebug( "$msg\n" ); + } } /** * This is a method to pass messages from wfDebug to the pretty debugger. * Do NOT use this method, use MWDebug::log or wfDebug() * + * @since 1.19 * @param $str string */ public static function debugMsg( $str ) { - if ( !self::$enabled ) { - return; - } + global $wgDebugComments, $wgShowDebug; - self::$debug[] = trim( $str ); + if ( self::$enabled || $wgDebugComments || $wgShowDebug ) { + self::$debug[] = rtrim( $str ); + } } /** * Begins profiling on a database query * + * @since 1.19 * @param $sql string * @param $function string * @param $isMaster bool @@ -209,6 +329,7 @@ class MWDebug { /** * Calculates how long a query took. * + * @since 1.19 * @param $id int */ public static function queryTime( $id ) { @@ -243,26 +364,162 @@ class MWDebug { /** * Returns the HTML to add to the page for the toolbar * + * @since 1.19 * @param $context IContextSource * @return string */ public static function getDebugHTML( IContextSource $context ) { - if ( !self::$enabled ) { + global $wgDebugComments; + + $html = ''; + + if ( self::$enabled ) { + MWDebug::log( 'MWDebug output complete' ); + $debugInfo = self::getDebugInfo( $context ); + + // Cannot use OutputPage::addJsConfigVars because those are already outputted + // by the time this method is called. + $html = Html::inlineScript( + ResourceLoader::makeLoaderConditionalScript( + ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) ) + ) + ); + } + + if ( $wgDebugComments ) { + $html .= "<!-- Debug output:\n" . + htmlspecialchars( implode( "\n", self::$debug ) ) . + "\n\n-->"; + } + + return $html; + } + + /** + * Generate debug log in HTML for displaying at the bottom of the main + * content area. + * If $wgShowDebug is false, an empty string is always returned. + * + * @since 1.20 + * @return string HTML fragment + */ + public static function getHTMLDebugLog() { + global $wgDebugTimestamps, $wgShowDebug; + + if ( !$wgShowDebug ) { return ''; } - global $wgVersion, $wgRequestTime; + $curIdent = 0; + $ret = "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">\n<li>"; + + foreach ( self::$debug as $line ) { + $pre = ''; + if ( $wgDebugTimestamps ) { + $matches = array(); + if ( preg_match( '/^(\d+\.\d+ {1,3}\d+.\dM\s{2})/', $line, $matches ) ) { + $pre = $matches[1]; + $line = substr( $line, strlen( $pre ) ); + } + } + $display = ltrim( $line ); + $ident = strlen( $line ) - strlen( $display ); + $diff = $ident - $curIdent; + + $display = $pre . $display; + if ( $display == '' ) { + $display = "\xc2\xa0"; + } + + if ( !$ident && $diff < 0 && substr( $display, 0, 9 ) != 'Entering ' && substr( $display, 0, 8 ) != 'Exiting ' ) { + $ident = $curIdent; + $diff = 0; + $display = '<span style="background:yellow;">' . nl2br( htmlspecialchars( $display ) ) . '</span>'; + } else { + $display = nl2br( htmlspecialchars( $display ) ); + } + + if ( $diff < 0 ) { + $ret .= str_repeat( "</li></ul>\n", -$diff ) . "</li><li>\n"; + } elseif ( $diff == 0 ) { + $ret .= "</li><li>\n"; + } else { + $ret .= str_repeat( "<ul><li>\n", $diff ); + } + $ret .= "<tt>$display</tt>\n"; + + $curIdent = $ident; + } + + $ret .= str_repeat( '</li></ul>', $curIdent ) . "</li>\n</ul>\n"; + + return $ret; + } + + /** + * Append the debug info to given ApiResult + * + * @param $context IContextSource + * @param $result ApiResult + */ + public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) { + if ( !self::$enabled ) { + return; + } + + // output errors as debug info, when display_errors is on + // this is necessary for all non html output of the api, because that clears all errors first + $obContents = ob_get_contents(); + if( $obContents ) { + $obContentArray = explode( '<br />', $obContents ); + foreach( $obContentArray as $obContent ) { + if( trim( $obContent ) ) { + self::debugMsg( Sanitizer::stripAllTags( $obContent ) ); + } + } + } + MWDebug::log( 'MWDebug output complete' ); + $debugInfo = self::getDebugInfo( $context ); + + $result->setIndexedTagName( $debugInfo, 'debuginfo' ); + $result->setIndexedTagName( $debugInfo['log'], 'line' ); + foreach( $debugInfo['debugLog'] as $index => $debugLogText ) { + $vals = array(); + ApiResult::setContent( $vals, $debugLogText ); + $debugInfo['debugLog'][$index] = $vals; //replace + } + $result->setIndexedTagName( $debugInfo['debugLog'], 'msg' ); + $result->setIndexedTagName( $debugInfo['queries'], 'query' ); + $result->setIndexedTagName( $debugInfo['includes'], 'queries' ); + $result->addValue( array(), 'debuginfo', $debugInfo ); + } + + /** + * Returns the HTML to add to the page for the toolbar + * + * @param $context IContextSource + * @return array + */ + public static function getDebugInfo( IContextSource $context ) { + if ( !self::$enabled ) { + return array(); + } + + global $wgVersion, $wgRequestTime; $request = $context->getRequest(); - $debugInfo = array( + return array( 'mwVersion' => $wgVersion, 'phpVersion' => PHP_VERSION, + 'gitRevision' => GitInfo::headSHA1(), + 'gitBranch' => GitInfo::currentBranch(), + 'gitViewUrl' => GitInfo::headViewUrl(), 'time' => microtime( true ) - $wgRequestTime, 'log' => self::$log, 'debugLog' => self::$debug, 'queries' => self::$query, 'request' => array( - 'method' => $_SERVER['REQUEST_METHOD'], + 'method' => $request->getMethod(), 'url' => $request->getRequestURL(), 'headers' => $request->getAllHeaders(), 'params' => $request->getValues(), @@ -271,15 +528,5 @@ class MWDebug { 'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage() ), 'includes' => self::getFilesIncluded( $context ), ); - - // Cannot use OutputPage::addJsConfigVars because those are already outputted - // by the time this method is called. - $html = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - ResourceLoader::makeConfigSetScript( array( 'debugInfo' => $debugInfo ) ) - ) - ); - - return $html; } } diff --git a/includes/diff/DairikiDiff.php b/includes/diff/DairikiDiff.php index c935eee2..72eb5d3c 100644 --- a/includes/diff/DairikiDiff.php +++ b/includes/diff/DairikiDiff.php @@ -185,8 +185,8 @@ class _DiffEngine { $edits = array(); $xi = $yi = 0; while ( $xi < $n_from || $yi < $n_to ) { - assert( $yi < $n_to || $this->xchanged[$xi] ); - assert( $xi < $n_from || $this->ychanged[$yi] ); + assert( '$yi < $n_to || $this->xchanged[$xi]' ); + assert( '$xi < $n_from || $this->ychanged[$yi]' ); // Skip matching "snake". $copy = array(); @@ -374,14 +374,14 @@ class _DiffEngine { while ( list( , $y ) = each( $matches ) ) { if ( empty( $this->in_seq[$y] ) ) { $k = $this->_lcs_pos( $y ); - assert( $k > 0 ); + assert( '$k > 0' ); $ymids[$k] = $ymids[$k -1]; break; } } while ( list ( , $y ) = each( $matches ) ) { if ( $y > $this->seq[$k -1] ) { - assert( $y < $this->seq[$k] ); + assert( '$y < $this->seq[$k]' ); // Optimization: this is a common case: // next match is just replacing previous match. $this->in_seq[$this->seq[$k]] = false; @@ -389,7 +389,7 @@ class _DiffEngine { $this->in_seq[$y] = 1; } elseif ( empty( $this->in_seq[$y] ) ) { $k = $this->_lcs_pos( $y ); - assert( $k > 0 ); + assert( '$k > 0' ); $ymids[$k] = $ymids[$k -1]; } } @@ -430,7 +430,7 @@ class _DiffEngine { } } - assert( $ypos != $this->seq[$end] ); + assert( '$ypos != $this->seq[$end]' ); $this->in_seq[$this->seq[$end]] = false; $this->seq[$end] = $ypos; @@ -661,7 +661,7 @@ class Diff { * * $diff = new Diff($lines1, $lines2); * $rev = $diff->reverse(); - * @return object A Diff object representing the inverse of the + * @return Object A Diff object representing the inverse of the * original diff. */ function reverse() { @@ -814,8 +814,8 @@ class MappedDiff extends Diff { $mapped_from_lines, $mapped_to_lines ) { wfProfileIn( __METHOD__ ); - assert( sizeof( $from_lines ) == sizeof( $mapped_from_lines ) ); - assert( sizeof( $to_lines ) == sizeof( $mapped_to_lines ) ); + assert( 'sizeof( $from_lines ) == sizeof( $mapped_from_lines )' ); + assert( 'sizeof( $to_lines ) == sizeof( $mapped_to_lines )' ); parent::__construct( $mapped_from_lines, $mapped_to_lines ); @@ -1205,7 +1205,7 @@ class _HWLDF_WordAccumulator { $this->_flushLine( $tag ); $word = substr( $word, 1 ); } - assert( !strstr( $word, "\n" ) ); + assert( '!strstr( $word, "\n" )' ); $this->_group .= $word; } } diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 439e3204..c7156fb2 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -1,6 +1,21 @@ <?php /** - * User interface for the difference engine + * User interface for the difference engine. + * + * 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 DifferenceEngine @@ -164,6 +179,22 @@ class DifferenceEngine extends ContextSource { } } + private function showMissingRevision() { + $out = $this->getOutput(); + + $missing = array(); + if ( $this->mOldRev === null ) { + $missing[] = $this->deletedIdMarker( $this->mOldid ); + } + if ( $this->mNewRev === null ) { + $missing[] = $this->deletedIdMarker( $this->mNewid ); + } + + $out->setPageTitle( $this->msg( 'errorpagetitle' ) ); + $out->addWikiMsg( 'difference-missing-revision', + $this->getLanguage()->listToText( $missing ), count( $missing ) ); + } + function showDiffPage( $diffOnly = false ) { wfProfileIn( __METHOD__ ); @@ -173,13 +204,7 @@ class DifferenceEngine extends ContextSource { $out->setRobotPolicy( 'noindex,nofollow' ); if ( !$this->loadRevisionData() ) { - // Sounds like a deleted revision... Let's see what we can do. - $t = $this->getTitle()->getPrefixedText(); - $d = $this->msg( 'missingarticle-diff', - $this->deletedIdMarker( $this->mOldid ), - $this->deletedIdMarker( $this->mNewid ) )->escaped(); - $out->setPageTitle( $this->msg( 'errorpagetitle' ) ); - $out->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", "<span class='plainlinks'>$d</span>" ); + $this->showMissingRevision(); wfProfileOut( __METHOD__ ); return; } @@ -239,8 +264,7 @@ class DifferenceEngine extends ContextSource { # a diff between a version V and its previous version V' AND the version V # is the first version of that article. In that case, V' does not exist. if ( $this->mOldRev === false ) { - $out->setPageTitle( $this->mNewPage->getPrefixedText() ); - $out->addSubtitle( $this->msg( 'difference' ) ); + $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); $samePage = true; $oldHeader = ''; } else { @@ -252,19 +276,19 @@ class DifferenceEngine extends ContextSource { } if ( $this->mNewPage->equals( $this->mOldPage ) ) { - $out->setPageTitle( $this->mNewPage->getPrefixedText() ); - $out->addSubtitle( $this->msg( 'difference' ) ); + $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); $samePage = true; } else { - $out->setPageTitle( $this->mOldPage->getPrefixedText() . ', ' . $this->mNewPage->getPrefixedText() ); + $out->setPageTitle( $this->msg( 'difference-title-multipage', $this->mOldPage->getPrefixedText(), + $this->mNewPage->getPrefixedText() ) ); $out->addSubtitle( $this->msg( 'difference-multipage' ) ); $samePage = false; } - if ( $samePage && $this->mNewPage->userCan( 'edit', $user ) ) { + if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) { if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) { $out->preventClickjacking(); - $rollback = '   ' . Linker::generateRollback( $this->mNewRev ); + $rollback = '   ' . Linker::generateRollback( $this->mNewRev, $this->getContext() ); } if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { $undoLink = ' ' . $this->msg( 'parentheses' )->rawParams( @@ -403,7 +427,7 @@ class DifferenceEngine extends ContextSource { if ( $this->mMarkPatrolledLink === null ) { // Prepare a change patrol link, if applicable - if ( $wgUseRCPatrol && $this->mNewPage->userCan( 'patrol', $this->getUser() ) ) { + if ( $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $this->getUser() ) ) { // If we've been given an explicit change identifier, use it; saves time if ( $this->mRcidMarkPatrolled ) { $rcid = $this->mRcidMarkPatrolled; @@ -512,9 +536,7 @@ class DifferenceEngine extends ContextSource { $wikiPage = WikiPage::factory( $this->mNewPage ); } - $parserOptions = ParserOptions::newFromContext( $this->getContext() ); - $parserOptions->enableLimitReport(); - $parserOptions->setTidy( true ); + $parserOptions = $wikiPage->makeParserOptions( $this->getContext() ); if ( !$this->mNewRev->isCurrent() ) { $parserOptions->setEditSection( false ); @@ -543,7 +565,7 @@ class DifferenceEngine extends ContextSource { function showDiff( $otitle, $ntitle, $notice = '' ) { $diff = $this->getDiff( $otitle, $ntitle, $notice ); if ( $diff === false ) { - $this->getOutput()->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' ); + $this->showMissingRevision(); return false; } else { $this->showDiffStyle(); @@ -598,7 +620,7 @@ class DifferenceEngine extends ContextSource { return false; } // Short-circuit - // If mOldRev is false, it means that the + // If mOldRev is false, it means that the if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) ) { @@ -672,6 +694,7 @@ class DifferenceEngine extends ContextSource { * * @param $otext String: old text, must be already segmented * @param $ntext String: new text, must be already segmented + * @return bool|string */ function generateDiffBody( $otext, $ntext ) { global $wgExternalDiffEngine, $wgContLang; @@ -705,9 +728,9 @@ class DifferenceEngine extends ContextSource { } if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { # Diff via the shell - global $wgTmpDirectory; - $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); - $tempName2 = tempnam( $wgTmpDirectory, 'diff_' ); + $tmpDir = wfTempDir(); + $tempName1 = tempnam( $tmpDir, 'diff_' ); + $tempName2 = tempnam( $tmpDir, 'diff_' ); $tempFile1 = fopen( $tempName1, "w" ); if ( !$tempFile1 ) { @@ -747,6 +770,7 @@ class DifferenceEngine extends ContextSource { /** * Generate a debug comment indicating diff generating time, * server node, and generator backend. + * @return string */ protected function debug( $generator = "internal" ) { global $wgShowHostnames; @@ -768,6 +792,7 @@ class DifferenceEngine extends ContextSource { /** * Replace line numbers with the text in the user's language + * @return mixed */ function localiseLineNumbers( $text ) { return preg_replace_callback( '/<!--LINE (\d+)-->/', @@ -864,8 +889,9 @@ class DifferenceEngine extends ContextSource { $editQuery['oldid'] = $rev->getID(); } - $msg = $this->msg( $title->userCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped(); - $header .= ' (' . Linker::linkKnown( $title, $msg, array(), $editQuery ) . ')'; + $msg = $this->msg( $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped(); + $header .= ' ' . $this->msg( 'parentheses' )->rawParams( + Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain(); if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); } @@ -889,7 +915,7 @@ class DifferenceEngine extends ContextSource { if ( !$diff && !$otitle ) { $header .= " - <tr valign='top'> + <tr style='vertical-align: top;'> <td class='diff-ntitle'>{$ntitle}</td> </tr>"; $multiColspan = 1; @@ -907,17 +933,17 @@ class DifferenceEngine extends ContextSource { $multiColspan = 2; } $header .= " - <tr valign='top'> + <tr style='vertical-align: top;'> <td colspan='$colspan' class='diff-otitle'>{$otitle}</td> <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td> </tr>"; } if ( $multi != '' ) { - $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>"; + $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' class='diff-multi'>{$multi}</td></tr>"; } if ( $notice != '' ) { - $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>"; + $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;'>{$notice}</td></tr>"; } return $header . $diff . "</table>"; @@ -1002,7 +1028,7 @@ class DifferenceEngine extends ContextSource { // Load the new revision object $this->mNewRev = $this->mNewid ? Revision::newFromId( $this->mNewid ) - : Revision::newFromTitle( $this->getTitle() ); + : Revision::newFromTitle( $this->getTitle(), false, Revision::READ_NORMAL ); if ( !$this->mNewRev instanceof Revision ) { return false; diff --git a/includes/filerepo/backend/FSFile.php b/includes/filebackend/FSFile.php index 54dd1359..e07c99d4 100644 --- a/includes/filerepo/backend/FSFile.php +++ b/includes/filebackend/FSFile.php @@ -1,5 +1,22 @@ <?php /** + * Non-directory file on the file system. + * + * 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 FileBackend */ @@ -15,7 +32,8 @@ class FSFile { /** * Sets up the file object * - * @param String $path Path to temporary file on local disk + * @param $path string Path to temporary file on local disk + * @throws MWException */ public function __construct( $path ) { if ( FileBackend::isStoragePath( $path ) ) { @@ -45,7 +63,7 @@ class FSFile { /** * Get the file size in bytes * - * @return int|false + * @return int|bool */ public function getSize() { return filesize( $this->path ); @@ -54,7 +72,7 @@ class FSFile { /** * Get the file's last-modified timestamp * - * @return string|false TS_MW timestamp or false on failure + * @return string|bool TS_MW timestamp or false on failure */ public function getTimestamp() { wfSuppressWarnings(); @@ -152,6 +170,7 @@ class FSFile { /** * Exract image size information * + * @param $gis array * @return Array */ protected function extractImageSizeInfo( array $gis ) { @@ -174,7 +193,7 @@ class FSFile { * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36 * fairly neatly. * - * @return false|string False on failure + * @return bool|string False on failure */ public function getSha1Base36() { wfProfileIn( __METHOD__ ); @@ -224,7 +243,7 @@ class FSFile { * * @param $path string * - * @return false|string False on failure + * @return bool|string False on failure */ public static function getSha1Base36FromPath( $path ) { $fsFile = new self( $path ); diff --git a/includes/filebackend/FSFileBackend.php b/includes/filebackend/FSFileBackend.php new file mode 100644 index 00000000..93495340 --- /dev/null +++ b/includes/filebackend/FSFileBackend.php @@ -0,0 +1,986 @@ +<?php +/** + * File system based backend. + * + * 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 FileBackend + * @author Aaron Schulz + */ + +/** + * @brief Class for a file system (FS) based file backend. + * + * All "containers" each map to a directory under the backend's base directory. + * For backwards-compatibility, some container paths can be set to custom paths. + * The wiki ID will not be used in any custom paths, so this should be avoided. + * + * Having directories with thousands of files will diminish performance. + * Sharding can be accomplished by using FileRepo-style hash paths. + * + * Status messages should avoid mentioning the internal FS paths. + * PHP warnings are assumed to be logged rather than output. + * + * @ingroup FileBackend + * @since 1.19 + */ +class FSFileBackend extends FileBackendStore { + protected $basePath; // string; directory holding the container directories + /** @var Array Map of container names to root paths */ + protected $containerPaths = array(); // for custom container paths + protected $fileMode; // integer; file permission mode + protected $fileOwner; // string; required OS username to own files + protected $currentUser; // string; OS username running this script + + protected $hadWarningErrors = array(); + + /** + * @see FileBackendStore::__construct() + * Additional $config params include: + * - basePath : File system directory that holds containers. + * - containerPaths : Map of container names to custom file system directories. + * This should only be used for backwards-compatibility. + * - fileMode : Octal UNIX file permissions to use on files stored. + */ + public function __construct( array $config ) { + parent::__construct( $config ); + + // Remove any possible trailing slash from directories + if ( isset( $config['basePath'] ) ) { + $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash + } else { + $this->basePath = null; // none; containers must have explicit paths + } + + if ( isset( $config['containerPaths'] ) ) { + $this->containerPaths = (array)$config['containerPaths']; + foreach ( $this->containerPaths as &$path ) { + $path = rtrim( $path, '/' ); // remove trailing slash + } + } + + $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644; + if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) { + $this->fileOwner = $config['fileOwner']; + $info = posix_getpwuid( posix_getuid() ); + $this->currentUser = $info['name']; // cache this, assuming it doesn't change + } + } + + /** + * @see FileBackendStore::resolveContainerPath() + * @param $container string + * @param $relStoragePath string + * @return null|string + */ + protected function resolveContainerPath( $container, $relStoragePath ) { + // Check that container has a root directory + if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) { + // Check for sane relative paths (assume the base paths are OK) + if ( $this->isLegalRelPath( $relStoragePath ) ) { + return $relStoragePath; + } + } + return null; + } + + /** + * Sanity check a relative file system path for validity + * + * @param $path string Normalized relative path + * @return bool + */ + protected function isLegalRelPath( $path ) { + // Check for file names longer than 255 chars + if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS + return false; + } + if ( wfIsWindows() ) { // NTFS + return !preg_match( '![:*?"<>|]!', $path ); + } else { + return true; + } + } + + /** + * Given the short (unresolved) and full (resolved) name of + * a container, return the file system path of the container. + * + * @param $shortCont string + * @param $fullCont string + * @return string|null + */ + protected function containerFSRoot( $shortCont, $fullCont ) { + if ( isset( $this->containerPaths[$shortCont] ) ) { + return $this->containerPaths[$shortCont]; + } elseif ( isset( $this->basePath ) ) { + return "{$this->basePath}/{$fullCont}"; + } + return null; // no container base path defined + } + + /** + * Get the absolute file system path for a storage path + * + * @param $storagePath string Storage path + * @return string|null + */ + protected function resolveToFSPath( $storagePath ) { + list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath ); + if ( $relPath === null ) { + return null; // invalid + } + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath ); + $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + if ( $relPath != '' ) { + $fsPath .= "/{$relPath}"; + } + return $fsPath; + } + + /** + * @see FileBackendStore::isPathUsableInternal() + * @return bool + */ + public function isPathUsableInternal( $storagePath ) { + $fsPath = $this->resolveToFSPath( $storagePath ); + if ( $fsPath === null ) { + return false; // invalid + } + $parentDir = dirname( $fsPath ); + + if ( file_exists( $fsPath ) ) { + $ok = is_file( $fsPath ) && is_writable( $fsPath ); + } else { + $ok = is_dir( $parentDir ) && is_writable( $parentDir ); + } + + if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) { + $ok = false; + trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." ); + } + + return $ok; + } + + /** + * @see FileBackendStore::doStoreInternal() + * @return Status + */ + protected function doStoreInternal( array $params ) { + $status = Status::newGood(); + + $dest = $this->resolveToFSPath( $params['dst'] ); + if ( $dest === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + if ( file_exists( $dest ) ) { + if ( !empty( $params['overwrite'] ) ) { + $ok = unlink( $dest ); + if ( !$ok ) { + $status->fatal( 'backend-fail-delete', $params['dst'] ); + return $status; + } + } else { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } + + if ( !empty( $params['async'] ) ) { // deferred + $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp', + wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ), + wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) + ) ); + $status->value = new FSFileOpHandle( $this, $params, 'Store', $cmd, $dest ); + } else { // immediate write + $ok = copy( $params['src'], $dest ); + // In some cases (at least over NFS), copy() returns true when it fails + if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) { + if ( $ok ) { // PHP bug + unlink( $dest ); // remove broken file + trigger_error( __METHOD__ . ": copy() failed but returned true." ); + } + $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); + return $status; + } + $this->chmod( $dest ); + } + + return $status; + } + + /** + * @see FSFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseStore( $errors, Status $status, array $params, $cmd ) { + if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { + $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); + trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output + } + } + + /** + * @see FileBackendStore::doCopyInternal() + * @return Status + */ + protected function doCopyInternal( array $params ) { + $status = Status::newGood(); + + $source = $this->resolveToFSPath( $params['src'] ); + if ( $source === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['src'] ); + return $status; + } + + $dest = $this->resolveToFSPath( $params['dst'] ); + if ( $dest === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + if ( file_exists( $dest ) ) { + if ( !empty( $params['overwrite'] ) ) { + $ok = unlink( $dest ); + if ( !$ok ) { + $status->fatal( 'backend-fail-delete', $params['dst'] ); + return $status; + } + } else { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } + + if ( !empty( $params['async'] ) ) { // deferred + $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp', + wfEscapeShellArg( $this->cleanPathSlashes( $source ) ), + wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) + ) ); + $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd, $dest ); + } else { // immediate write + $ok = copy( $source, $dest ); + // In some cases (at least over NFS), copy() returns true when it fails + if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) { + if ( $ok ) { // PHP bug + unlink( $dest ); // remove broken file + trigger_error( __METHOD__ . ": copy() failed but returned true." ); + } + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + return $status; + } + $this->chmod( $dest ); + } + + return $status; + } + + /** + * @see FSFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseCopy( $errors, Status $status, array $params, $cmd ) { + if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output + } + } + + /** + * @see FileBackendStore::doMoveInternal() + * @return Status + */ + protected function doMoveInternal( array $params ) { + $status = Status::newGood(); + + $source = $this->resolveToFSPath( $params['src'] ); + if ( $source === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['src'] ); + return $status; + } + + $dest = $this->resolveToFSPath( $params['dst'] ); + if ( $dest === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + if ( file_exists( $dest ) ) { + if ( !empty( $params['overwrite'] ) ) { + // Windows does not support moving over existing files + if ( wfIsWindows() ) { + $ok = unlink( $dest ); + if ( !$ok ) { + $status->fatal( 'backend-fail-delete', $params['dst'] ); + return $status; + } + } + } else { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } + + if ( !empty( $params['async'] ) ) { // deferred + $cmd = implode( ' ', array( wfIsWindows() ? 'MOVE' : 'mv', + wfEscapeShellArg( $this->cleanPathSlashes( $source ) ), + wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) + ) ); + $status->value = new FSFileOpHandle( $this, $params, 'Move', $cmd ); + } else { // immediate write + $ok = rename( $source, $dest ); + clearstatcache(); // file no longer at source + if ( !$ok ) { + $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); + return $status; + } + } + + return $status; + } + + /** + * @see FSFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseMove( $errors, Status $status, array $params, $cmd ) { + if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { + $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); + trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output + } + } + + /** + * @see FileBackendStore::doDeleteInternal() + * @return Status + */ + protected function doDeleteInternal( array $params ) { + $status = Status::newGood(); + + $source = $this->resolveToFSPath( $params['src'] ); + if ( $source === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['src'] ); + return $status; + } + + if ( !is_file( $source ) ) { + if ( empty( $params['ignoreMissingSource'] ) ) { + $status->fatal( 'backend-fail-delete', $params['src'] ); + } + return $status; // do nothing; either OK or bad status + } + + if ( !empty( $params['async'] ) ) { // deferred + $cmd = implode( ' ', array( wfIsWindows() ? 'DEL' : 'unlink', + wfEscapeShellArg( $this->cleanPathSlashes( $source ) ) + ) ); + $status->value = new FSFileOpHandle( $this, $params, 'Copy', $cmd ); + } else { // immediate write + $ok = unlink( $source ); + if ( !$ok ) { + $status->fatal( 'backend-fail-delete', $params['src'] ); + return $status; + } + } + + return $status; + } + + /** + * @see FSFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseDelete( $errors, Status $status, array $params, $cmd ) { + if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { + $status->fatal( 'backend-fail-delete', $params['src'] ); + trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output + } + } + + /** + * @see FileBackendStore::doCreateInternal() + * @return Status + */ + protected function doCreateInternal( array $params ) { + $status = Status::newGood(); + + $dest = $this->resolveToFSPath( $params['dst'] ); + if ( $dest === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + if ( file_exists( $dest ) ) { + if ( !empty( $params['overwrite'] ) ) { + $ok = unlink( $dest ); + if ( !$ok ) { + $status->fatal( 'backend-fail-delete', $params['dst'] ); + return $status; + } + } else { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } + + if ( !empty( $params['async'] ) ) { // deferred + $tempFile = TempFSFile::factory( 'create_', 'tmp' ); + if ( !$tempFile ) { + $status->fatal( 'backend-fail-create', $params['dst'] ); + return $status; + } + $bytes = file_put_contents( $tempFile->getPath(), $params['content'] ); + if ( $bytes === false ) { + $status->fatal( 'backend-fail-create', $params['dst'] ); + return $status; + } + $cmd = implode( ' ', array( wfIsWindows() ? 'COPY' : 'cp', + wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ), + wfEscapeShellArg( $this->cleanPathSlashes( $dest ) ) + ) ); + $status->value = new FSFileOpHandle( $this, $params, 'Create', $cmd, $dest ); + $tempFile->bind( $status->value ); + } else { // immediate write + $bytes = file_put_contents( $dest, $params['content'] ); + if ( $bytes === false ) { + $status->fatal( 'backend-fail-create', $params['dst'] ); + return $status; + } + $this->chmod( $dest ); + } + + return $status; + } + + /** + * @see FSFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseCreate( $errors, Status $status, array $params, $cmd ) { + if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) { + $status->fatal( 'backend-fail-create', $params['dst'] ); + trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output + } + } + + /** + * @see FileBackendStore::doPrepareInternal() + * @return Status + */ + protected function doPrepareInternal( $fullCont, $dirRel, array $params ) { + $status = Status::newGood(); + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + $existed = is_dir( $dir ); // already there? + if ( !wfMkdirParents( $dir ) ) { // make directory and its parents + $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races + } elseif ( !is_writable( $dir ) ) { + $status->fatal( 'directoryreadonlyerror', $params['dir'] ); + } elseif ( !is_readable( $dir ) ) { + $status->fatal( 'directorynotreadableerror', $params['dir'] ); + } + if ( is_dir( $dir ) && !$existed ) { + // Respect any 'noAccess' or 'noListing' flags... + $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) ); + } + return $status; + } + + /** + * @see FileBackendStore::doSecureInternal() + * @return Status + */ + protected function doSecureInternal( $fullCont, $dirRel, array $params ) { + $status = Status::newGood(); + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + // Seed new directories with a blank index.html, to prevent crawling... + if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) { + $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() ); + if ( $bytes === false ) { + $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' ); + return $status; + } + } + // Add a .htaccess file to the root of the container... + if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) { + $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() ); + if ( $bytes === false ) { + $storeDir = "mwstore://{$this->name}/{$shortCont}"; + $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" ); + return $status; + } + } + return $status; + } + + /** + * @see FileBackendStore::doPublishInternal() + * @return Status + */ + protected function doPublishInternal( $fullCont, $dirRel, array $params ) { + $status = Status::newGood(); + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + // Unseed new directories with a blank index.html, to allow crawling... + if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) { + $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() ); + if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure() + $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' ); + return $status; + } + } + // Remove the .htaccess file from the root of the container... + if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) { + $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() ); + if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure() + $storeDir = "mwstore://{$this->name}/{$shortCont}"; + $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" ); + return $status; + } + } + return $status; + } + + /** + * @see FileBackendStore::doCleanInternal() + * @return Status + */ + protected function doCleanInternal( $fullCont, $dirRel, array $params ) { + $status = Status::newGood(); + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + wfSuppressWarnings(); + if ( is_dir( $dir ) ) { + rmdir( $dir ); // remove directory if empty + } + wfRestoreWarnings(); + return $status; + } + + /** + * @see FileBackendStore::doFileExists() + * @return array|bool|null + */ + protected function doGetFileStat( array $params ) { + $source = $this->resolveToFSPath( $params['src'] ); + if ( $source === null ) { + return false; // invalid storage path + } + + $this->trapWarnings(); // don't trust 'false' if there were errors + $stat = is_file( $source ) ? stat( $source ) : false; // regular files only + $hadError = $this->untrapWarnings(); + + if ( $stat ) { + return array( + 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ), + 'size' => $stat['size'] + ); + } elseif ( !$hadError ) { + return false; // file does not exist + } else { + return null; // failure + } + } + + /** + * @see FileBackendStore::doClearCache() + */ + protected function doClearCache( array $paths = null ) { + clearstatcache(); // clear the PHP file stat cache + } + + /** + * @see FileBackendStore::doDirectoryExists() + * @return bool|null + */ + protected function doDirectoryExists( $fullCont, $dirRel, array $params ) { + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + + $this->trapWarnings(); // don't trust 'false' if there were errors + $exists = is_dir( $dir ); + $hadError = $this->untrapWarnings(); + + return $hadError ? null : $exists; + } + + /** + * @see FileBackendStore::getDirectoryListInternal() + * @return Array|null + */ + public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) { + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + $exists = is_dir( $dir ); + if ( !$exists ) { + wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" ); + return array(); // nothing under this dir + } elseif ( !is_readable( $dir ) ) { + wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); + return null; // bad permissions? + } + return new FSFileBackendDirList( $dir, $params ); + } + + /** + * @see FileBackendStore::getFileListInternal() + * @return array|FSFileBackendFileList|null + */ + public function getFileListInternal( $fullCont, $dirRel, array $params ) { + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; + $exists = is_dir( $dir ); + if ( !$exists ) { + wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" ); + return array(); // nothing under this dir + } elseif ( !is_readable( $dir ) ) { + wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); + return null; // bad permissions? + } + return new FSFileBackendFileList( $dir, $params ); + } + + /** + * @see FileBackendStore::getLocalReference() + * @return FSFile|null + */ + public function getLocalReference( array $params ) { + $source = $this->resolveToFSPath( $params['src'] ); + if ( $source === null ) { + return null; + } + return new FSFile( $source ); + } + + /** + * @see FileBackendStore::getLocalCopy() + * @return null|TempFSFile + */ + public function getLocalCopy( array $params ) { + $source = $this->resolveToFSPath( $params['src'] ); + if ( $source === null ) { + return null; + } + + // Create a new temporary file with the same extension... + $ext = FileBackend::extensionFromPath( $params['src'] ); + $tmpFile = TempFSFile::factory( 'localcopy_', $ext ); + if ( !$tmpFile ) { + return null; + } + $tmpPath = $tmpFile->getPath(); + + // Copy the source file over the temp file + $ok = copy( $source, $tmpPath ); + if ( !$ok ) { + return null; + } + + $this->chmod( $tmpPath ); + + return $tmpFile; + } + + /** + * @see FileBackendStore::directoriesAreVirtual() + * @return bool + */ + protected function directoriesAreVirtual() { + return false; + } + + /** + * @see FileBackendStore::doExecuteOpHandlesInternal() + * @return Array List of corresponding Status objects + */ + protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { + $statuses = array(); + + $pipes = array(); + foreach ( $fileOpHandles as $index => $fileOpHandle ) { + $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' ); + } + + $errs = array(); + foreach ( $pipes as $index => $pipe ) { + // Result will be empty on success in *NIX. On Windows, + // it may be something like " 1 file(s) [copied|moved].". + $errs[$index] = stream_get_contents( $pipe ); + fclose( $pipe ); + } + + foreach ( $fileOpHandles as $index => $fileOpHandle ) { + $status = Status::newGood(); + $function = '_getResponse' . $fileOpHandle->call; + $this->$function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd ); + $statuses[$index] = $status; + if ( $status->isOK() && $fileOpHandle->chmodPath ) { + $this->chmod( $fileOpHandle->chmodPath ); + } + } + + clearstatcache(); // files changed + return $statuses; + } + + /** + * Chmod a file, suppressing the warnings + * + * @param $path string Absolute file system path + * @return bool Success + */ + protected function chmod( $path ) { + wfSuppressWarnings(); + $ok = chmod( $path, $this->fileMode ); + wfRestoreWarnings(); + + return $ok; + } + + /** + * Return the text of an index.html file to hide directory listings + * + * @return string + */ + protected function indexHtmlPrivate() { + return ''; + } + + /** + * Return the text of a .htaccess file to make a directory private + * + * @return string + */ + protected function htaccessPrivate() { + return "Deny from all\n"; + } + + /** + * Clean up directory separators for the given OS + * + * @param $path string FS path + * @return string + */ + protected function cleanPathSlashes( $path ) { + return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path; + } + + /** + * Listen for E_WARNING errors and track whether any happen + * + * @return bool + */ + protected function trapWarnings() { + $this->hadWarningErrors[] = false; // push to stack + set_error_handler( array( $this, 'handleWarning' ), E_WARNING ); + return false; // invoke normal PHP error handler + } + + /** + * Stop listening for E_WARNING errors and return true if any happened + * + * @return bool + */ + protected function untrapWarnings() { + restore_error_handler(); // restore previous handler + return array_pop( $this->hadWarningErrors ); // pop from stack + } + + /** + * @return bool + */ + private function handleWarning() { + $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true; + return true; // suppress from PHP handler + } +} + +/** + * @see FileBackendStoreOpHandle + */ +class FSFileOpHandle extends FileBackendStoreOpHandle { + public $cmd; // string; shell command + public $chmodPath; // string; file to chmod + + /** + * @param $backend + * @param $params array + * @param $call + * @param $cmd + * @param $chmodPath null + */ + public function __construct( $backend, array $params, $call, $cmd, $chmodPath = null ) { + $this->backend = $backend; + $this->params = $params; + $this->call = $call; + $this->cmd = $cmd; + $this->chmodPath = $chmodPath; + } +} + +/** + * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that + * catches exception or does any custom behavoir that we may want. + * Do not use this class from places outside FSFileBackend. + * + * @ingroup FileBackend + */ +abstract class FSFileBackendList implements Iterator { + /** @var Iterator */ + protected $iter; + protected $suffixStart; // integer + protected $pos = 0; // integer + /** @var Array */ + protected $params = array(); + + /** + * @param $dir string file system directory + * @param $params array + */ + public function __construct( $dir, array $params ) { + $dir = realpath( $dir ); // normalize + $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/" + $this->params = $params; + + try { + $this->iter = $this->initIterator( $dir ); + } catch ( UnexpectedValueException $e ) { + $this->iter = null; // bad permissions? deleted? + } + } + + /** + * Return an appropriate iterator object to wrap + * + * @param $dir string file system directory + * @return Iterator + */ + protected function initIterator( $dir ) { + if ( !empty( $this->params['topOnly'] ) ) { // non-recursive + # Get an iterator that will get direct sub-nodes + return new DirectoryIterator( $dir ); + } else { // recursive + # Get an iterator that will return leaf nodes (non-directories) + # RecursiveDirectoryIterator extends FilesystemIterator. + # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x. + $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS; + return new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $dir, $flags ), + RecursiveIteratorIterator::CHILD_FIRST // include dirs + ); + } + } + + /** + * @see Iterator::key() + * @return integer + */ + public function key() { + return $this->pos; + } + + /** + * @see Iterator::current() + * @return string|bool String or false + */ + public function current() { + return $this->getRelPath( $this->iter->current()->getPathname() ); + } + + /** + * @see Iterator::next() + * @return void + */ + public function next() { + try { + $this->iter->next(); + $this->filterViaNext(); + } catch ( UnexpectedValueException $e ) { + $this->iter = null; + } + ++$this->pos; + } + + /** + * @see Iterator::rewind() + * @return void + */ + public function rewind() { + $this->pos = 0; + try { + $this->iter->rewind(); + $this->filterViaNext(); + } catch ( UnexpectedValueException $e ) { + $this->iter = null; + } + } + + /** + * @see Iterator::valid() + * @return bool + */ + public function valid() { + return $this->iter && $this->iter->valid(); + } + + /** + * Filter out items by advancing to the next ones + */ + protected function filterViaNext() {} + + /** + * Return only the relative path and normalize slashes to FileBackend-style. + * Uses the "real path" since the suffix is based upon that. + * + * @param $path string + * @return string + */ + protected function getRelPath( $path ) { + return strtr( substr( realpath( $path ), $this->suffixStart ), '\\', '/' ); + } +} + +class FSFileBackendDirList extends FSFileBackendList { + protected function filterViaNext() { + while ( $this->iter->valid() ) { + if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) { + $this->iter->next(); // skip non-directories and dot files + } else { + break; + } + } + } +} + +class FSFileBackendFileList extends FSFileBackendList { + protected function filterViaNext() { + while ( $this->iter->valid() ) { + if ( !$this->iter->current()->isFile() ) { + $this->iter->next(); // skip non-files and dot files + } else { + break; + } + } + } +} diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php new file mode 100644 index 00000000..76c761b0 --- /dev/null +++ b/includes/filebackend/FileBackend.php @@ -0,0 +1,1173 @@ +<?php +/** + * @defgroup FileBackend File backend + * @ingroup FileRepo + * + * File backend is used to interact with file storage systems, + * such as the local file system, NFS, or cloud storage systems. + */ + +/** + * Base class for all file backends. + * + * 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 FileBackend + * @author Aaron Schulz + */ + +/** + * @brief Base class for all file backend classes (including multi-write backends). + * + * This class defines the methods as abstract that subclasses must implement. + * Outside callers can assume that all backends will have these functions. + * + * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>". + * The "<path>" portion is a relative path that uses UNIX file system (FS) + * notation, though any particular backend may not actually be using a local + * filesystem. Therefore, the relative paths are only virtual. + * + * Backend contents are stored under wiki-specific container names by default. + * For legacy reasons, this has no effect for the FS backend class, and per-wiki + * segregation must be done by setting the container paths appropriately. + * + * FS-based backends are somewhat more restrictive due to the existence of real + * directory files; a regular file cannot have the same name as a directory. Other + * backends with virtual directories may not have this limitation. Callers should + * store files in such a way that no files and directories are under the same path. + * + * Methods of subclasses should avoid throwing exceptions at all costs. + * As a corollary, external dependencies should be kept to a minimum. + * + * @ingroup FileBackend + * @since 1.19 + */ +abstract class FileBackend { + protected $name; // string; unique backend name + protected $wikiId; // string; unique wiki name + protected $readOnly; // string; read-only explanation message + protected $parallelize; // string; when to do operations in parallel + protected $concurrency; // integer; how many operations can be done in parallel + + /** @var LockManager */ + protected $lockManager; + /** @var FileJournal */ + protected $fileJournal; + + /** + * Create a new backend instance from configuration. + * This should only be called from within FileBackendGroup. + * + * $config includes: + * - name : The unique name of this backend. + * This should consist of alphanumberic, '-', and '_' characters. + * This name should not be changed after use. + * - wikiId : Prefix to container names that is unique to this wiki. + * It should only consist of alphanumberic, '-', and '_' characters. + * - lockManager : Registered name of a file lock manager to use. + * - fileJournal : File journal configuration; see FileJournal::factory(). + * Journals simply log changes to files stored in the backend. + * - readOnly : Write operations are disallowed if this is a non-empty string. + * It should be an explanation for the backend being read-only. + * - parallelize : When to do file operations in parallel (when possible). + * Allowed values are "implicit", "explicit" and "off". + * - concurrency : How many file operations can be done in parallel. + * + * @param $config Array + * @throws MWException + */ + public function __construct( array $config ) { + $this->name = $config['name']; + if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) { + throw new MWException( "Backend name `{$this->name}` is invalid." ); + } + $this->wikiId = isset( $config['wikiId'] ) + ? $config['wikiId'] + : wfWikiID(); // e.g. "my_wiki-en_" + $this->lockManager = ( $config['lockManager'] instanceof LockManager ) + ? $config['lockManager'] + : LockManagerGroup::singleton()->get( $config['lockManager'] ); + $this->fileJournal = isset( $config['fileJournal'] ) + ? ( ( $config['fileJournal'] instanceof FileJournal ) + ? $config['fileJournal'] + : FileJournal::factory( $config['fileJournal'], $this->name ) ) + : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name ); + $this->readOnly = isset( $config['readOnly'] ) + ? (string)$config['readOnly'] + : ''; + $this->parallelize = isset( $config['parallelize'] ) + ? (string)$config['parallelize'] + : 'off'; + $this->concurrency = isset( $config['concurrency'] ) + ? (int)$config['concurrency'] + : 50; + } + + /** + * Get the unique backend name. + * We may have multiple different backends of the same type. + * For example, we can have two Swift backends using different proxies. + * + * @return string + */ + final public function getName() { + return $this->name; + } + + /** + * Get the wiki identifier used for this backend (possibly empty) + * + * @return string + * @since 1.20 + */ + final public function getWikiId() { + return $this->wikiId; + } + + /** + * Check if this backend is read-only + * + * @return bool + */ + final public function isReadOnly() { + return ( $this->readOnly != '' ); + } + + /** + * Get an explanatory message if this backend is read-only + * + * @return string|bool Returns false if the backend is not read-only + */ + final public function getReadOnlyReason() { + return ( $this->readOnly != '' ) ? $this->readOnly : false; + } + + /** + * This is the main entry point into the backend for write operations. + * Callers supply an ordered list of operations to perform as a transaction. + * Files will be locked, the stat cache cleared, and then the operations attempted. + * If any serious errors occur, all attempted operations will be rolled back. + * + * $ops is an array of arrays. The outer array holds a list of operations. + * Each inner array is a set of key value pairs that specify an operation. + * + * Supported operations and their parameters. The supported actions are: + * - create + * - store + * - copy + * - move + * - delete + * - null + * + * a) Create a new file in storage with the contents of a string + * @code + * array( + * 'op' => 'create', + * 'dst' => <storage path>, + * 'content' => <string of new file contents>, + * 'overwrite' => <boolean>, + * 'overwriteSame' => <boolean>, + * 'disposition' => <Content-Disposition header value> + * ); + * @endcode + * + * b) Copy a file system file into storage + * @code + * array( + * 'op' => 'store', + * 'src' => <file system path>, + * 'dst' => <storage path>, + * 'overwrite' => <boolean>, + * 'overwriteSame' => <boolean>, + * 'disposition' => <Content-Disposition header value> + * ) + * @endcode + * + * c) Copy a file within storage + * @code + * array( + * 'op' => 'copy', + * 'src' => <storage path>, + * 'dst' => <storage path>, + * 'overwrite' => <boolean>, + * 'overwriteSame' => <boolean>, + * 'disposition' => <Content-Disposition header value> + * ) + * @endcode + * + * d) Move a file within storage + * @code + * array( + * 'op' => 'move', + * 'src' => <storage path>, + * 'dst' => <storage path>, + * 'overwrite' => <boolean>, + * 'overwriteSame' => <boolean>, + * 'disposition' => <Content-Disposition header value> + * ) + * @endcode + * + * e) Delete a file within storage + * @code + * array( + * 'op' => 'delete', + * 'src' => <storage path>, + * 'ignoreMissingSource' => <boolean> + * ) + * @endcode + * + * f) Do nothing (no-op) + * @code + * array( + * 'op' => 'null', + * ) + * @endcode + * + * Boolean flags for operations (operation-specific): + * - ignoreMissingSource : The operation will simply succeed and do + * nothing if the source file does not exist. + * - overwrite : Any destination file will be overwritten. + * - overwriteSame : An error will not be given if a file already + * exists at the destination that has the same + * contents as the new contents to be written there. + * - disposition : When supplied, the backend will add a Content-Disposition + * header when GETs/HEADs of the destination file are made. + * Backends that don't support file metadata will ignore this. + * See http://tools.ietf.org/html/rfc6266 (since 1.20). + * + * $opts is an associative of boolean flags, including: + * - force : Operation precondition errors no longer trigger an abort. + * Any remaining operations are still attempted. Unexpected + * failures may still cause remaning operations to be aborted. + * - nonLocking : No locks are acquired for the operations. + * This can increase performance for non-critical writes. + * This has no effect unless the 'force' flag is set. + * - allowStale : Don't require the latest available data. + * This can increase performance for non-critical writes. + * This has no effect unless the 'force' flag is set. + * - nonJournaled : Don't log this operation batch in the file journal. + * This limits the ability of recovery scripts. + * - parallelize : Try to do operations in parallel when possible. + * - bypassReadOnly : Allow writes in read-only mode (since 1.20). + * - preserveCache : Don't clear the process cache before checking files. + * This should only be used if all entries in the process + * cache were added after the files were already locked (since 1.20). + * + * @remarks Remarks on locking: + * File system paths given to operations should refer to files that are + * already locked or otherwise safe from modification from other processes. + * Normally these files will be new temp files, which should be adequate. + * + * @par Return value: + * + * This returns a Status, which contains all warnings and fatals that occurred + * during the operation. The 'failCount', 'successCount', and 'success' members + * will reflect each operation attempted. + * + * The status will be "OK" unless: + * - a) unexpected operation errors occurred (network partitions, disk full...) + * - b) significant operation errors occurred and 'force' was not set + * + * @param $ops Array List of operations to execute in order + * @param $opts Array Batch operation options + * @return Status + */ + final public function doOperations( array $ops, array $opts = array() ) { + if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) { + return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); + } + if ( empty( $opts['force'] ) ) { // sanity + unset( $opts['nonLocking'] ); + unset( $opts['allowStale'] ); + } + $opts['concurrency'] = 1; // off + if ( $this->parallelize === 'implicit' ) { + if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) { + $opts['concurrency'] = $this->concurrency; + } + } elseif ( $this->parallelize === 'explicit' ) { + if ( !empty( $opts['parallelize'] ) ) { + $opts['concurrency'] = $this->concurrency; + } + } + return $this->doOperationsInternal( $ops, $opts ); + } + + /** + * @see FileBackend::doOperations() + */ + abstract protected function doOperationsInternal( array $ops, array $opts ); + + /** + * Same as doOperations() except it takes a single operation. + * If you are doing a batch of operations that should either + * all succeed or all fail, then use that function instead. + * + * @see FileBackend::doOperations() + * + * @param $op Array Operation + * @param $opts Array Operation options + * @return Status + */ + final public function doOperation( array $op, array $opts = array() ) { + return $this->doOperations( array( $op ), $opts ); + } + + /** + * Performs a single create operation. + * This sets $params['op'] to 'create' and passes it to doOperation(). + * + * @see FileBackend::doOperation() + * + * @param $params Array Operation parameters + * @param $opts Array Operation options + * @return Status + */ + final public function create( array $params, array $opts = array() ) { + return $this->doOperation( array( 'op' => 'create' ) + $params, $opts ); + } + + /** + * Performs a single store operation. + * This sets $params['op'] to 'store' and passes it to doOperation(). + * + * @see FileBackend::doOperation() + * + * @param $params Array Operation parameters + * @param $opts Array Operation options + * @return Status + */ + final public function store( array $params, array $opts = array() ) { + return $this->doOperation( array( 'op' => 'store' ) + $params, $opts ); + } + + /** + * Performs a single copy operation. + * This sets $params['op'] to 'copy' and passes it to doOperation(). + * + * @see FileBackend::doOperation() + * + * @param $params Array Operation parameters + * @param $opts Array Operation options + * @return Status + */ + final public function copy( array $params, array $opts = array() ) { + return $this->doOperation( array( 'op' => 'copy' ) + $params, $opts ); + } + + /** + * Performs a single move operation. + * This sets $params['op'] to 'move' and passes it to doOperation(). + * + * @see FileBackend::doOperation() + * + * @param $params Array Operation parameters + * @param $opts Array Operation options + * @return Status + */ + final public function move( array $params, array $opts = array() ) { + return $this->doOperation( array( 'op' => 'move' ) + $params, $opts ); + } + + /** + * Performs a single delete operation. + * This sets $params['op'] to 'delete' and passes it to doOperation(). + * + * @see FileBackend::doOperation() + * + * @param $params Array Operation parameters + * @param $opts Array Operation options + * @return Status + */ + final public function delete( array $params, array $opts = array() ) { + return $this->doOperation( array( 'op' => 'delete' ) + $params, $opts ); + } + + /** + * Perform a set of independent file operations on some files. + * + * This does no locking, nor journaling, and possibly no stat calls. + * Any destination files that already exist will be overwritten. + * This should *only* be used on non-original files, like cache files. + * + * Supported operations and their parameters: + * - create + * - store + * - copy + * - move + * - delete + * - null + * + * a) Create a new file in storage with the contents of a string + * @code + * array( + * 'op' => 'create', + * 'dst' => <storage path>, + * 'content' => <string of new file contents>, + * 'disposition' => <Content-Disposition header value> + * ) + * @endcode + * b) Copy a file system file into storage + * @code + * array( + * 'op' => 'store', + * 'src' => <file system path>, + * 'dst' => <storage path>, + * 'disposition' => <Content-Disposition header value> + * ) + * @endcode + * c) Copy a file within storage + * @code + * array( + * 'op' => 'copy', + * 'src' => <storage path>, + * 'dst' => <storage path>, + * 'disposition' => <Content-Disposition header value> + * ) + * @endcode + * d) Move a file within storage + * @code + * array( + * 'op' => 'move', + * 'src' => <storage path>, + * 'dst' => <storage path>, + * 'disposition' => <Content-Disposition header value> + * ) + * @endcode + * e) Delete a file within storage + * @code + * array( + * 'op' => 'delete', + * 'src' => <storage path>, + * 'ignoreMissingSource' => <boolean> + * ) + * @endcode + * f) Do nothing (no-op) + * @code + * array( + * 'op' => 'null', + * ) + * @endcode + * + * @par Boolean flags for operations (operation-specific): + * - ignoreMissingSource : The operation will simply succeed and do + * nothing if the source file does not exist. + * - disposition : When supplied, the backend will add a Content-Disposition + * header when GETs/HEADs of the destination file are made. + * Backends that don't support file metadata will ignore this. + * See http://tools.ietf.org/html/rfc6266 (since 1.20). + * + * $opts is an associative of boolean flags, including: + * - bypassReadOnly : Allow writes in read-only mode (since 1.20) + * + * @par Return value: + * This returns a Status, which contains all warnings and fatals that occurred + * during the operation. The 'failCount', 'successCount', and 'success' members + * will reflect each operation attempted for the given files. The status will be + * considered "OK" as long as no fatal errors occurred. + * + * @param $ops Array Set of operations to execute + * @param $opts Array Batch operation options + * @return Status + * @since 1.20 + */ + final public function doQuickOperations( array $ops, array $opts = array() ) { + if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) { + return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); + } + foreach ( $ops as &$op ) { + $op['overwrite'] = true; // avoids RTTs in key/value stores + } + return $this->doQuickOperationsInternal( $ops ); + } + + /** + * @see FileBackend::doQuickOperations() + * @since 1.20 + */ + abstract protected function doQuickOperationsInternal( array $ops ); + + /** + * Same as doQuickOperations() except it takes a single operation. + * If you are doing a batch of operations, then use that function instead. + * + * @see FileBackend::doQuickOperations() + * + * @param $op Array Operation + * @return Status + * @since 1.20 + */ + final public function doQuickOperation( array $op ) { + return $this->doQuickOperations( array( $op ) ); + } + + /** + * Performs a single quick create operation. + * This sets $params['op'] to 'create' and passes it to doQuickOperation(). + * + * @see FileBackend::doQuickOperation() + * + * @param $params Array Operation parameters + * @return Status + * @since 1.20 + */ + final public function quickCreate( array $params ) { + return $this->doQuickOperation( array( 'op' => 'create' ) + $params ); + } + + /** + * Performs a single quick store operation. + * This sets $params['op'] to 'store' and passes it to doQuickOperation(). + * + * @see FileBackend::doQuickOperation() + * + * @param $params Array Operation parameters + * @return Status + * @since 1.20 + */ + final public function quickStore( array $params ) { + return $this->doQuickOperation( array( 'op' => 'store' ) + $params ); + } + + /** + * Performs a single quick copy operation. + * This sets $params['op'] to 'copy' and passes it to doQuickOperation(). + * + * @see FileBackend::doQuickOperation() + * + * @param $params Array Operation parameters + * @return Status + * @since 1.20 + */ + final public function quickCopy( array $params ) { + return $this->doQuickOperation( array( 'op' => 'copy' ) + $params ); + } + + /** + * Performs a single quick move operation. + * This sets $params['op'] to 'move' and passes it to doQuickOperation(). + * + * @see FileBackend::doQuickOperation() + * + * @param $params Array Operation parameters + * @return Status + * @since 1.20 + */ + final public function quickMove( array $params ) { + return $this->doQuickOperation( array( 'op' => 'move' ) + $params ); + } + + /** + * Performs a single quick delete operation. + * This sets $params['op'] to 'delete' and passes it to doQuickOperation(). + * + * @see FileBackend::doQuickOperation() + * + * @param $params Array Operation parameters + * @return Status + * @since 1.20 + */ + final public function quickDelete( array $params ) { + return $this->doQuickOperation( array( 'op' => 'delete' ) + $params ); + } + + /** + * Concatenate a list of storage files into a single file system file. + * The target path should refer to a file that is already locked or + * otherwise safe from modification from other processes. Normally, + * the file will be a new temp file, which should be adequate. + * + * @param $params Array Operation parameters + * $params include: + * - srcs : ordered source storage paths (e.g. chunk1, chunk2, ...) + * - dst : file system path to 0-byte temp file + * @return Status + */ + abstract public function concatenate( array $params ); + + /** + * Prepare a storage directory for usage. + * This will create any required containers and parent directories. + * Backends using key/value stores only need to create the container. + * + * The 'noAccess' and 'noListing' parameters works the same as in secure(), + * except they are only applied *if* the directory/container had to be created. + * These flags should always be set for directories that have private files. + * + * @param $params Array + * $params include: + * - dir : storage directory + * - noAccess : try to deny file access (since 1.20) + * - noListing : try to deny file listing (since 1.20) + * - bypassReadOnly : allow writes in read-only mode (since 1.20) + * @return Status + */ + final public function prepare( array $params ) { + if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { + return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); + } + return $this->doPrepare( $params ); + } + + /** + * @see FileBackend::prepare() + */ + abstract protected function doPrepare( array $params ); + + /** + * Take measures to block web access to a storage directory and + * the container it belongs to. FS backends might add .htaccess + * files whereas key/value store backends might revoke container + * access to the storage user representing end-users in web requests. + * This is not guaranteed to actually do anything. + * + * @param $params Array + * $params include: + * - dir : storage directory + * - noAccess : try to deny file access + * - noListing : try to deny file listing + * - bypassReadOnly : allow writes in read-only mode (since 1.20) + * @return Status + */ + final public function secure( array $params ) { + if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { + return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); + } + return $this->doSecure( $params ); + } + + /** + * @see FileBackend::secure() + */ + abstract protected function doSecure( array $params ); + + /** + * Remove measures to block web access to a storage directory and + * the container it belongs to. FS backends might remove .htaccess + * files whereas key/value store backends might grant container + * access to the storage user representing end-users in web requests. + * This essentially can undo the result of secure() calls. + * + * @param $params Array + * $params include: + * - dir : storage directory + * - access : try to allow file access + * - listing : try to allow file listing + * - bypassReadOnly : allow writes in read-only mode (since 1.20) + * @return Status + * @since 1.20 + */ + final public function publish( array $params ) { + if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { + return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); + } + return $this->doPublish( $params ); + } + + /** + * @see FileBackend::publish() + */ + abstract protected function doPublish( array $params ); + + /** + * Delete a storage directory if it is empty. + * Backends using key/value stores may do nothing unless the directory + * is that of an empty container, in which case it will be deleted. + * + * @param $params Array + * $params include: + * - dir : storage directory + * - recursive : recursively delete empty subdirectories first (since 1.20) + * - bypassReadOnly : allow writes in read-only mode (since 1.20) + * @return Status + */ + final public function clean( array $params ) { + if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) { + return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); + } + return $this->doClean( $params ); + } + + /** + * @see FileBackend::clean() + */ + abstract protected function doClean( array $params ); + + /** + * Check if a file exists at a storage path in the backend. + * This returns false if only a directory exists at the path. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return bool|null Returns null on failure + */ + abstract public function fileExists( array $params ); + + /** + * Get the last-modified timestamp of the file at a storage path. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return string|bool TS_MW timestamp or false on failure + */ + abstract public function getFileTimestamp( array $params ); + + /** + * Get the contents of a file at a storage path in the backend. + * This should be avoided for potentially large files. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return string|bool Returns false on failure + */ + abstract public function getFileContents( array $params ); + + /** + * Get the size (bytes) of a file at a storage path in the backend. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return integer|bool Returns false on failure + */ + abstract public function getFileSize( array $params ); + + /** + * Get quick information about a file at a storage path in the backend. + * If the file does not exist, then this returns false. + * Otherwise, the result is an associative array that includes: + * - mtime : the last-modified timestamp (TS_MW) + * - size : the file size (bytes) + * Additional values may be included for internal use only. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return Array|bool|null Returns null on failure + */ + abstract public function getFileStat( array $params ); + + /** + * Get a SHA-1 hash of the file at a storage path in the backend. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return string|bool Hash string or false on failure + */ + abstract public function getFileSha1Base36( array $params ); + + /** + * Get the properties of the file at a storage path in the backend. + * Returns FSFile::placeholderProps() on failure. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return Array + */ + abstract public function getFileProps( array $params ); + + /** + * Stream the file at a storage path in the backend. + * If the file does not exists, an HTTP 404 error will be given. + * Appropriate HTTP headers (Status, Content-Type, Content-Length) + * will be sent if streaming began, while none will be sent otherwise. + * Implementations should flush the output buffer before sending data. + * + * @param $params Array + * $params include: + * - src : source storage path + * - headers : list of additional HTTP headers to send on success + * - latest : use the latest available data + * @return Status + */ + abstract public function streamFile( array $params ); + + /** + * Returns a file system file, identical to the file at a storage path. + * The file returned is either: + * - a) A local copy of the file at a storage path in the backend. + * The temporary copy will have the same extension as the source. + * - b) An original of the file at a storage path in the backend. + * Temporary files may be purged when the file object falls out of scope. + * + * Write operations should *never* be done on this file as some backends + * may do internal tracking or may be instances of FileBackendMultiWrite. + * In that later case, there are copies of the file that must stay in sync. + * Additionally, further calls to this function may return the same file. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return FSFile|null Returns null on failure + */ + abstract public function getLocalReference( array $params ); + + /** + * Get a local copy on disk of the file at a storage path in the backend. + * The temporary copy will have the same file extension as the source. + * Temporary files may be purged when the file object falls out of scope. + * + * @param $params Array + * $params include: + * - src : source storage path + * - latest : use the latest available data + * @return TempFSFile|null Returns null on failure + */ + abstract public function getLocalCopy( array $params ); + + /** + * Check if a directory exists at a given storage path. + * Backends using key/value stores will check if the path is a + * virtual directory, meaning there are files under the given directory. + * + * Storage backends with eventual consistency might return stale data. + * + * @param $params array + * $params include: + * - dir : storage directory + * @return bool|null Returns null on failure + * @since 1.20 + */ + abstract public function directoryExists( array $params ); + + /** + * Get an iterator to list *all* directories under a storage directory. + * If the directory is of the form "mwstore://backend/container", + * then all directories in the container will be listed. + * If the directory is of form "mwstore://backend/container/dir", + * then all directories directly under that directory will be listed. + * Results will be storage directories relative to the given directory. + * + * Storage backends with eventual consistency might return stale data. + * + * @param $params array + * $params include: + * - dir : storage directory + * - topOnly : only return direct child dirs of the directory + * @return Traversable|Array|null Returns null on failure + * @since 1.20 + */ + abstract public function getDirectoryList( array $params ); + + /** + * Same as FileBackend::getDirectoryList() except only lists + * directories that are immediately under the given directory. + * + * Storage backends with eventual consistency might return stale data. + * + * @param $params array + * $params include: + * - dir : storage directory + * @return Traversable|Array|null Returns null on failure + * @since 1.20 + */ + final public function getTopDirectoryList( array $params ) { + return $this->getDirectoryList( array( 'topOnly' => true ) + $params ); + } + + /** + * Get an iterator to list *all* stored files under a storage directory. + * If the directory is of the form "mwstore://backend/container", + * then all files in the container will be listed. + * If the directory is of form "mwstore://backend/container/dir", + * then all files under that directory will be listed. + * Results will be storage paths relative to the given directory. + * + * Storage backends with eventual consistency might return stale data. + * + * @param $params array + * $params include: + * - dir : storage directory + * - topOnly : only return direct child files of the directory (since 1.20) + * @return Traversable|Array|null Returns null on failure + */ + abstract public function getFileList( array $params ); + + /** + * Same as FileBackend::getFileList() except only lists + * files that are immediately under the given directory. + * + * Storage backends with eventual consistency might return stale data. + * + * @param $params array + * $params include: + * - dir : storage directory + * @return Traversable|Array|null Returns null on failure + * @since 1.20 + */ + final public function getTopFileList( array $params ) { + return $this->getFileList( array( 'topOnly' => true ) + $params ); + } + + /** + * Preload persistent file stat and property cache into in-process cache. + * This should be used when stat calls will be made on a known list of a many files. + * + * @param $paths Array Storage paths + * @return void + */ + public function preloadCache( array $paths ) {} + + /** + * Invalidate any in-process file stat and property cache. + * If $paths is given, then only the cache for those files will be cleared. + * + * @param $paths Array Storage paths (optional) + * @return void + */ + public function clearCache( array $paths = null ) {} + + /** + * Lock the files at the given storage paths in the backend. + * This will either lock all the files or none (on failure). + * + * Callers should consider using getScopedFileLocks() instead. + * + * @param $paths Array Storage paths + * @param $type integer LockManager::LOCK_* constant + * @return Status + */ + final public function lockFiles( array $paths, $type ) { + return $this->lockManager->lock( $paths, $type ); + } + + /** + * Unlock the files at the given storage paths in the backend. + * + * @param $paths Array Storage paths + * @param $type integer LockManager::LOCK_* constant + * @return Status + */ + final public function unlockFiles( array $paths, $type ) { + return $this->lockManager->unlock( $paths, $type ); + } + + /** + * Lock the files at the given storage paths in the backend. + * This will either lock all the files or none (on failure). + * On failure, the status object will be updated with errors. + * + * Once the return value goes out scope, the locks will be released and + * the status updated. Unlock fatals will not change the status "OK" value. + * + * @param $paths Array Storage paths + * @param $type integer LockManager::LOCK_* constant + * @param $status Status Status to update on lock/unlock + * @return ScopedLock|null Returns null on failure + */ + final public function getScopedFileLocks( array $paths, $type, Status $status ) { + return ScopedLock::factory( $this->lockManager, $paths, $type, $status ); + } + + /** + * Get an array of scoped locks needed for a batch of file operations. + * + * Normally, FileBackend::doOperations() handles locking, unless + * the 'nonLocking' param is passed in. This function is useful if you + * want the files to be locked for a broader scope than just when the + * files are changing. For example, if you need to update DB metadata, + * you may want to keep the files locked until finished. + * + * @see FileBackend::doOperations() + * + * @param $ops Array List of file operations to FileBackend::doOperations() + * @param $status Status Status to update on lock/unlock + * @return Array List of ScopedFileLocks or null values + * @since 1.20 + */ + abstract public function getScopedLocksForOps( array $ops, Status $status ); + + /** + * Get the root storage path of this backend. + * All container paths are "subdirectories" of this path. + * + * @return string Storage path + * @since 1.20 + */ + final public function getRootStoragePath() { + return "mwstore://{$this->name}"; + } + + /** + * Get the file journal object for this backend + * + * @return FileJournal + */ + final public function getJournal() { + return $this->fileJournal; + } + + /** + * Check if a given path is a "mwstore://" path. + * This does not do any further validation or any existence checks. + * + * @param $path string + * @return bool + */ + final public static function isStoragePath( $path ) { + return ( strpos( $path, 'mwstore://' ) === 0 ); + } + + /** + * Split a storage path into a backend name, a container name, + * and a relative file path. The relative path may be the empty string. + * This does not do any path normalization or traversal checks. + * + * @param $storagePath string + * @return Array (backend, container, rel object) or (null, null, null) + */ + final public static function splitStoragePath( $storagePath ) { + if ( self::isStoragePath( $storagePath ) ) { + // Remove the "mwstore://" prefix and split the path + $parts = explode( '/', substr( $storagePath, 10 ), 3 ); + if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) { + if ( count( $parts ) == 3 ) { + return $parts; // e.g. "backend/container/path" + } else { + return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" + } + } + } + return array( null, null, null ); + } + + /** + * Normalize a storage path by cleaning up directory separators. + * Returns null if the path is not of the format of a valid storage path. + * + * @param $storagePath string + * @return string|null + */ + final public static function normalizeStoragePath( $storagePath ) { + list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); + if ( $relPath !== null ) { // must be for this backend + $relPath = self::normalizeContainerPath( $relPath ); + if ( $relPath !== null ) { + return ( $relPath != '' ) + ? "mwstore://{$backend}/{$container}/{$relPath}" + : "mwstore://{$backend}/{$container}"; + } + } + return null; + } + + /** + * Get the parent storage directory of a storage path. + * This returns a path like "mwstore://backend/container", + * "mwstore://backend/container/...", or null if there is no parent. + * + * @param $storagePath string + * @return string|null + */ + final public static function parentStoragePath( $storagePath ) { + $storagePath = dirname( $storagePath ); + list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath ); + return ( $rel === null ) ? null : $storagePath; + } + + /** + * Get the final extension from a storage or FS path + * + * @param $path string + * @return string + */ + final public static function extensionFromPath( $path ) { + $i = strrpos( $path, '.' ); + return strtolower( $i ? substr( $path, $i + 1 ) : '' ); + } + + /** + * Check if a relative path has no directory traversals + * + * @param $path string + * @return bool + * @since 1.20 + */ + final public static function isPathTraversalFree( $path ) { + return ( self::normalizeContainerPath( $path ) !== null ); + } + + /** + * Build a Content-Disposition header value per RFC 6266. + * + * @param $type string One of (attachment, inline) + * @param $filename string Suggested file name (should not contain slashes) + * @return string + * @since 1.20 + */ + final public static function makeContentDisposition( $type, $filename = '' ) { + $parts = array(); + + $type = strtolower( $type ); + if ( !in_array( $type, array( 'inline', 'attachment' ) ) ) { + throw new MWException( "Invalid Content-Disposition type '$type'." ); + } + $parts[] = $type; + + if ( strlen( $filename ) ) { + $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) ); + } + + return implode( ';', $parts ); + } + + /** + * Validate and normalize a relative storage path. + * Null is returned if the path involves directory traversal. + * Traversal is insecure for FS backends and broken for others. + * + * This uses the same traversal protection as Title::secureAndSplit(). + * + * @param $path string Storage path relative to a container + * @return string|null + */ + final protected static function normalizeContainerPath( $path ) { + // Normalize directory separators + $path = strtr( $path, '\\', '/' ); + // Collapse any consecutive directory separators + $path = preg_replace( '![/]{2,}!', '/', $path ); + // Remove any leading directory separator + $path = ltrim( $path, '/' ); + // Use the same traversal protection as Title::secureAndSplit() + if ( strpos( $path, '.' ) !== false ) { + if ( + $path === '.' || + $path === '..' || + strpos( $path, './' ) === 0 || + strpos( $path, '../' ) === 0 || + strpos( $path, '/./' ) !== false || + strpos( $path, '/../' ) !== false + ) { + return null; + } + } + return $path; + } +} diff --git a/includes/filerepo/backend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php index 73815cfb..8bbc96d0 100644 --- a/includes/filerepo/backend/FileBackendGroup.php +++ b/includes/filebackend/FileBackendGroup.php @@ -1,5 +1,22 @@ <?php /** + * File backend registration handling. + * + * 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 FileBackend * @author Aaron Schulz @@ -21,7 +38,6 @@ class FileBackendGroup { protected $backends = array(); protected function __construct() {} - protected function __clone() {} /** * @return FileBackendGroup @@ -36,7 +52,7 @@ class FileBackendGroup { /** * Destroy the singleton instance - * + * * @return void */ public static function destroySingleton() { @@ -45,7 +61,7 @@ class FileBackendGroup { /** * Register file backends from the global variables - * + * * @return void */ protected function initFromGlobals() { @@ -141,6 +157,21 @@ class FileBackendGroup { } /** + * Get the config array for a backend object with a given name + * + * @param $name string + * @return Array + * @throws MWException + */ + public function config( $name ) { + if ( !isset( $this->backends[$name] ) ) { + throw new MWException( "No backend defined with the name `$name`." ); + } + $class = $this->backends[$name]['class']; + return array( 'class' => $class ) + $this->backends[$name]['config']; + } + + /** * Get an appropriate backend object from a storage path * * @param $storagePath string diff --git a/includes/filebackend/FileBackendMultiWrite.php b/includes/filebackend/FileBackendMultiWrite.php new file mode 100644 index 00000000..4be03231 --- /dev/null +++ b/includes/filebackend/FileBackendMultiWrite.php @@ -0,0 +1,689 @@ +<?php +/** + * Proxy backend that mirrors writes to several internal backends. + * + * 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 FileBackend + * @author Aaron Schulz + */ + +/** + * @brief Proxy backend that mirrors writes to several internal backends. + * + * This class defines a multi-write backend. Multiple backends can be + * registered to this proxy backend and it will act as a single backend. + * Use this when all access to those backends is through this proxy backend. + * At least one of the backends must be declared the "master" backend. + * + * Only use this class when transitioning from one storage system to another. + * + * Read operations are only done on the 'master' backend for consistency. + * Write operations are performed on all backends, in the order defined. + * If an operation fails on one backend it will be rolled back from the others. + * + * @ingroup FileBackend + * @since 1.19 + */ +class FileBackendMultiWrite extends FileBackend { + /** @var Array Prioritized list of FileBackendStore objects */ + protected $backends = array(); // array of (backend index => backends) + protected $masterIndex = -1; // integer; index of master backend + protected $syncChecks = 0; // integer; bitfield + protected $autoResync = false; // boolean + + /** @var Array */ + protected $noPushDirConts = array(); + protected $noPushQuickOps = false; // boolean + + /* Possible internal backend consistency checks */ + const CHECK_SIZE = 1; + const CHECK_TIME = 2; + const CHECK_SHA1 = 4; + + /** + * Construct a proxy backend that consists of several internal backends. + * Locking, journaling, and read-only checks are handled by the proxy backend. + * + * Additional $config params include: + * - backends : Array of backend config and multi-backend settings. + * Each value is the config used in the constructor of a + * FileBackendStore class, but with these additional settings: + * - class : The name of the backend class + * - isMultiMaster : This must be set for one backend. + * - template: : If given a backend name, this will use + * the config of that backend as a template. + * Values specified here take precedence. + * - syncChecks : Integer bitfield of internal backend sync checks to perform. + * Possible bits include the FileBackendMultiWrite::CHECK_* constants. + * There are constants for SIZE, TIME, and SHA1. + * The checks are done before allowing any file operations. + * - autoResync : Automatically resync the clone backends to the master backend + * when pre-operation sync checks fail. This should only be used + * if the master backend is stable and not missing any files. + * - noPushQuickOps : (hack) Only apply doQuickOperations() to the master backend. + * - noPushDirConts : (hack) Only apply directory functions to the master backend. + * + * @param $config Array + * @throws MWException + */ + public function __construct( array $config ) { + parent::__construct( $config ); + $this->syncChecks = isset( $config['syncChecks'] ) + ? $config['syncChecks'] + : self::CHECK_SIZE; + $this->autoResync = !empty( $config['autoResync'] ); + $this->noPushQuickOps = isset( $config['noPushQuickOps'] ) + ? $config['noPushQuickOps'] + : false; + $this->noPushDirConts = isset( $config['noPushDirConts'] ) + ? $config['noPushDirConts'] + : array(); + // Construct backends here rather than via registration + // to keep these backends hidden from outside the proxy. + $namesUsed = array(); + foreach ( $config['backends'] as $index => $config ) { + if ( isset( $config['template'] ) ) { + // Config is just a modified version of a registered backend's. + // This should only be used when that config is used only by this backend. + $config = $config + FileBackendGroup::singleton()->config( $config['template'] ); + } + $name = $config['name']; + if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates + throw new MWException( "Two or more backends defined with the name $name." ); + } + $namesUsed[$name] = 1; + // Alter certain sub-backend settings for sanity + unset( $config['readOnly'] ); // use proxy backend setting + unset( $config['fileJournal'] ); // use proxy backend journal + $config['wikiId'] = $this->wikiId; // use the proxy backend wiki ID + $config['lockManager'] = 'nullLockManager'; // lock under proxy backend + if ( !empty( $config['isMultiMaster'] ) ) { + if ( $this->masterIndex >= 0 ) { + throw new MWException( 'More than one master backend defined.' ); + } + $this->masterIndex = $index; // this is the "master" + $config['fileJournal'] = $this->fileJournal; // log under proxy backend + } + // Create sub-backend object + if ( !isset( $config['class'] ) ) { + throw new MWException( 'No class given for a backend config.' ); + } + $class = $config['class']; + $this->backends[$index] = new $class( $config ); + } + if ( $this->masterIndex < 0 ) { // need backends and must have a master + throw new MWException( 'No master backend defined.' ); + } + } + + /** + * @see FileBackend::doOperationsInternal() + * @return Status + */ + final protected function doOperationsInternal( array $ops, array $opts ) { + $status = Status::newGood(); + + $mbe = $this->backends[$this->masterIndex]; // convenience + + // Get the paths to lock from the master backend + $realOps = $this->substOpBatchPaths( $ops, $mbe ); + $paths = $mbe->getPathsToLockForOpsInternal( $mbe->getOperationsInternal( $realOps ) ); + // Get the paths under the proxy backend's name + $paths['sh'] = $this->unsubstPaths( $paths['sh'] ); + $paths['ex'] = $this->unsubstPaths( $paths['ex'] ); + // Try to lock those files for the scope of this function... + if ( empty( $opts['nonLocking'] ) ) { + // Try to lock those files for the scope of this function... + $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ); + $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status ); + if ( !$status->isOK() ) { + return $status; // abort + } + } + // Clear any cache entries (after locks acquired) + $this->clearCache(); + $opts['preserveCache'] = true; // only locked files are cached + // Get the list of paths to read/write... + $relevantPaths = $this->fileStoragePathsForOps( $ops ); + // Check if the paths are valid and accessible on all backends... + $status->merge( $this->accessibilityCheck( $relevantPaths ) ); + if ( !$status->isOK() ) { + return $status; // abort + } + // Do a consistency check to see if the backends are consistent... + $syncStatus = $this->consistencyCheck( $relevantPaths ); + if ( !$syncStatus->isOK() ) { + wfDebugLog( 'FileOperation', get_class( $this ) . + " failed sync check: " . FormatJson::encode( $relevantPaths ) ); + // Try to resync the clone backends to the master on the spot... + if ( !$this->autoResync || !$this->resyncFiles( $relevantPaths )->isOK() ) { + $status->merge( $syncStatus ); + return $status; // abort + } + } + // Actually attempt the operation batch on the master backend... + $masterStatus = $mbe->doOperations( $realOps, $opts ); + $status->merge( $masterStatus ); + // Propagate the operations to the clone backends if there were no fatal errors. + // If $ops only had one operation, this might avoid backend inconsistencies. + // This also avoids inconsistency for expected errors (like "file already exists"). + if ( !count( $masterStatus->getErrorsArray() ) ) { + foreach ( $this->backends as $index => $backend ) { + if ( $index !== $this->masterIndex ) { // not done already + $realOps = $this->substOpBatchPaths( $ops, $backend ); + $status->merge( $backend->doOperations( $realOps, $opts ) ); + } + } + } + // Make 'success', 'successCount', and 'failCount' fields reflect + // the overall operation, rather than all the batches for each backend. + // Do this by only using success values from the master backend's batch. + $status->success = $masterStatus->success; + $status->successCount = $masterStatus->successCount; + $status->failCount = $masterStatus->failCount; + + return $status; + } + + /** + * Check that a set of files are consistent across all internal backends + * + * @param $paths Array List of storage paths + * @return Status + */ + public function consistencyCheck( array $paths ) { + $status = Status::newGood(); + if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) { + return $status; // skip checks + } + + $mBackend = $this->backends[$this->masterIndex]; + foreach ( $paths as $path ) { + $params = array( 'src' => $path, 'latest' => true ); + $mParams = $this->substOpPaths( $params, $mBackend ); + // Stat the file on the 'master' backend + $mStat = $mBackend->getFileStat( $mParams ); + if ( $this->syncChecks & self::CHECK_SHA1 ) { + $mSha1 = $mBackend->getFileSha1Base36( $mParams ); + } else { + $mSha1 = false; + } + // Check if all clone backends agree with the master... + foreach ( $this->backends as $index => $cBackend ) { + if ( $index === $this->masterIndex ) { + continue; // master + } + $cParams = $this->substOpPaths( $params, $cBackend ); + $cStat = $cBackend->getFileStat( $cParams ); + if ( $mStat ) { // file is in master + if ( !$cStat ) { // file should exist + $status->fatal( 'backend-fail-synced', $path ); + continue; + } + if ( $this->syncChecks & self::CHECK_SIZE ) { + if ( $cStat['size'] != $mStat['size'] ) { // wrong size + $status->fatal( 'backend-fail-synced', $path ); + continue; + } + } + if ( $this->syncChecks & self::CHECK_TIME ) { + $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] ); + $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] ); + if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere + $status->fatal( 'backend-fail-synced', $path ); + continue; + } + } + if ( $this->syncChecks & self::CHECK_SHA1 ) { + if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1 + $status->fatal( 'backend-fail-synced', $path ); + continue; + } + } + } else { // file is not in master + if ( $cStat ) { // file should not exist + $status->fatal( 'backend-fail-synced', $path ); + } + } + } + } + + return $status; + } + + /** + * Check that a set of file paths are usable across all internal backends + * + * @param $paths Array List of storage paths + * @return Status + */ + public function accessibilityCheck( array $paths ) { + $status = Status::newGood(); + if ( count( $this->backends ) <= 1 ) { + return $status; // skip checks + } + + foreach ( $paths as $path ) { + foreach ( $this->backends as $backend ) { + $realPath = $this->substPaths( $path, $backend ); + if ( !$backend->isPathUsableInternal( $realPath ) ) { + $status->fatal( 'backend-fail-usable', $path ); + } + } + } + + return $status; + } + + /** + * Check that a set of files are consistent across all internal backends + * and re-synchronize those files againt the "multi master" if needed. + * + * @param $paths Array List of storage paths + * @return Status + */ + public function resyncFiles( array $paths ) { + $status = Status::newGood(); + + $mBackend = $this->backends[$this->masterIndex]; + foreach ( $paths as $path ) { + $mPath = $this->substPaths( $path, $mBackend ); + $mSha1 = $mBackend->getFileSha1Base36( array( 'src' => $mPath ) ); + $mExist = $mBackend->fileExists( array( 'src' => $mPath ) ); + // Check if the master backend is available... + if ( $mExist === null ) { + $status->fatal( 'backend-fail-internal', $this->name ); + } + // Check of all clone backends agree with the master... + foreach ( $this->backends as $index => $cBackend ) { + if ( $index === $this->masterIndex ) { + continue; // master + } + $cPath = $this->substPaths( $path, $cBackend ); + $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath ) ); + if ( $mSha1 === $cSha1 ) { + // already synced; nothing to do + } elseif ( $mSha1 ) { // file is in master + $fsFile = $mBackend->getLocalReference( array( 'src' => $mPath ) ); + $status->merge( $cBackend->quickStore( + array( 'src' => $fsFile->getPath(), 'dst' => $cPath ) + ) ); + } elseif ( $mExist === false ) { // file is not in master + $status->merge( $cBackend->quickDelete( array( 'src' => $cPath ) ) ); + } + } + } + + return $status; + } + + /** + * Get a list of file storage paths to read or write for a list of operations + * + * @param $ops Array Same format as doOperations() + * @return Array List of storage paths to files (does not include directories) + */ + protected function fileStoragePathsForOps( array $ops ) { + $paths = array(); + foreach ( $ops as $op ) { + if ( isset( $op['src'] ) ) { + $paths[] = $op['src']; + } + if ( isset( $op['srcs'] ) ) { + $paths = array_merge( $paths, $op['srcs'] ); + } + if ( isset( $op['dst'] ) ) { + $paths[] = $op['dst']; + } + } + return array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ); + } + + /** + * Substitute the backend name in storage path parameters + * for a set of operations with that of a given internal backend. + * + * @param $ops Array List of file operation arrays + * @param $backend FileBackendStore + * @return Array + */ + protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) { + $newOps = array(); // operations + foreach ( $ops as $op ) { + $newOp = $op; // operation + foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) { + if ( isset( $newOp[$par] ) ) { // string or array + $newOp[$par] = $this->substPaths( $newOp[$par], $backend ); + } + } + $newOps[] = $newOp; + } + return $newOps; + } + + /** + * Same as substOpBatchPaths() but for a single operation + * + * @param $ops array File operation array + * @param $backend FileBackendStore + * @return Array + */ + protected function substOpPaths( array $ops, FileBackendStore $backend ) { + $newOps = $this->substOpBatchPaths( array( $ops ), $backend ); + return $newOps[0]; + } + + /** + * Substitute the backend of storage paths with an internal backend's name + * + * @param $paths Array|string List of paths or single string path + * @param $backend FileBackendStore + * @return Array|string + */ + protected function substPaths( $paths, FileBackendStore $backend ) { + return preg_replace( + '!^mwstore://' . preg_quote( $this->name ) . '/!', + StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ), + $paths // string or array + ); + } + + /** + * Substitute the backend of internal storage paths with the proxy backend's name + * + * @param $paths Array|string List of paths or single string path + * @return Array|string + */ + protected function unsubstPaths( $paths ) { + return preg_replace( + '!^mwstore://([^/]+)!', + StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ), + $paths // string or array + ); + } + + /** + * @see FileBackend::doQuickOperationsInternal() + * @return Status + */ + protected function doQuickOperationsInternal( array $ops ) { + $status = Status::newGood(); + // Do the operations on the master backend; setting Status fields... + $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] ); + $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps ); + $status->merge( $masterStatus ); + // Propagate the operations to the clone backends... + if ( !$this->noPushQuickOps ) { + foreach ( $this->backends as $index => $backend ) { + if ( $index !== $this->masterIndex ) { // not done already + $realOps = $this->substOpBatchPaths( $ops, $backend ); + $status->merge( $backend->doQuickOperations( $realOps ) ); + } + } + } + // Make 'success', 'successCount', and 'failCount' fields reflect + // the overall operation, rather than all the batches for each backend. + // Do this by only using success values from the master backend's batch. + $status->success = $masterStatus->success; + $status->successCount = $masterStatus->successCount; + $status->failCount = $masterStatus->failCount; + return $status; + } + + /** + * @param $path string Storage path + * @return bool Path container should have dir changes pushed to all backends + */ + protected function replicateContainerDirChanges( $path ) { + list( $b, $shortCont, $r ) = self::splitStoragePath( $path ); + return !in_array( $shortCont, $this->noPushDirConts ); + } + + /** + * @see FileBackend::doPrepare() + * @return Status + */ + protected function doPrepare( array $params ) { + $status = Status::newGood(); + $replicate = $this->replicateContainerDirChanges( $params['dir'] ); + foreach ( $this->backends as $index => $backend ) { + if ( $replicate || $index == $this->masterIndex ) { + $realParams = $this->substOpPaths( $params, $backend ); + $status->merge( $backend->doPrepare( $realParams ) ); + } + } + return $status; + } + + /** + * @see FileBackend::doSecure() + * @param $params array + * @return Status + */ + protected function doSecure( array $params ) { + $status = Status::newGood(); + $replicate = $this->replicateContainerDirChanges( $params['dir'] ); + foreach ( $this->backends as $index => $backend ) { + if ( $replicate || $index == $this->masterIndex ) { + $realParams = $this->substOpPaths( $params, $backend ); + $status->merge( $backend->doSecure( $realParams ) ); + } + } + return $status; + } + + /** + * @see FileBackend::doPublish() + * @param $params array + * @return Status + */ + protected function doPublish( array $params ) { + $status = Status::newGood(); + $replicate = $this->replicateContainerDirChanges( $params['dir'] ); + foreach ( $this->backends as $index => $backend ) { + if ( $replicate || $index == $this->masterIndex ) { + $realParams = $this->substOpPaths( $params, $backend ); + $status->merge( $backend->doPublish( $realParams ) ); + } + } + return $status; + } + + /** + * @see FileBackend::doClean() + * @param $params array + * @return Status + */ + protected function doClean( array $params ) { + $status = Status::newGood(); + $replicate = $this->replicateContainerDirChanges( $params['dir'] ); + foreach ( $this->backends as $index => $backend ) { + if ( $replicate || $index == $this->masterIndex ) { + $realParams = $this->substOpPaths( $params, $backend ); + $status->merge( $backend->doClean( $realParams ) ); + } + } + return $status; + } + + /** + * @see FileBackend::concatenate() + * @param $params array + * @return Status + */ + public function concatenate( array $params ) { + // We are writing to an FS file, so we don't need to do this per-backend + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->concatenate( $realParams ); + } + + /** + * @see FileBackend::fileExists() + * @param $params array + */ + public function fileExists( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->fileExists( $realParams ); + } + + /** + * @see FileBackend::getFileTimestamp() + * @param $params array + * @return bool|string + */ + public function getFileTimestamp( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams ); + } + + /** + * @see FileBackend::getFileSize() + * @param $params array + * @return bool|int + */ + public function getFileSize( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getFileSize( $realParams ); + } + + /** + * @see FileBackend::getFileStat() + * @param $params array + * @return Array|bool|null + */ + public function getFileStat( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getFileStat( $realParams ); + } + + /** + * @see FileBackend::getFileContents() + * @param $params array + * @return bool|string + */ + public function getFileContents( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getFileContents( $realParams ); + } + + /** + * @see FileBackend::getFileSha1Base36() + * @param $params array + * @return bool|string + */ + public function getFileSha1Base36( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams ); + } + + /** + * @see FileBackend::getFileProps() + * @param $params array + * @return Array + */ + public function getFileProps( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getFileProps( $realParams ); + } + + /** + * @see FileBackend::streamFile() + * @param $params array + * @return \Status + */ + public function streamFile( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->streamFile( $realParams ); + } + + /** + * @see FileBackend::getLocalReference() + * @param $params array + * @return FSFile|null + */ + public function getLocalReference( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getLocalReference( $realParams ); + } + + /** + * @see FileBackend::getLocalCopy() + * @param $params array + * @return null|TempFSFile + */ + public function getLocalCopy( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getLocalCopy( $realParams ); + } + + /** + * @see FileBackend::directoryExists() + * @param $params array + * @return bool|null + */ + public function directoryExists( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->directoryExists( $realParams ); + } + + /** + * @see FileBackend::getSubdirectoryList() + * @param $params array + * @return Array|null|Traversable + */ + public function getDirectoryList( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getDirectoryList( $realParams ); + } + + /** + * @see FileBackend::getFileList() + * @param $params array + * @return Array|null|\Traversable + */ + public function getFileList( array $params ) { + $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); + return $this->backends[$this->masterIndex]->getFileList( $realParams ); + } + + /** + * @see FileBackend::clearCache() + */ + public function clearCache( array $paths = null ) { + foreach ( $this->backends as $backend ) { + $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null; + $backend->clearCache( $realPaths ); + } + } + + /** + * @see FileBackend::getScopedLocksForOps() + */ + public function getScopedLocksForOps( array $ops, Status $status ) { + $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $ops ); + // Get the paths to lock from the master backend + $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps ); + // Get the paths under the proxy backend's name + $paths['sh'] = $this->unsubstPaths( $paths['sh'] ); + $paths['ex'] = $this->unsubstPaths( $paths['ex'] ); + return array( + $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ), + $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status ) + ); + } +} diff --git a/includes/filebackend/FileBackendStore.php b/includes/filebackend/FileBackendStore.php new file mode 100644 index 00000000..083dfea9 --- /dev/null +++ b/includes/filebackend/FileBackendStore.php @@ -0,0 +1,1766 @@ +<?php +/** + * Base class for all backends using particular storage medium. + * + * 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 FileBackend + * @author Aaron Schulz + */ + +/** + * @brief Base class for all backends using particular storage medium. + * + * This class defines the methods as abstract that subclasses must implement. + * Outside callers should *not* use functions with "Internal" in the name. + * + * The FileBackend operations are implemented using basic functions + * such as storeInternal(), copyInternal(), deleteInternal() and the like. + * This class is also responsible for path resolution and sanitization. + * + * @ingroup FileBackend + * @since 1.19 + */ +abstract class FileBackendStore extends FileBackend { + /** @var BagOStuff */ + protected $memCache; + /** @var ProcessCacheLRU */ + protected $cheapCache; // Map of paths to small (RAM/disk) cache items + /** @var ProcessCacheLRU */ + protected $expensiveCache; // Map of paths to large (RAM/disk) cache items + + /** @var Array Map of container names to sharding settings */ + protected $shardViaHashLevels = array(); // (container name => config array) + + protected $maxFileSize = 4294967296; // integer bytes (4GiB) + + /** + * @see FileBackend::__construct() + * + * @param $config Array + */ + public function __construct( array $config ) { + parent::__construct( $config ); + $this->memCache = new EmptyBagOStuff(); // disabled by default + $this->cheapCache = new ProcessCacheLRU( 300 ); + $this->expensiveCache = new ProcessCacheLRU( 5 ); + } + + /** + * Get the maximum allowable file size given backend + * medium restrictions and basic performance constraints. + * Do not call this function from places outside FileBackend and FileOp. + * + * @return integer Bytes + */ + final public function maxFileSizeInternal() { + return $this->maxFileSize; + } + + /** + * Check if a file can be created at a given storage path. + * FS backends should check if the parent directory exists and the file is writable. + * Backends using key/value stores should check if the container exists. + * + * @param $storagePath string + * @return bool + */ + abstract public function isPathUsableInternal( $storagePath ); + + /** + * Create a file in the backend with the given contents. + * Do not call this function from places outside FileBackend and FileOp. + * + * $params include: + * - content : the raw file contents + * - dst : destination storage path + * - overwrite : overwrite any file that exists at the destination + * - disposition : Content-Disposition header value for the destination + * - async : Status will be returned immediately if supported. + * If the status is OK, then its value field will be + * set to a FileBackendStoreOpHandle object. + * + * @param $params Array + * @return Status + */ + final public function createInternal( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) { + $status = Status::newFatal( 'backend-fail-maxsize', + $params['dst'], $this->maxFileSizeInternal() ); + } else { + $status = $this->doCreateInternal( $params ); + $this->clearCache( array( $params['dst'] ) ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->deleteFileCache( $params['dst'] ); // persistent cache + } + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::createInternal() + */ + abstract protected function doCreateInternal( array $params ); + + /** + * Store a file into the backend from a file on disk. + * Do not call this function from places outside FileBackend and FileOp. + * + * $params include: + * - src : source path on disk + * - dst : destination storage path + * - overwrite : overwrite any file that exists at the destination + * - disposition : Content-Disposition header value for the destination + * - async : Status will be returned immediately if supported. + * If the status is OK, then its value field will be + * set to a FileBackendStoreOpHandle object. + * + * @param $params Array + * @return Status + */ + final public function storeInternal( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) { + $status = Status::newFatal( 'backend-fail-maxsize', + $params['dst'], $this->maxFileSizeInternal() ); + } else { + $status = $this->doStoreInternal( $params ); + $this->clearCache( array( $params['dst'] ) ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->deleteFileCache( $params['dst'] ); // persistent cache + } + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::storeInternal() + */ + abstract protected function doStoreInternal( array $params ); + + /** + * Copy a file from one storage path to another in the backend. + * Do not call this function from places outside FileBackend and FileOp. + * + * $params include: + * - src : source storage path + * - dst : destination storage path + * - overwrite : overwrite any file that exists at the destination + * - disposition : Content-Disposition header value for the destination + * - async : Status will be returned immediately if supported. + * If the status is OK, then its value field will be + * set to a FileBackendStoreOpHandle object. + * + * @param $params Array + * @return Status + */ + final public function copyInternal( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = $this->doCopyInternal( $params ); + $this->clearCache( array( $params['dst'] ) ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->deleteFileCache( $params['dst'] ); // persistent cache + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::copyInternal() + */ + abstract protected function doCopyInternal( array $params ); + + /** + * Delete a file at the storage path. + * Do not call this function from places outside FileBackend and FileOp. + * + * $params include: + * - src : source storage path + * - ignoreMissingSource : do nothing if the source file does not exist + * - async : Status will be returned immediately if supported. + * If the status is OK, then its value field will be + * set to a FileBackendStoreOpHandle object. + * + * @param $params Array + * @return Status + */ + final public function deleteInternal( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = $this->doDeleteInternal( $params ); + $this->clearCache( array( $params['src'] ) ); + $this->deleteFileCache( $params['src'] ); // persistent cache + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::deleteInternal() + */ + abstract protected function doDeleteInternal( array $params ); + + /** + * Move a file from one storage path to another in the backend. + * Do not call this function from places outside FileBackend and FileOp. + * + * $params include: + * - src : source storage path + * - dst : destination storage path + * - overwrite : overwrite any file that exists at the destination + * - disposition : Content-Disposition header value for the destination + * - async : Status will be returned immediately if supported. + * If the status is OK, then its value field will be + * set to a FileBackendStoreOpHandle object. + * + * @param $params Array + * @return Status + */ + final public function moveInternal( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = $this->doMoveInternal( $params ); + $this->clearCache( array( $params['src'], $params['dst'] ) ); + $this->deleteFileCache( $params['src'] ); // persistent cache + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->deleteFileCache( $params['dst'] ); // persistent cache + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::moveInternal() + * @return Status + */ + protected function doMoveInternal( array $params ) { + unset( $params['async'] ); // two steps, won't work here :) + // Copy source to dest + $status = $this->copyInternal( $params ); + if ( $status->isOK() ) { + // Delete source (only fails due to races or medium going down) + $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) ); + $status->setResult( true, $status->value ); // ignore delete() errors + } + return $status; + } + + /** + * No-op file operation that does nothing. + * Do not call this function from places outside FileBackend and FileOp. + * + * @param $params Array + * @return Status + */ + final public function nullInternal( array $params ) { + return Status::newGood(); + } + + /** + * @see FileBackend::concatenate() + * @return Status + */ + final public function concatenate( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = Status::newGood(); + + // Try to lock the source files for the scope of this function + $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status ); + if ( $status->isOK() ) { + // Actually do the file concatenation... + $start_time = microtime( true ); + $status->merge( $this->doConcatenate( $params ) ); + $sec = microtime( true ) - $start_time; + if ( !$status->isOK() ) { + wfDebugLog( 'FileOperation', get_class( $this ) . " failed to concatenate " . + count( $params['srcs'] ) . " file(s) [$sec sec]" ); + } + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::concatenate() + * @return Status + */ + protected function doConcatenate( array $params ) { + $status = Status::newGood(); + $tmpPath = $params['dst']; // convenience + + // Check that the specified temp file is valid... + wfSuppressWarnings(); + $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) ); + wfRestoreWarnings(); + if ( !$ok ) { // not present or not empty + $status->fatal( 'backend-fail-opentemp', $tmpPath ); + return $status; + } + + // Build up the temp file using the source chunks (in order)... + $tmpHandle = fopen( $tmpPath, 'ab' ); + if ( $tmpHandle === false ) { + $status->fatal( 'backend-fail-opentemp', $tmpPath ); + return $status; + } + foreach ( $params['srcs'] as $virtualSource ) { + // Get a local FS version of the chunk + $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) ); + if ( !$tmpFile ) { + $status->fatal( 'backend-fail-read', $virtualSource ); + return $status; + } + // Get a handle to the local FS version + $sourceHandle = fopen( $tmpFile->getPath(), 'r' ); + if ( $sourceHandle === false ) { + fclose( $tmpHandle ); + $status->fatal( 'backend-fail-read', $virtualSource ); + return $status; + } + // Append chunk to file (pass chunk size to avoid magic quotes) + if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) { + fclose( $sourceHandle ); + fclose( $tmpHandle ); + $status->fatal( 'backend-fail-writetemp', $tmpPath ); + return $status; + } + fclose( $sourceHandle ); + } + if ( !fclose( $tmpHandle ) ) { + $status->fatal( 'backend-fail-closetemp', $tmpPath ); + return $status; + } + + clearstatcache(); // temp file changed + + return $status; + } + + /** + * @see FileBackend::doPrepare() + * @return Status + */ + final protected function doPrepare( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + + $status = Status::newGood(); + list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); + if ( $dir === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; // invalid storage path + } + + if ( $shard !== null ) { // confined to a single container/shard + $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) ); + } else { // directory is on several shards + wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); + list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); + foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { + $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) ); + } + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::doPrepare() + * @return Status + */ + protected function doPrepareInternal( $container, $dir, array $params ) { + return Status::newGood(); + } + + /** + * @see FileBackend::doSecure() + * @return Status + */ + final protected function doSecure( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = Status::newGood(); + + list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); + if ( $dir === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; // invalid storage path + } + + if ( $shard !== null ) { // confined to a single container/shard + $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) ); + } else { // directory is on several shards + wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); + list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); + foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { + $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) ); + } + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::doSecure() + * @return Status + */ + protected function doSecureInternal( $container, $dir, array $params ) { + return Status::newGood(); + } + + /** + * @see FileBackend::doPublish() + * @return Status + */ + final protected function doPublish( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = Status::newGood(); + + list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); + if ( $dir === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; // invalid storage path + } + + if ( $shard !== null ) { // confined to a single container/shard + $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) ); + } else { // directory is on several shards + wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); + list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); + foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { + $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) ); + } + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::doPublish() + * @return Status + */ + protected function doPublishInternal( $container, $dir, array $params ) { + return Status::newGood(); + } + + /** + * @see FileBackend::doClean() + * @return Status + */ + final protected function doClean( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = Status::newGood(); + + // Recursive: first delete all empty subdirs recursively + if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) { + $subDirsRel = $this->getTopDirectoryList( array( 'dir' => $params['dir'] ) ); + if ( $subDirsRel !== null ) { // no errors + foreach ( $subDirsRel as $subDirRel ) { + $subDir = $params['dir'] . "/{$subDirRel}"; // full path + $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) ); + } + } + } + + list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); + if ( $dir === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; // invalid storage path + } + + // Attempt to lock this directory... + $filesLockEx = array( $params['dir'] ); + $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); + if ( !$status->isOK() ) { + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; // abort + } + + if ( $shard !== null ) { // confined to a single container/shard + $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) ); + $this->deleteContainerCache( $fullCont ); // purge cache + } else { // directory is on several shards + wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); + list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); + foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { + $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) ); + $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache + } + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::doClean() + * @return Status + */ + protected function doCleanInternal( $container, $dir, array $params ) { + return Status::newGood(); + } + + /** + * @see FileBackend::fileExists() + * @return bool|null + */ + final public function fileExists( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $stat = $this->getFileStat( $params ); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return ( $stat === null ) ? null : (bool)$stat; // null => failure + } + + /** + * @see FileBackend::getFileTimestamp() + * @return bool + */ + final public function getFileTimestamp( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $stat = $this->getFileStat( $params ); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $stat ? $stat['mtime'] : false; + } + + /** + * @see FileBackend::getFileSize() + * @return bool + */ + final public function getFileSize( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $stat = $this->getFileStat( $params ); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $stat ? $stat['size'] : false; + } + + /** + * @see FileBackend::getFileStat() + * @return bool + */ + final public function getFileStat( array $params ) { + $path = self::normalizeStoragePath( $params['src'] ); + if ( $path === null ) { + return false; // invalid storage path + } + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $latest = !empty( $params['latest'] ); // use latest data? + if ( !$this->cheapCache->has( $path, 'stat' ) ) { + $this->primeFileCache( array( $path ) ); // check persistent cache + } + if ( $this->cheapCache->has( $path, 'stat' ) ) { + $stat = $this->cheapCache->get( $path, 'stat' ); + // If we want the latest data, check that this cached + // value was in fact fetched with the latest available data. + if ( !$latest || $stat['latest'] ) { + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $stat; + } + } + wfProfileIn( __METHOD__ . '-miss' ); + wfProfileIn( __METHOD__ . '-miss-' . $this->name ); + $stat = $this->doGetFileStat( $params ); + wfProfileOut( __METHOD__ . '-miss-' . $this->name ); + wfProfileOut( __METHOD__ . '-miss' ); + if ( is_array( $stat ) ) { // don't cache negatives + $stat['latest'] = $latest; + $this->cheapCache->set( $path, 'stat', $stat ); + $this->setFileCache( $path, $stat ); // update persistent cache + if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata + $this->cheapCache->set( $path, 'sha1', + array( 'hash' => $stat['sha1'], 'latest' => $latest ) ); + } + } else { + wfDebug( __METHOD__ . ": File $path does not exist.\n" ); + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $stat; + } + + /** + * @see FileBackendStore::getFileStat() + */ + abstract protected function doGetFileStat( array $params ); + + /** + * @see FileBackend::getFileContents() + * @return bool|string + */ + public function getFileContents( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $tmpFile = $this->getLocalReference( $params ); + if ( !$tmpFile ) { + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return false; + } + wfSuppressWarnings(); + $data = file_get_contents( $tmpFile->getPath() ); + wfRestoreWarnings(); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $data; + } + + /** + * @see FileBackend::getFileSha1Base36() + * @return bool|string + */ + final public function getFileSha1Base36( array $params ) { + $path = self::normalizeStoragePath( $params['src'] ); + if ( $path === null ) { + return false; // invalid storage path + } + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $latest = !empty( $params['latest'] ); // use latest data? + if ( $this->cheapCache->has( $path, 'sha1' ) ) { + $stat = $this->cheapCache->get( $path, 'sha1' ); + // If we want the latest data, check that this cached + // value was in fact fetched with the latest available data. + if ( !$latest || $stat['latest'] ) { + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $stat['hash']; + } + } + wfProfileIn( __METHOD__ . '-miss' ); + wfProfileIn( __METHOD__ . '-miss-' . $this->name ); + $hash = $this->doGetFileSha1Base36( $params ); + wfProfileOut( __METHOD__ . '-miss-' . $this->name ); + wfProfileOut( __METHOD__ . '-miss' ); + if ( $hash ) { // don't cache negatives + $this->cheapCache->set( $path, 'sha1', + array( 'hash' => $hash, 'latest' => $latest ) ); + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $hash; + } + + /** + * @see FileBackendStore::getFileSha1Base36() + * @return bool|string + */ + protected function doGetFileSha1Base36( array $params ) { + $fsFile = $this->getLocalReference( $params ); + if ( !$fsFile ) { + return false; + } else { + return $fsFile->getSha1Base36(); + } + } + + /** + * @see FileBackend::getFileProps() + * @return Array + */ + final public function getFileProps( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $fsFile = $this->getLocalReference( $params ); + $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps(); + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $props; + } + + /** + * @see FileBackend::getLocalReference() + * @return TempFSFile|null + */ + public function getLocalReference( array $params ) { + $path = self::normalizeStoragePath( $params['src'] ); + if ( $path === null ) { + return null; // invalid storage path + } + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $latest = !empty( $params['latest'] ); // use latest data? + if ( $this->expensiveCache->has( $path, 'localRef' ) ) { + $val = $this->expensiveCache->get( $path, 'localRef' ); + // If we want the latest data, check that this cached + // value was in fact fetched with the latest available data. + if ( !$latest || $val['latest'] ) { + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $val['object']; + } + } + $tmpFile = $this->getLocalCopy( $params ); + if ( $tmpFile ) { // don't cache negatives + $this->expensiveCache->set( $path, 'localRef', + array( 'object' => $tmpFile, 'latest' => $latest ) ); + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $tmpFile; + } + + /** + * @see FileBackend::streamFile() + * @return Status + */ + final public function streamFile( array $params ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = Status::newGood(); + + $info = $this->getFileStat( $params ); + if ( !$info ) { // let StreamFile handle the 404 + $status->fatal( 'backend-fail-notexists', $params['src'] ); + } + + // Set output buffer and HTTP headers for stream + $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array(); + $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders ); + if ( $res == StreamFile::NOT_MODIFIED ) { + // do nothing; client cache is up to date + } elseif ( $res == StreamFile::READY_STREAM ) { + wfProfileIn( __METHOD__ . '-send' ); + wfProfileIn( __METHOD__ . '-send-' . $this->name ); + $status = $this->doStreamFile( $params ); + wfProfileOut( __METHOD__ . '-send-' . $this->name ); + wfProfileOut( __METHOD__ . '-send' ); + } else { + $status->fatal( 'backend-fail-stream', $params['src'] ); + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackendStore::streamFile() + * @return Status + */ + protected function doStreamFile( array $params ) { + $status = Status::newGood(); + + $fsFile = $this->getLocalReference( $params ); + if ( !$fsFile ) { + $status->fatal( 'backend-fail-stream', $params['src'] ); + } elseif ( !readfile( $fsFile->getPath() ) ) { + $status->fatal( 'backend-fail-stream', $params['src'] ); + } + + return $status; + } + + /** + * @see FileBackend::directoryExists() + * @return bool|null + */ + final public function directoryExists( array $params ) { + list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); + if ( $dir === null ) { + return false; // invalid storage path + } + if ( $shard !== null ) { // confined to a single container/shard + return $this->doDirectoryExists( $fullCont, $dir, $params ); + } else { // directory is on several shards + wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); + list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); + $res = false; // response + foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { + $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params ); + if ( $exists ) { + $res = true; + break; // found one! + } elseif ( $exists === null ) { // error? + $res = null; // if we don't find anything, it is indeterminate + } + } + return $res; + } + } + + /** + * @see FileBackendStore::directoryExists() + * + * @param $container string Resolved container name + * @param $dir string Resolved path relative to container + * @param $params Array + * @return bool|null + */ + abstract protected function doDirectoryExists( $container, $dir, array $params ); + + /** + * @see FileBackend::getDirectoryList() + * @return Traversable|Array|null Returns null on failure + */ + final public function getDirectoryList( array $params ) { + list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); + if ( $dir === null ) { // invalid storage path + return null; + } + if ( $shard !== null ) { + // File listing is confined to a single container/shard + return $this->getDirectoryListInternal( $fullCont, $dir, $params ); + } else { + wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); + // File listing spans multiple containers/shards + list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); + return new FileBackendStoreShardDirIterator( $this, + $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params ); + } + } + + /** + * Do not call this function from places outside FileBackend + * + * @see FileBackendStore::getDirectoryList() + * + * @param $container string Resolved container name + * @param $dir string Resolved path relative to container + * @param $params Array + * @return Traversable|Array|null Returns null on failure + */ + abstract public function getDirectoryListInternal( $container, $dir, array $params ); + + /** + * @see FileBackend::getFileList() + * @return Traversable|Array|null Returns null on failure + */ + final public function getFileList( array $params ) { + list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); + if ( $dir === null ) { // invalid storage path + return null; + } + if ( $shard !== null ) { + // File listing is confined to a single container/shard + return $this->getFileListInternal( $fullCont, $dir, $params ); + } else { + wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); + // File listing spans multiple containers/shards + list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); + return new FileBackendStoreShardFileIterator( $this, + $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params ); + } + } + + /** + * Do not call this function from places outside FileBackend + * + * @see FileBackendStore::getFileList() + * + * @param $container string Resolved container name + * @param $dir string Resolved path relative to container + * @param $params Array + * @return Traversable|Array|null Returns null on failure + */ + abstract public function getFileListInternal( $container, $dir, array $params ); + + /** + * Return a list of FileOp objects from a list of operations. + * Do not call this function from places outside FileBackend. + * + * The result must have the same number of items as the input. + * An exception is thrown if an unsupported operation is requested. + * + * @param $ops Array Same format as doOperations() + * @return Array List of FileOp objects + * @throws MWException + */ + final public function getOperationsInternal( array $ops ) { + $supportedOps = array( + 'store' => 'StoreFileOp', + 'copy' => 'CopyFileOp', + 'move' => 'MoveFileOp', + 'delete' => 'DeleteFileOp', + 'create' => 'CreateFileOp', + 'null' => 'NullFileOp' + ); + + $performOps = array(); // array of FileOp objects + // Build up ordered array of FileOps... + foreach ( $ops as $operation ) { + $opName = $operation['op']; + if ( isset( $supportedOps[$opName] ) ) { + $class = $supportedOps[$opName]; + // Get params for this operation + $params = $operation; + // Append the FileOp class + $performOps[] = new $class( $this, $params ); + } else { + throw new MWException( "Operation '$opName' is not supported." ); + } + } + + return $performOps; + } + + /** + * Get a list of storage paths to lock for a list of operations + * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys, + * each corresponding to a list of storage paths to be locked. + * + * @param $performOps Array List of FileOp objects + * @return Array ('sh' => list of paths, 'ex' => list of paths) + */ + final public function getPathsToLockForOpsInternal( array $performOps ) { + // Build up a list of files to lock... + $paths = array( 'sh' => array(), 'ex' => array() ); + foreach ( $performOps as $fileOp ) { + $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() ); + $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() ); + } + // Optimization: if doing an EX lock anyway, don't also set an SH one + $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] ); + // Get a shared lock on the parent directory of each path changed + $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) ); + + return $paths; + } + + /** + * @see FileBackend::getScopedLocksForOps() + * @return Array + */ + public function getScopedLocksForOps( array $ops, Status $status ) { + $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) ); + return array( + $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ), + $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status ) + ); + } + + /** + * @see FileBackend::doOperationsInternal() + * @return Status + */ + final protected function doOperationsInternal( array $ops, array $opts ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = Status::newGood(); + + // Build up a list of FileOps... + $performOps = $this->getOperationsInternal( $ops ); + + // Acquire any locks as needed... + if ( empty( $opts['nonLocking'] ) ) { + // Build up a list of files to lock... + $paths = $this->getPathsToLockForOpsInternal( $performOps ); + // Try to lock those files for the scope of this function... + $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ); + $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status ); + if ( !$status->isOK() ) { + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; // abort + } + } + + // Clear any file cache entries (after locks acquired) + if ( empty( $opts['preserveCache'] ) ) { + $this->clearCache(); + } + + // Load from the persistent file and container caches + $this->primeFileCache( $performOps ); + $this->primeContainerCache( $performOps ); + + // Actually attempt the operation batch... + $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal ); + + // Merge errors into status fields + $status->merge( $subStatus ); + $status->success = $subStatus->success; // not done in merge() + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * @see FileBackend::doQuickOperationsInternal() + * @return Status + * @throws MWException + */ + final protected function doQuickOperationsInternal( array $ops ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + $status = Status::newGood(); + + $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' ); + $async = ( $this->parallelize === 'implicit' ); + $maxConcurrency = $this->concurrency; // throttle + + $statuses = array(); // array of (index => Status) + $fileOpHandles = array(); // list of (index => handle) arrays + $curFileOpHandles = array(); // current handle batch + // Perform the sync-only ops and build up op handles for the async ops... + foreach ( $ops as $index => $params ) { + if ( !in_array( $params['op'], $supportedOps ) ) { + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + throw new MWException( "Operation '{$params['op']}' is not supported." ); + } + $method = $params['op'] . 'Internal'; // e.g. "storeInternal" + $subStatus = $this->$method( array( 'async' => $async ) + $params ); + if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async + if ( count( $curFileOpHandles ) >= $maxConcurrency ) { + $fileOpHandles[] = $curFileOpHandles; // push this batch + $curFileOpHandles = array(); + } + $curFileOpHandles[$index] = $subStatus->value; // keep index + } else { // error or completed + $statuses[$index] = $subStatus; // keep index + } + } + if ( count( $curFileOpHandles ) ) { + $fileOpHandles[] = $curFileOpHandles; // last batch + } + // Do all the async ops that can be done concurrently... + foreach ( $fileOpHandles as $fileHandleBatch ) { + $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch ); + } + // Marshall and merge all the responses... + foreach ( $statuses as $index => $subStatus ) { + $status->merge( $subStatus ); + if ( $subStatus->isOK() ) { + $status->success[$index] = true; + ++$status->successCount; + } else { + $status->success[$index] = false; + ++$status->failCount; + } + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * Execute a list of FileBackendStoreOpHandle handles in parallel. + * The resulting Status object fields will correspond + * to the order in which the handles where given. + * + * @param $handles Array List of FileBackendStoreOpHandle objects + * @return Array Map of Status objects + * @throws MWException + */ + final public function executeOpHandlesInternal( array $fileOpHandles ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + foreach ( $fileOpHandles as $fileOpHandle ) { + if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) { + throw new MWException( "Given a non-FileBackendStoreOpHandle object." ); + } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) { + throw new MWException( "Given a FileBackendStoreOpHandle for the wrong backend." ); + } + } + $res = $this->doExecuteOpHandlesInternal( $fileOpHandles ); + foreach ( $fileOpHandles as $fileOpHandle ) { + $fileOpHandle->closeResources(); + } + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + return $res; + } + + /** + * @see FileBackendStore::executeOpHandlesInternal() + * @return Array List of corresponding Status objects + */ + protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { + foreach ( $fileOpHandles as $fileOpHandle ) { // OK if empty + throw new MWException( "This backend supports no asynchronous operations." ); + } + return array(); + } + + /** + * @see FileBackend::preloadCache() + */ + final public function preloadCache( array $paths ) { + $fullConts = array(); // full container names + foreach ( $paths as $path ) { + list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path ); + $fullConts[] = $fullCont; + } + // Load from the persistent file and container caches + $this->primeContainerCache( $fullConts ); + $this->primeFileCache( $paths ); + } + + /** + * @see FileBackend::clearCache() + */ + final public function clearCache( array $paths = null ) { + if ( is_array( $paths ) ) { + $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); + $paths = array_filter( $paths, 'strlen' ); // remove nulls + } + if ( $paths === null ) { + $this->cheapCache->clear(); + $this->expensiveCache->clear(); + } else { + foreach ( $paths as $path ) { + $this->cheapCache->clear( $path ); + $this->expensiveCache->clear( $path ); + } + } + $this->doClearCache( $paths ); + } + + /** + * Clears any additional stat caches for storage paths + * + * @see FileBackend::clearCache() + * + * @param $paths Array Storage paths (optional) + * @return void + */ + protected function doClearCache( array $paths = null ) {} + + /** + * Is this a key/value store where directories are just virtual? + * Virtual directories exists in so much as files exists that are + * prefixed with the directory path followed by a forward slash. + * + * @return bool + */ + abstract protected function directoriesAreVirtual(); + + /** + * Check if a container name is valid. + * This checks for for length and illegal characters. + * + * @param $container string + * @return bool + */ + final protected static function isValidContainerName( $container ) { + // This accounts for Swift and S3 restrictions while leaving room + // for things like '.xxx' (hex shard chars) or '.seg' (segments). + // This disallows directory separators or traversal characters. + // Note that matching strings URL encode to the same string; + // in Swift, the length restriction is *after* URL encoding. + return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container ); + } + + /** + * Splits a storage path into an internal container name, + * an internal relative file name, and a container shard suffix. + * Any shard suffix is already appended to the internal container name. + * This also checks that the storage path is valid and within this backend. + * + * If the container is sharded but a suffix could not be determined, + * this means that the path can only refer to a directory and can only + * be scanned by looking in all the container shards. + * + * @param $storagePath string + * @return Array (container, path, container suffix) or (null, null, null) if invalid + */ + final protected function resolveStoragePath( $storagePath ) { + list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); + if ( $backend === $this->name ) { // must be for this backend + $relPath = self::normalizeContainerPath( $relPath ); + if ( $relPath !== null ) { + // Get shard for the normalized path if this container is sharded + $cShard = $this->getContainerShard( $container, $relPath ); + // Validate and sanitize the relative path (backend-specific) + $relPath = $this->resolveContainerPath( $container, $relPath ); + if ( $relPath !== null ) { + // Prepend any wiki ID prefix to the container name + $container = $this->fullContainerName( $container ); + if ( self::isValidContainerName( $container ) ) { + // Validate and sanitize the container name (backend-specific) + $container = $this->resolveContainerName( "{$container}{$cShard}" ); + if ( $container !== null ) { + return array( $container, $relPath, $cShard ); + } + } + } + } + } + return array( null, null, null ); + } + + /** + * Like resolveStoragePath() except null values are returned if + * the container is sharded and the shard could not be determined. + * + * @see FileBackendStore::resolveStoragePath() + * + * @param $storagePath string + * @return Array (container, path) or (null, null) if invalid + */ + final protected function resolveStoragePathReal( $storagePath ) { + list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath ); + if ( $cShard !== null ) { + return array( $container, $relPath ); + } + return array( null, null ); + } + + /** + * Get the container name shard suffix for a given path. + * Any empty suffix means the container is not sharded. + * + * @param $container string Container name + * @param $relPath string Storage path relative to the container + * @return string|null Returns null if shard could not be determined + */ + final protected function getContainerShard( $container, $relPath ) { + list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container ); + if ( $levels == 1 || $levels == 2 ) { + // Hash characters are either base 16 or 36 + $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]'; + // Get a regex that represents the shard portion of paths. + // The concatenation of the captures gives us the shard. + if ( $levels === 1 ) { // 16 or 36 shards per container + $hashDirRegex = '(' . $char . ')'; + } else { // 256 or 1296 shards per container + if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc") + $hashDirRegex = $char . '/(' . $char . '{2})'; + } else { // short hash dir format (e.g. "a/b/c") + $hashDirRegex = '(' . $char . ')/(' . $char . ')'; + } + } + // Allow certain directories to be above the hash dirs so as + // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab"). + // They must be 2+ chars to avoid any hash directory ambiguity. + $m = array(); + if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) { + return '.' . implode( '', array_slice( $m, 1 ) ); + } + return null; // failed to match + } + return ''; // no sharding + } + + /** + * Check if a storage path maps to a single shard. + * Container dirs like "a", where the container shards on "x/xy", + * can reside on several shards. Such paths are tricky to handle. + * + * @param $storagePath string Storage path + * @return bool + */ + final public function isSingleShardPathInternal( $storagePath ) { + list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath ); + return ( $shard !== null ); + } + + /** + * Get the sharding config for a container. + * If greater than 0, then all file storage paths within + * the container are required to be hashed accordingly. + * + * @param $container string + * @return Array (integer levels, integer base, repeat flag) or (0, 0, false) + */ + final protected function getContainerHashLevels( $container ) { + if ( isset( $this->shardViaHashLevels[$container] ) ) { + $config = $this->shardViaHashLevels[$container]; + $hashLevels = (int)$config['levels']; + if ( $hashLevels == 1 || $hashLevels == 2 ) { + $hashBase = (int)$config['base']; + if ( $hashBase == 16 || $hashBase == 36 ) { + return array( $hashLevels, $hashBase, $config['repeat'] ); + } + } + } + return array( 0, 0, false ); // no sharding + } + + /** + * Get a list of full container shard suffixes for a container + * + * @param $container string + * @return Array + */ + final protected function getContainerSuffixes( $container ) { + $shards = array(); + list( $digits, $base ) = $this->getContainerHashLevels( $container ); + if ( $digits > 0 ) { + $numShards = pow( $base, $digits ); + for ( $index = 0; $index < $numShards; $index++ ) { + $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits ); + } + } + return $shards; + } + + /** + * Get the full container name, including the wiki ID prefix + * + * @param $container string + * @return string + */ + final protected function fullContainerName( $container ) { + if ( $this->wikiId != '' ) { + return "{$this->wikiId}-$container"; + } else { + return $container; + } + } + + /** + * Resolve a container name, checking if it's allowed by the backend. + * This is intended for internal use, such as encoding illegal chars. + * Subclasses can override this to be more restrictive. + * + * @param $container string + * @return string|null + */ + protected function resolveContainerName( $container ) { + return $container; + } + + /** + * Resolve a relative storage path, checking if it's allowed by the backend. + * This is intended for internal use, such as encoding illegal chars or perhaps + * getting absolute paths (e.g. FS based backends). Note that the relative path + * may be the empty string (e.g. the path is simply to the container). + * + * @param $container string Container name + * @param $relStoragePath string Storage path relative to the container + * @return string|null Path or null if not valid + */ + protected function resolveContainerPath( $container, $relStoragePath ) { + return $relStoragePath; + } + + /** + * Get the cache key for a container + * + * @param $container string Resolved container name + * @return string + */ + private function containerCacheKey( $container ) { + return wfMemcKey( 'backend', $this->getName(), 'container', $container ); + } + + /** + * Set the cached info for a container + * + * @param $container string Resolved container name + * @param $val mixed Information to cache + */ + final protected function setContainerCache( $container, $val ) { + $this->memCache->add( $this->containerCacheKey( $container ), $val, 14*86400 ); + } + + /** + * Delete the cached info for a container. + * The cache key is salted for a while to prevent race conditions. + * + * @param $container string Resolved container name + */ + final protected function deleteContainerCache( $container ) { + if ( !$this->memCache->set( $this->containerCacheKey( $container ), 'PURGED', 300 ) ) { + trigger_error( "Unable to delete stat cache for container $container." ); + } + } + + /** + * Do a batch lookup from cache for container stats for all containers + * used in a list of container names, storage paths, or FileOp objects. + * + * @param $items Array + * @return void + */ + final protected function primeContainerCache( array $items ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + + $paths = array(); // list of storage paths + $contNames = array(); // (cache key => resolved container name) + // Get all the paths/containers from the items... + foreach ( $items as $item ) { + if ( $item instanceof FileOp ) { + $paths = array_merge( $paths, $item->storagePathsRead() ); + $paths = array_merge( $paths, $item->storagePathsChanged() ); + } elseif ( self::isStoragePath( $item ) ) { + $paths[] = $item; + } elseif ( is_string( $item ) ) { // full container name + $contNames[$this->containerCacheKey( $item )] = $item; + } + } + // Get all the corresponding cache keys for paths... + foreach ( $paths as $path ) { + list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path ); + if ( $fullCont !== null ) { // valid path for this backend + $contNames[$this->containerCacheKey( $fullCont )] = $fullCont; + } + } + + $contInfo = array(); // (resolved container name => cache value) + // Get all cache entries for these container cache keys... + $values = $this->memCache->getMulti( array_keys( $contNames ) ); + foreach ( $values as $cacheKey => $val ) { + $contInfo[$contNames[$cacheKey]] = $val; + } + + // Populate the container process cache for the backend... + $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) ); + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + } + + /** + * Fill the backend-specific process cache given an array of + * resolved container names and their corresponding cached info. + * Only containers that actually exist should appear in the map. + * + * @param $containerInfo Array Map of resolved container names to cached info + * @return void + */ + protected function doPrimeContainerCache( array $containerInfo ) {} + + /** + * Get the cache key for a file path + * + * @param $path string Storage path + * @return string + */ + private function fileCacheKey( $path ) { + return wfMemcKey( 'backend', $this->getName(), 'file', sha1( $path ) ); + } + + /** + * Set the cached stat info for a file path. + * Negatives (404s) are not cached. By not caching negatives, we can skip cache + * salting for the case when a file is created at a path were there was none before. + * + * @param $path string Storage path + * @param $val mixed Information to cache + */ + final protected function setFileCache( $path, $val ) { + $this->memCache->add( $this->fileCacheKey( $path ), $val, 7*86400 ); + } + + /** + * Delete the cached stat info for a file path. + * The cache key is salted for a while to prevent race conditions. + * + * @param $path string Storage path + */ + final protected function deleteFileCache( $path ) { + if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) { + trigger_error( "Unable to delete stat cache for file $path." ); + } + } + + /** + * Do a batch lookup from cache for file stats for all paths + * used in a list of storage paths or FileOp objects. + * + * @param $items Array List of storage paths or FileOps + * @return void + */ + final protected function primeFileCache( array $items ) { + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-' . $this->name ); + + $paths = array(); // list of storage paths + $pathNames = array(); // (cache key => storage path) + // Get all the paths/containers from the items... + foreach ( $items as $item ) { + if ( $item instanceof FileOp ) { + $paths = array_merge( $paths, $item->storagePathsRead() ); + $paths = array_merge( $paths, $item->storagePathsChanged() ); + } elseif ( self::isStoragePath( $item ) ) { + $paths[] = $item; + } + } + // Get all the corresponding cache keys for paths... + foreach ( $paths as $path ) { + list( $cont, $rel, $s ) = $this->resolveStoragePath( $path ); + if ( $rel !== null ) { // valid path for this backend + $pathNames[$this->fileCacheKey( $path )] = $path; + } + } + // Get all cache entries for these container cache keys... + $values = $this->memCache->getMulti( array_keys( $pathNames ) ); + foreach ( $values as $cacheKey => $val ) { + if ( is_array( $val ) ) { + $path = $pathNames[$cacheKey]; + $this->cheapCache->set( $path, 'stat', $val ); + if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata + $this->cheapCache->set( $path, 'sha1', + array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) ); + } + } + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + wfProfileOut( __METHOD__ ); + } +} + +/** + * FileBackendStore helper class for performing asynchronous file operations. + * + * For example, calling FileBackendStore::createInternal() with the "async" + * param flag may result in a Status that contains this object as a value. + * This class is largely backend-specific and is mostly just "magic" to be + * passed to FileBackendStore::executeOpHandlesInternal(). + */ +abstract class FileBackendStoreOpHandle { + /** @var Array */ + public $params = array(); // params to caller functions + /** @var FileBackendStore */ + public $backend; + /** @var Array */ + public $resourcesToClose = array(); + + public $call; // string; name that identifies the function called + + /** + * Close all open file handles + * + * @return void + */ + public function closeResources() { + array_map( 'fclose', $this->resourcesToClose ); + } +} + +/** + * FileBackendStore helper function to handle listings that span container shards. + * Do not use this class from places outside of FileBackendStore. + * + * @ingroup FileBackend + */ +abstract class FileBackendStoreShardListIterator implements Iterator { + /** @var FileBackendStore */ + protected $backend; + /** @var Array */ + protected $params; + /** @var Array */ + protected $shardSuffixes; + protected $container; // string; full container name + protected $directory; // string; resolved relative path + + /** @var Traversable */ + protected $iter; + protected $curShard = 0; // integer + protected $pos = 0; // integer + + /** @var Array */ + protected $multiShardPaths = array(); // (rel path => 1) + + /** + * @param $backend FileBackendStore + * @param $container string Full storage container name + * @param $dir string Storage directory relative to container + * @param $suffixes Array List of container shard suffixes + * @param $params Array + */ + public function __construct( + FileBackendStore $backend, $container, $dir, array $suffixes, array $params + ) { + $this->backend = $backend; + $this->container = $container; + $this->directory = $dir; + $this->shardSuffixes = $suffixes; + $this->params = $params; + } + + /** + * @see Iterator::key() + * @return integer + */ + public function key() { + return $this->pos; + } + + /** + * @see Iterator::valid() + * @return bool + */ + public function valid() { + if ( $this->iter instanceof Iterator ) { + return $this->iter->valid(); + } elseif ( is_array( $this->iter ) ) { + return ( current( $this->iter ) !== false ); // no paths can have this value + } + return false; // some failure? + } + + /** + * @see Iterator::current() + * @return string|bool String or false + */ + public function current() { + return ( $this->iter instanceof Iterator ) + ? $this->iter->current() + : current( $this->iter ); + } + + /** + * @see Iterator::next() + * @return void + */ + public function next() { + ++$this->pos; + ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter ); + do { + $continue = false; // keep scanning shards? + $this->filterViaNext(); // filter out duplicates + // Find the next non-empty shard if no elements are left + if ( !$this->valid() ) { + $this->nextShardIteratorIfNotValid(); + $continue = $this->valid(); // re-filter unless we ran out of shards + } + } while ( $continue ); + } + + /** + * @see Iterator::rewind() + * @return void + */ + public function rewind() { + $this->pos = 0; + $this->curShard = 0; + $this->setIteratorFromCurrentShard(); + do { + $continue = false; // keep scanning shards? + $this->filterViaNext(); // filter out duplicates + // Find the next non-empty shard if no elements are left + if ( !$this->valid() ) { + $this->nextShardIteratorIfNotValid(); + $continue = $this->valid(); // re-filter unless we ran out of shards + } + } while ( $continue ); + } + + /** + * Filter out duplicate items by advancing to the next ones + */ + protected function filterViaNext() { + while ( $this->valid() ) { + $rel = $this->iter->current(); // path relative to given directory + $path = $this->params['dir'] . "/{$rel}"; // full storage path + if ( $this->backend->isSingleShardPathInternal( $path ) ) { + break; // path is only on one shard; no issue with duplicates + } elseif ( isset( $this->multiShardPaths[$rel] ) ) { + // Don't keep listing paths that are on multiple shards + ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter ); + } else { + $this->multiShardPaths[$rel] = 1; + break; + } + } + } + + /** + * If the list iterator for this container shard is out of items, + * then move on to the next container that has items. + * If there are none, then it advances to the last container. + */ + protected function nextShardIteratorIfNotValid() { + while ( !$this->valid() && ++$this->curShard < count( $this->shardSuffixes ) ) { + $this->setIteratorFromCurrentShard(); + } + } + + /** + * Set the list iterator to that of the current container shard + */ + protected function setIteratorFromCurrentShard() { + $this->iter = $this->listFromShard( + $this->container . $this->shardSuffixes[$this->curShard], + $this->directory, $this->params ); + // Start loading results so that current() works + if ( $this->iter ) { + ( $this->iter instanceof Iterator ) ? $this->iter->rewind() : reset( $this->iter ); + } + } + + /** + * Get the list for a given container shard + * + * @param $container string Resolved container name + * @param $dir string Resolved path relative to container + * @param $params Array + * @return Traversable|Array|null + */ + abstract protected function listFromShard( $container, $dir, array $params ); +} + +/** + * Iterator for listing directories + */ +class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator { + /** + * @see FileBackendStoreShardListIterator::listFromShard() + * @return Array|null|Traversable + */ + protected function listFromShard( $container, $dir, array $params ) { + return $this->backend->getDirectoryListInternal( $container, $dir, $params ); + } +} + +/** + * Iterator for listing regular files + */ +class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator { + /** + * @see FileBackendStoreShardListIterator::listFromShard() + * @return Array|null|Traversable + */ + protected function listFromShard( $container, $dir, array $params ) { + return $this->backend->getFileListInternal( $container, $dir, $params ); + } +} diff --git a/includes/filerepo/backend/FileOp.php b/includes/filebackend/FileOp.php index 5844c9f2..7c43c489 100644 --- a/includes/filerepo/backend/FileOp.php +++ b/includes/filebackend/FileOp.php @@ -1,17 +1,35 @@ <?php /** + * Helper class for representing operations with transaction support. + * + * 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 FileBackend * @author Aaron Schulz */ /** - * Helper class for representing operations with transaction support. + * FileBackend helper class for representing operations. * Do not use this class from places outside FileBackend. * - * Methods called from attemptBatch() should avoid throwing exceptions at all costs. - * FileOp objects should be lightweight in order to support large arrays in memory. - * + * Methods called from FileOpBatch::attempt() should avoid throwing + * exceptions at all costs. FileOp objects should be lightweight in order + * to support large arrays in memory and serialization. + * * @ingroup FileBackend * @since 1.19 */ @@ -23,7 +41,9 @@ abstract class FileOp { protected $state = self::STATE_NEW; // integer protected $failed = false; // boolean + protected $async = false; // boolean protected $useLatest = true; // boolean + protected $batchId; // string protected $sourceSha1; // string protected $destSameAsSource; // boolean @@ -33,15 +53,11 @@ abstract class FileOp { const STATE_CHECKED = 2; const STATE_ATTEMPTED = 3; - /* Timeout related parameters */ - const MAX_BATCH_SIZE = 1000; - const TIME_LIMIT_SEC = 300; // 5 minutes - /** * Build a new file operation transaction * - * @params $backend FileBackendStore - * @params $params Array + * @param $backend FileBackendStore + * @param $params Array * @throws MWException */ final public function __construct( FileBackendStore $backend, array $params ) { @@ -63,101 +79,28 @@ abstract class FileOp { } /** - * Allow stale data for file reads and existence checks + * Set the batch UUID this operation belongs to * + * @param $batchId string * @return void */ - final protected function allowStaleReads() { - $this->useLatest = false; + final public function setBatchId( $batchId ) { + $this->batchId = $batchId; } /** - * Attempt a series of file operations. - * Callers are responsible for handling file locking. - * - * $opts is an array of options, including: - * 'force' : Errors that would normally cause a rollback do not. - * The remaining operations are still attempted if any fail. - * 'allowStale' : Don't require the latest available data. - * This can increase performance for non-critical writes. - * This has no effect unless the 'force' flag is set. + * Whether to allow stale data for file reads and stat checks * - * The resulting Status will be "OK" unless: - * a) unexpected operation errors occurred (network partitions, disk full...) - * b) significant operation errors occured and 'force' was not set - * - * @param $performOps Array List of FileOp operations - * @param $opts Array Batch operation options - * @return Status - */ - final public static function attemptBatch( array $performOps, array $opts ) { - $status = Status::newGood(); - - $allowStale = !empty( $opts['allowStale'] ); - $ignoreErrors = !empty( $opts['force'] ); - - $n = count( $performOps ); - if ( $n > self::MAX_BATCH_SIZE ) { - $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE ); - return $status; - } - - $predicates = FileOp::newPredicates(); // account for previous op in prechecks - // Do pre-checks for each operation; abort on failure... - foreach ( $performOps as $index => $fileOp ) { - if ( $allowStale ) { - $fileOp->allowStaleReads(); // allow potentially stale reads - } - $subStatus = $fileOp->precheck( $predicates ); - $status->merge( $subStatus ); - if ( !$subStatus->isOK() ) { // operation failed? - $status->success[$index] = false; - ++$status->failCount; - if ( !$ignoreErrors ) { - return $status; // abort - } - } - } - - if ( $ignoreErrors ) { - # Treat all precheck() fatals as merely warnings - $status->setResult( true, $status->value ); - } - - // Restart PHP's execution timer and set the timeout to safe amount. - // This handles cases where the operations take a long time or where we are - // already running low on time left. The old timeout is restored afterwards. - # @TODO: re-enable this for when the number of batches is high. - #$scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC ); - - // Attempt each operation... - foreach ( $performOps as $index => $fileOp ) { - if ( $fileOp->failed() ) { - continue; // nothing to do - } - $subStatus = $fileOp->attempt(); - $status->merge( $subStatus ); - if ( $subStatus->isOK() ) { - $status->success[$index] = true; - ++$status->successCount; - } else { - $status->success[$index] = false; - ++$status->failCount; - // We can't continue (even with $ignoreErrors) as $predicates is wrong. - // Log the remaining ops as failed for recovery... - for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) { - $performOps[$i]->logFailure( 'attempt_aborted' ); - } - return $status; // bail out - } - } - - return $status; + * @param $allowStale bool + * @return void + */ + final public function allowStaleReads( $allowStale ) { + $this->useLatest = !$allowStale; } /** * Get the value of the parameter with the given name - * + * * @param $name string * @return mixed Returns null if the parameter is not set */ @@ -167,8 +110,8 @@ abstract class FileOp { /** * Check if this operation failed precheck() or attempt() - * - * @return bool + * + * @return bool */ final public function failed() { return $this->failed; @@ -177,13 +120,91 @@ abstract class FileOp { /** * Get a new empty predicates array for precheck() * - * @return Array + * @return Array */ final public static function newPredicates() { return array( 'exists' => array(), 'sha1' => array() ); } /** + * Get a new empty dependency tracking array for paths read/written to + * + * @return Array + */ + final public static function newDependencies() { + return array( 'read' => array(), 'write' => array() ); + } + + /** + * Update a dependency tracking array to account for this operation + * + * @param $deps Array Prior path reads/writes; format of FileOp::newPredicates() + * @return Array + */ + final public function applyDependencies( array $deps ) { + $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 ); + $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 ); + return $deps; + } + + /** + * Check if this operation changes files listed in $paths + * + * @param $paths Array Prior path reads/writes; format of FileOp::newPredicates() + * @return boolean + */ + final public function dependsOn( array $deps ) { + foreach ( $this->storagePathsChanged() as $path ) { + if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) { + return true; // "output" or "anti" dependency + } + } + foreach ( $this->storagePathsRead() as $path ) { + if ( isset( $deps['write'][$path] ) ) { + return true; // "flow" dependency + } + } + return false; + } + + /** + * Get the file journal entries for this file operation + * + * @param $oPredicates Array Pre-op info about files (format of FileOp::newPredicates) + * @param $nPredicates Array Post-op info about files (format of FileOp::newPredicates) + * @return Array + */ + final public function getJournalEntries( array $oPredicates, array $nPredicates ) { + $nullEntries = array(); + $updateEntries = array(); + $deleteEntries = array(); + $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() ); + foreach ( $pathsUsed as $path ) { + $nullEntries[] = array( // assertion for recovery + 'op' => 'null', + 'path' => $path, + 'newSha1' => $this->fileSha1( $path, $oPredicates ) + ); + } + foreach ( $this->storagePathsChanged() as $path ) { + if ( $nPredicates['sha1'][$path] === false ) { // deleted + $deleteEntries[] = array( + 'op' => 'delete', + 'path' => $path, + 'newSha1' => '' + ); + } else { // created/updated + $updateEntries[] = array( + 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create', + 'path' => $path, + 'newSha1' => $nPredicates['sha1'][$path] + ); + } + } + return array_merge( $nullEntries, $updateEntries, $deleteEntries ); + } + + /** * Check preconditions of the operation without writing anything * * @param $predicates Array @@ -202,7 +223,14 @@ abstract class FileOp { } /** - * Attempt the operation, backing up files as needed; this must be reversible + * @return Status + */ + protected function doPrecheck( array &$predicates ) { + return Status::newGood(); + } + + /** + * Attempt the operation * * @return Status */ @@ -222,8 +250,27 @@ abstract class FileOp { } /** + * @return Status + */ + protected function doAttempt() { + return Status::newGood(); + } + + /** + * Attempt the operation in the background + * + * @return Status + */ + final public function attemptAsync() { + $this->async = true; + $result = $this->attempt(); + $this->async = false; + return $result; + } + + /** * Get the file operation parameters - * + * * @return Array (required params list, optional params list) */ protected function allowedParams() { @@ -231,42 +278,54 @@ abstract class FileOp { } /** + * Adjust params to FileBackendStore internal file calls + * + * @param $params Array + * @return Array (required params list, optional params list) + */ + protected function setFlags( array $params ) { + return array( 'async' => $this->async ) + $params; + } + + /** * Get a list of storage paths read from for this operation * * @return Array */ - public function storagePathsRead() { - return array(); + final public function storagePathsRead() { + return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() ); } /** - * Get a list of storage paths written to for this operation - * + * @see FileOp::storagePathsRead() * @return Array */ - public function storagePathsChanged() { + protected function doStoragePathsRead() { return array(); } /** - * @return Status + * Get a list of storage paths written to for this operation + * + * @return Array */ - protected function doPrecheck( array &$predicates ) { - return Status::newGood(); + final public function storagePathsChanged() { + return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() ); } /** - * @return Status + * @see FileOp::storagePathsChanged() + * @return Array */ - protected function doAttempt() { - return Status::newGood(); + protected function doStoragePathsChanged() { + return array(); } /** * Check for errors with regards to the destination file already existing. * This also updates the destSameAsSource and sourceSha1 member variables. * A bad status will be returned if there is no chance it can be overwritten. - * + * * @param $predicates Array * @return Status */ @@ -305,7 +364,7 @@ abstract class FileOp { * precheckDestExistence() helper function to get the source file SHA-1. * Subclasses should overwride this iff the source is not in storage. * - * @return string|false Returns false on failure + * @return string|bool Returns false on failure */ protected function getSourceSha1Base36() { return null; // N/A @@ -313,10 +372,10 @@ abstract class FileOp { /** * Check if a file will exist in storage when this operation is attempted - * + * * @param $source string Storage path * @param $predicates Array - * @return bool + * @return bool */ final protected function fileExists( $source, array $predicates ) { if ( isset( $predicates['exists'][$source] ) ) { @@ -329,10 +388,10 @@ abstract class FileOp { /** * Get the SHA-1 of a file in storage when this operation is attempted - * + * * @param $source string Storage path * @param $predicates Array - * @return string|false + * @return string|bool False on failure */ final protected function fileSha1( $source, array $predicates ) { if ( isset( $predicates['sha1'][$source] ) ) { @@ -344,17 +403,26 @@ abstract class FileOp { } /** + * Get the backend this operation is for + * + * @return FileBackendStore + */ + public function getBackend() { + return $this->backend; + } + + /** * Log a file operation failure and preserve any temp files - * + * * @param $action string * @return void */ - final protected function logFailure( $action ) { + final public function logFailure( $action ) { $params = $this->params; $params['failedAction'] = $action; try { - wfDebugLog( 'FileOperation', - get_class( $this ) . ' failed:' . serialize( $params ) ); + wfDebugLog( 'FileOperation', get_class( $this ) . + " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) ); } catch ( Exception $e ) { // bad config? debug log error? } @@ -362,75 +430,22 @@ abstract class FileOp { } /** - * FileOp helper class to expand PHP execution time for a function. - * On construction, set_time_limit() is called and set to $seconds. - * When the object goes out of scope, the timer is restarted, with - * the original time limit minus the time the object existed. + * Store a file into the backend from a file on the file system. + * Parameters for this operation are outlined in FileBackend::doOperations(). */ -class FileOpScopedPHPTimeout { - protected $startTime; // float; seconds - protected $oldTimeout; // integer; seconds - - protected static $stackDepth = 0; // integer - protected static $totalCalls = 0; // integer - protected static $totalElapsed = 0; // float; seconds - - /* Prevent callers in infinite loops from running forever */ - const MAX_TOTAL_CALLS = 1000000; - const MAX_TOTAL_TIME = 300; // seconds - +class StoreFileOp extends FileOp { /** - * @param $seconds integer + * @return array */ - public function __construct( $seconds ) { - if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0 - if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) { - trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." ); - } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) { - trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." ); - } elseif ( self::$stackDepth > 0 ) { // recursion guard - trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." ); - } else { - $this->oldTimeout = ini_set( 'max_execution_time', $seconds ); - $this->startTime = microtime( true ); - ++self::$stackDepth; - ++self::$totalCalls; // proof against < 1us scopes - } - } + protected function allowedParams() { + return array( array( 'src', 'dst' ), + array( 'overwrite', 'overwriteSame', 'disposition' ) ); } /** - * Restore the original timeout. - * This does not account for the timer value on __construct(). + * @param $predicates array + * @return Status */ - public function __destruct() { - if ( $this->oldTimeout ) { - $elapsed = microtime( true ) - $this->startTime; - // Note: a limit of 0 is treated as "forever" - set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) ); - // If each scoped timeout is for less than one second, we end up - // restoring the original timeout without any decrease in value. - // Thus web scripts in an infinite loop can run forever unless we - // take some measures to prevent this. Track total time and calls. - self::$totalElapsed += $elapsed; - --self::$stackDepth; - } - } -} - -/** - * Store a file into the backend from a file on the file system. - * Parameters similar to FileBackendStore::storeInternal(), which include: - * src : source path on file system - * dst : destination storage path - * overwrite : do nothing and pass if an identical file exists at destination - * overwriteSame : override any existing file at destination - */ -class StoreFileOp extends FileOp { - protected function allowedParams() { - return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); - } - protected function doPrecheck( array &$predicates ) { $status = Status::newGood(); // Check if the source file exists on the file system @@ -439,10 +454,13 @@ class StoreFileOp extends FileOp { return $status; // Check if the source file is too big } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) { + $status->fatal( 'backend-fail-maxsize', + $this->params['dst'], $this->backend->maxFileSizeInternal() ); $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); return $status; // Check if a file can be placed at the destination } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] ); return $status; } @@ -456,15 +474,20 @@ class StoreFileOp extends FileOp { return $status; // safe to call attempt() } + /** + * @return Status + */ protected function doAttempt() { - $status = Status::newGood(); // Store the file at the destination if ( !$this->destSameAsSource ) { - $status->merge( $this->backend->storeInternal( $this->params ) ); + return $this->backend->storeInternal( $this->setFlags( $this->params ) ); } - return $status; + return Status::newGood(); } + /** + * @return bool|string + */ protected function getSourceSha1Base36() { wfSuppressWarnings(); $hash = sha1_file( $this->params['src'] ); @@ -475,32 +498,32 @@ class StoreFileOp extends FileOp { return $hash; } - public function storagePathsChanged() { + protected function doStoragePathsChanged() { return array( $this->params['dst'] ); } } /** * Create a file in the backend with the given content. - * Parameters similar to FileBackendStore::createInternal(), which include: - * content : the raw file contents - * dst : destination storage path - * overwrite : do nothing and pass if an identical file exists at destination - * overwriteSame : override any existing file at destination + * Parameters for this operation are outlined in FileBackend::doOperations(). */ class CreateFileOp extends FileOp { protected function allowedParams() { - return array( array( 'content', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); + return array( array( 'content', 'dst' ), + array( 'overwrite', 'overwriteSame', 'disposition' ) ); } protected function doPrecheck( array &$predicates ) { $status = Status::newGood(); // Check if the source data is too big if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) { + $status->fatal( 'backend-fail-maxsize', + $this->params['dst'], $this->backend->maxFileSizeInternal() ); $status->fatal( 'backend-fail-create', $this->params['dst'] ); return $status; // Check if a file can be placed at the destination } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); $status->fatal( 'backend-fail-create', $this->params['dst'] ); return $status; } @@ -514,37 +537,49 @@ class CreateFileOp extends FileOp { return $status; // safe to call attempt() } + /** + * @return Status + */ protected function doAttempt() { - $status = Status::newGood(); - // Create the file at the destination if ( !$this->destSameAsSource ) { - $status->merge( $this->backend->createInternal( $this->params ) ); + // Create the file at the destination + return $this->backend->createInternal( $this->setFlags( $this->params ) ); } - return $status; + return Status::newGood(); } + /** + * @return bool|String + */ protected function getSourceSha1Base36() { return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); } - public function storagePathsChanged() { + /** + * @return array + */ + protected function doStoragePathsChanged() { return array( $this->params['dst'] ); } } /** * Copy a file from one storage path to another in the backend. - * Parameters similar to FileBackendStore::copyInternal(), which include: - * src : source storage path - * dst : destination storage path - * overwrite : do nothing and pass if an identical file exists at destination - * overwriteSame : override any existing file at destination + * Parameters for this operation are outlined in FileBackend::doOperations(). */ class CopyFileOp extends FileOp { + /** + * @return array + */ protected function allowedParams() { - return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); + return array( array( 'src', 'dst' ), + array( 'overwrite', 'overwriteSame', 'disposition' ) ); } + /** + * @param $predicates array + * @return Status + */ protected function doPrecheck( array &$predicates ) { $status = Status::newGood(); // Check if the source file exists @@ -553,6 +588,7 @@ class CopyFileOp extends FileOp { return $status; // Check if a file can be placed at the destination } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] ); return $status; } @@ -566,40 +602,52 @@ class CopyFileOp extends FileOp { return $status; // safe to call attempt() } + /** + * @return Status + */ protected function doAttempt() { - $status = Status::newGood(); // Do nothing if the src/dst paths are the same if ( $this->params['src'] !== $this->params['dst'] ) { // Copy the file into the destination if ( !$this->destSameAsSource ) { - $status->merge( $this->backend->copyInternal( $this->params ) ); + return $this->backend->copyInternal( $this->setFlags( $this->params ) ); } } - return $status; + return Status::newGood(); } - public function storagePathsRead() { + /** + * @return array + */ + protected function doStoragePathsRead() { return array( $this->params['src'] ); } - public function storagePathsChanged() { + /** + * @return array + */ + protected function doStoragePathsChanged() { return array( $this->params['dst'] ); } } /** * Move a file from one storage path to another in the backend. - * Parameters similar to FileBackendStore::moveInternal(), which include: - * src : source storage path - * dst : destination storage path - * overwrite : do nothing and pass if an identical file exists at destination - * overwriteSame : override any existing file at destination + * Parameters for this operation are outlined in FileBackend::doOperations(). */ class MoveFileOp extends FileOp { + /** + * @return array + */ protected function allowedParams() { - return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) ); + return array( array( 'src', 'dst' ), + array( 'overwrite', 'overwriteSame', 'disposition' ) ); } + /** + * @param $predicates array + * @return Status + */ protected function doPrecheck( array &$predicates ) { $status = Status::newGood(); // Check if the source file exists @@ -608,6 +656,7 @@ class MoveFileOp extends FileOp { return $status; // Check if a file can be placed at the destination } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) { + $status->fatal( 'backend-fail-usable', $this->params['dst'] ); $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] ); return $status; } @@ -623,44 +672,57 @@ class MoveFileOp extends FileOp { return $status; // safe to call attempt() } + /** + * @return Status + */ protected function doAttempt() { - $status = Status::newGood(); // Do nothing if the src/dst paths are the same if ( $this->params['src'] !== $this->params['dst'] ) { if ( !$this->destSameAsSource ) { // Move the file into the destination - $status->merge( $this->backend->moveInternal( $this->params ) ); + return $this->backend->moveInternal( $this->setFlags( $this->params ) ); } else { // Just delete source as the destination needs no changes $params = array( 'src' => $this->params['src'] ); - $status->merge( $this->backend->deleteInternal( $params ) ); + return $this->backend->deleteInternal( $this->setFlags( $params ) ); } } - return $status; + return Status::newGood(); } - public function storagePathsRead() { + /** + * @return array + */ + protected function doStoragePathsRead() { return array( $this->params['src'] ); } - public function storagePathsChanged() { - return array( $this->params['dst'] ); + /** + * @return array + */ + protected function doStoragePathsChanged() { + return array( $this->params['src'], $this->params['dst'] ); } } /** * Delete a file at the given storage path from the backend. - * Parameters similar to FileBackendStore::deleteInternal(), which include: - * src : source storage path - * ignoreMissingSource : don't return an error if the file does not exist + * Parameters for this operation are outlined in FileBackend::doOperations(). */ class DeleteFileOp extends FileOp { + /** + * @return array + */ protected function allowedParams() { return array( array( 'src' ), array( 'ignoreMissingSource' ) ); } protected $needsDelete = true; + /** + * @param array $predicates + * @return Status + */ protected function doPrecheck( array &$predicates ) { $status = Status::newGood(); // Check if the source file exists @@ -677,16 +739,21 @@ class DeleteFileOp extends FileOp { return $status; // safe to call attempt() } + /** + * @return Status + */ protected function doAttempt() { - $status = Status::newGood(); if ( $this->needsDelete ) { // Delete the source file - $status->merge( $this->backend->deleteInternal( $this->params ) ); + return $this->backend->deleteInternal( $this->setFlags( $this->params ) ); } - return $status; + return Status::newGood(); } - public function storagePathsChanged() { + /** + * @return array + */ + protected function doStoragePathsChanged() { return array( $this->params['src'] ); } } diff --git a/includes/filebackend/FileOpBatch.php b/includes/filebackend/FileOpBatch.php new file mode 100644 index 00000000..33558725 --- /dev/null +++ b/includes/filebackend/FileOpBatch.php @@ -0,0 +1,240 @@ +<?php +/** + * Helper class for representing batch file operations. + * + * 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 FileBackend + * @author Aaron Schulz + */ + +/** + * Helper class for representing batch file operations. + * Do not use this class from places outside FileBackend. + * + * Methods should avoid throwing exceptions at all costs. + * + * @ingroup FileBackend + * @since 1.20 + */ +class FileOpBatch { + /* Timeout related parameters */ + const MAX_BATCH_SIZE = 1000; // integer + + /** + * Attempt to perform a series of file operations. + * Callers are responsible for handling file locking. + * + * $opts is an array of options, including: + * - force : Errors that would normally cause a rollback do not. + * The remaining operations are still attempted if any fail. + * - allowStale : Don't require the latest available data. + * This can increase performance for non-critical writes. + * This has no effect unless the 'force' flag is set. + * - nonJournaled : Don't log this operation batch in the file journal. + * - concurrency : Try to do this many operations in parallel when possible. + * + * The resulting Status will be "OK" unless: + * - a) unexpected operation errors occurred (network partitions, disk full...) + * - b) significant operation errors occurred and 'force' was not set + * + * @param $performOps Array List of FileOp operations + * @param $opts Array Batch operation options + * @param $journal FileJournal Journal to log operations to + * @return Status + */ + public static function attempt( array $performOps, array $opts, FileJournal $journal ) { + wfProfileIn( __METHOD__ ); + $status = Status::newGood(); + + $n = count( $performOps ); + if ( $n > self::MAX_BATCH_SIZE ) { + $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE ); + wfProfileOut( __METHOD__ ); + return $status; + } + + $batchId = $journal->getTimestampedUUID(); + $allowStale = !empty( $opts['allowStale'] ); + $ignoreErrors = !empty( $opts['force'] ); + $journaled = empty( $opts['nonJournaled'] ); + $maxConcurrency = isset( $opts['concurrency'] ) ? $opts['concurrency'] : 1; + + $entries = array(); // file journal entry list + $predicates = FileOp::newPredicates(); // account for previous ops in prechecks + $curBatch = array(); // concurrent FileOp sub-batch accumulation + $curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch + $pPerformOps = array(); // ordered list of concurrent FileOp sub-batches + $lastBackend = null; // last op backend name + // Do pre-checks for each operation; abort on failure... + foreach ( $performOps as $index => $fileOp ) { + $backendName = $fileOp->getBackend()->getName(); + $fileOp->setBatchId( $batchId ); // transaction ID + $fileOp->allowStaleReads( $allowStale ); // consistency level + // Decide if this op can be done concurrently within this sub-batch + // or if a new concurrent sub-batch must be started after this one... + if ( $fileOp->dependsOn( $curBatchDeps ) + || count( $curBatch ) >= $maxConcurrency + || ( $backendName !== $lastBackend && count( $curBatch ) ) + ) { + $pPerformOps[] = $curBatch; // push this batch + $curBatch = array(); // start a new sub-batch + $curBatchDeps = FileOp::newDependencies(); + } + $lastBackend = $backendName; + $curBatch[$index] = $fileOp; // keep index + // Update list of affected paths in this batch + $curBatchDeps = $fileOp->applyDependencies( $curBatchDeps ); + // Simulate performing the operation... + $oldPredicates = $predicates; + $subStatus = $fileOp->precheck( $predicates ); // updates $predicates + $status->merge( $subStatus ); + if ( $subStatus->isOK() ) { + if ( $journaled ) { // journal log entries + $entries = array_merge( $entries, + $fileOp->getJournalEntries( $oldPredicates, $predicates ) ); + } + } else { // operation failed? + $status->success[$index] = false; + ++$status->failCount; + if ( !$ignoreErrors ) { + wfProfileOut( __METHOD__ ); + return $status; // abort + } + } + } + // Push the last sub-batch + if ( count( $curBatch ) ) { + $pPerformOps[] = $curBatch; + } + + // Log the operations in the file journal... + if ( count( $entries ) ) { + $subStatus = $journal->logChangeBatch( $entries, $batchId ); + if ( !$subStatus->isOK() ) { + wfProfileOut( __METHOD__ ); + return $subStatus; // abort + } + } + + if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings + $status->setResult( true, $status->value ); + } + + // Attempt each operation (in parallel if allowed and possible)... + if ( count( $pPerformOps ) < count( $performOps ) ) { + self::runBatchParallel( $pPerformOps, $status ); + } else { + self::runBatchSeries( $performOps, $status ); + } + + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * Attempt a list of file operations in series. + * This will abort remaining ops on failure. + * + * @param $performOps Array + * @param $status Status + * @return bool Success + */ + protected static function runBatchSeries( array $performOps, Status $status ) { + foreach ( $performOps as $index => $fileOp ) { + if ( $fileOp->failed() ) { + continue; // nothing to do + } + $subStatus = $fileOp->attempt(); + $status->merge( $subStatus ); + if ( $subStatus->isOK() ) { + $status->success[$index] = true; + ++$status->successCount; + } else { + $status->success[$index] = false; + ++$status->failCount; + // We can't continue (even with $ignoreErrors) as $predicates is wrong. + // Log the remaining ops as failed for recovery... + for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) { + $performOps[$i]->logFailure( 'attempt_aborted' ); + } + return false; // bail out + } + } + return true; + } + + /** + * Attempt a list of file operations sub-batches in series. + * + * The operations *in* each sub-batch will be done in parallel. + * The caller is responsible for making sure the operations + * within any given sub-batch do not depend on each other. + * This will abort remaining ops on failure. + * + * @param $pPerformOps Array + * @param $status Status + * @return bool Success + */ + protected static function runBatchParallel( array $pPerformOps, Status $status ) { + $aborted = false; + foreach ( $pPerformOps as $performOpsBatch ) { + if ( $aborted ) { // check batch op abort flag... + // We can't continue (even with $ignoreErrors) as $predicates is wrong. + // Log the remaining ops as failed for recovery... + foreach ( $performOpsBatch as $i => $fileOp ) { + $performOpsBatch[$i]->logFailure( 'attempt_aborted' ); + } + continue; + } + $statuses = array(); + $opHandles = array(); + // Get the backend; all sub-batch ops belong to a single backend + $backend = reset( $performOpsBatch )->getBackend(); + // If attemptAsync() returns synchronously, it was either an + // error Status or the backend just doesn't support async ops. + foreach ( $performOpsBatch as $i => $fileOp ) { + if ( !$fileOp->failed() ) { // failed => already has Status + $subStatus = $fileOp->attemptAsync(); + if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { + $opHandles[$i] = $subStatus->value; // deferred + } else { + $statuses[$i] = $subStatus; // done already + } + } + } + // Try to do all the operations concurrently... + $statuses = $statuses + $backend->executeOpHandlesInternal( $opHandles ); + // Marshall and merge all the responses (blocking)... + foreach ( $performOpsBatch as $i => $fileOp ) { + if ( !$fileOp->failed() ) { // failed => already has Status + $subStatus = $statuses[$i]; + $status->merge( $subStatus ); + if ( $subStatus->isOK() ) { + $status->success[$i] = true; + ++$status->successCount; + } else { + $status->success[$i] = false; + ++$status->failCount; + $aborted = true; // set abort flag; we can't continue + } + } + } + } + return $status; + } +} diff --git a/includes/filebackend/SwiftFileBackend.php b/includes/filebackend/SwiftFileBackend.php new file mode 100644 index 00000000..b6f0aa60 --- /dev/null +++ b/includes/filebackend/SwiftFileBackend.php @@ -0,0 +1,1544 @@ +<?php +/** + * OpenStack Swift based file backend. + * + * 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 FileBackend + * @author Russ Nelson + * @author Aaron Schulz + */ + +/** + * @brief Class for an OpenStack Swift based file backend. + * + * This requires the SwiftCloudFiles MediaWiki extension, which includes + * the php-cloudfiles library (https://github.com/rackspace/php-cloudfiles). + * php-cloudfiles requires the curl, fileinfo, and mb_string PHP extensions. + * + * Status messages should avoid mentioning the Swift account name. + * Likewise, error suppression should be used to avoid path disclosure. + * + * @ingroup FileBackend + * @since 1.19 + */ +class SwiftFileBackend extends FileBackendStore { + /** @var CF_Authentication */ + protected $auth; // Swift authentication handler + protected $authTTL; // integer seconds + protected $swiftAnonUser; // string; username to handle unauthenticated requests + protected $swiftUseCDN; // boolean; whether CloudFiles CDN is enabled + protected $swiftCDNExpiry; // integer; how long to cache things in the CDN + protected $swiftCDNPurgable; // boolean; whether object CDN purging is enabled + + /** @var CF_Connection */ + protected $conn; // Swift connection handle + protected $sessionStarted = 0; // integer UNIX timestamp + + /** @var CloudFilesException */ + protected $connException; + protected $connErrorTime = 0; // UNIX timestamp + + /** @var BagOStuff */ + protected $srvCache; + + /** @var ProcessCacheLRU */ + protected $connContainerCache; // container object cache + + /** + * @see FileBackendStore::__construct() + * Additional $config params include: + * - swiftAuthUrl : Swift authentication server URL + * - swiftUser : Swift user used by MediaWiki (account:username) + * - swiftKey : Swift authentication key for the above user + * - swiftAuthTTL : Swift authentication TTL (seconds) + * - swiftAnonUser : Swift user used for end-user requests (account:username). + * If set, then views of public containers are assumed to go + * through this user. If not set, then public containers are + * accessible to unauthenticated requests via ".r:*" in the ACL. + * - swiftUseCDN : Whether a Cloud Files Content Delivery Network is set up + * - swiftCDNExpiry : How long (in seconds) to store content in the CDN. + * If files may likely change, this should probably not exceed + * a few days. For example, deletions may take this long to apply. + * If object purging is enabled, however, this is not an issue. + * - swiftCDNPurgable : Whether object purge requests are allowed by the CDN. + * - shardViaHashLevels : Map of container names to sharding config with: + * - base : base of hash characters, 16 or 36 + * - levels : the number of hash levels (and digits) + * - repeat : hash subdirectories are prefixed with all the + * parent hash directory names (e.g. "a/ab/abc") + * - cacheAuthInfo : Whether to cache authentication tokens in APC, XCache, ect. + * If those are not available, then the main cache will be used. + * This is probably insecure in shared hosting environments. + */ + public function __construct( array $config ) { + parent::__construct( $config ); + if ( !MWInit::classExists( 'CF_Constants' ) ) { + throw new MWException( 'SwiftCloudFiles extension not installed.' ); + } + // Required settings + $this->auth = new CF_Authentication( + $config['swiftUser'], + $config['swiftKey'], + null, // account; unused + $config['swiftAuthUrl'] + ); + // Optional settings + $this->authTTL = isset( $config['swiftAuthTTL'] ) + ? $config['swiftAuthTTL'] + : 5 * 60; // some sane number + $this->swiftAnonUser = isset( $config['swiftAnonUser'] ) + ? $config['swiftAnonUser'] + : ''; + $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] ) + ? $config['shardViaHashLevels'] + : ''; + $this->swiftUseCDN = isset( $config['swiftUseCDN'] ) + ? $config['swiftUseCDN'] + : false; + $this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] ) + ? $config['swiftCDNExpiry'] + : 12*3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org) + $this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] ) + ? $config['swiftCDNPurgable'] + : true; + // Cache container information to mask latency + $this->memCache = wfGetMainCache(); + // Process cache for container info + $this->connContainerCache = new ProcessCacheLRU( 300 ); + // Cache auth token information to avoid RTTs + if ( !empty( $config['cacheAuthInfo'] ) ) { + if ( php_sapi_name() === 'cli' ) { + $this->srvCache = wfGetMainCache(); // preferrably memcached + } else { + try { // look for APC, XCache, WinCache, ect... + $this->srvCache = ObjectCache::newAccelerator( array() ); + } catch ( Exception $e ) {} + } + } + $this->srvCache = $this->srvCache ? $this->srvCache : new EmptyBagOStuff(); + } + + /** + * @see FileBackendStore::resolveContainerPath() + * @return null + */ + protected function resolveContainerPath( $container, $relStoragePath ) { + if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) { // mb_string required by CF + return null; // not UTF-8, makes it hard to use CF and the swift HTTP API + } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) { + return null; // too long for Swift + } + return $relStoragePath; + } + + /** + * @see FileBackendStore::isPathUsableInternal() + * @return bool + */ + public function isPathUsableInternal( $storagePath ) { + list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath ); + if ( $rel === null ) { + return false; // invalid + } + + try { + $this->getContainer( $container ); + return true; // container exists + } catch ( NoSuchContainerException $e ) { + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, null, __METHOD__, array( 'path' => $storagePath ) ); + } + + return false; + } + + /** + * @param $disposition string Content-Disposition header value + * @return string Truncated Content-Disposition header value to meet Swift limits + */ + protected function truncDisp( $disposition ) { + $res = ''; + foreach ( explode( ';', $disposition ) as $part ) { + $part = trim( $part ); + $new = ( $res === '' ) ? $part : "{$res};{$part}"; + if ( strlen( $new ) <= 255 ) { + $res = $new; + } else { + break; // too long; sigh + } + } + return $res; + } + + /** + * @see FileBackendStore::doCreateInternal() + * @return Status + */ + protected function doCreateInternal( array $params ) { + $status = Status::newGood(); + + list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); + if ( $dstRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + // (a) Check the destination container and object + try { + $dContObj = $this->getContainer( $dstCont ); + if ( empty( $params['overwrite'] ) && + $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) ) + { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } catch ( NoSuchContainerException $e ) { + $status->fatal( 'backend-fail-create', $params['dst'] ); + return $status; + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + // (b) Get a SHA-1 hash of the object + $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 ); + + // (c) Actually create the object + try { + // Create a fresh CF_Object with no fields preloaded. + // We don't want to preserve headers, metadata, and such. + $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD + // Note: metadata keys stored as [Upper case char][[Lower case char]...] + $obj->metadata = array( 'Sha1base36' => $sha1Hash ); + // Manually set the ETag (https://github.com/rackspace/php-cloudfiles/issues/59). + // The MD5 here will be checked within Swift against its own MD5. + $obj->set_etag( md5( $params['content'] ) ); + // Use the same content type as StreamFile for security + $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] ); + if ( !strlen( $obj->content_type ) ) { // special case + $obj->content_type = 'unknown/unknown'; + } + // Set the Content-Disposition header if requested + if ( isset( $params['disposition'] ) ) { + $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); + } + if ( !empty( $params['async'] ) ) { // deferred + $op = $obj->write_async( $params['content'] ); + $status->value = new SwiftFileOpHandle( $this, $params, 'Create', $op ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $status->value->affectedObjects[] = $obj; + } + } else { // actually write the object in Swift + $obj->write( $params['content'] ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->purgeCDNCache( array( $obj ) ); + } + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( BadContentTypeException $e ) { + $status->fatal( 'backend-fail-contenttype', $params['dst'] ); + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see SwiftFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseCreate( CF_Async_Op $cfOp, Status $status, array $params ) { + try { + $cfOp->getLastResponse(); + } catch ( BadContentTypeException $e ) { + $status->fatal( 'backend-fail-contenttype', $params['dst'] ); + } + } + + /** + * @see FileBackendStore::doStoreInternal() + * @return Status + */ + protected function doStoreInternal( array $params ) { + $status = Status::newGood(); + + list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); + if ( $dstRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + // (a) Check the destination container and object + try { + $dContObj = $this->getContainer( $dstCont ); + if ( empty( $params['overwrite'] ) && + $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) ) + { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } catch ( NoSuchContainerException $e ) { + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + return $status; + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + // (b) Get a SHA-1 hash of the object + $sha1Hash = sha1_file( $params['src'] ); + if ( $sha1Hash === false ) { // source doesn't exist? + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + return $status; + } + $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 ); + + // (c) Actually store the object + try { + // Create a fresh CF_Object with no fields preloaded. + // We don't want to preserve headers, metadata, and such. + $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD + // Note: metadata keys stored as [Upper case char][[Lower case char]...] + $obj->metadata = array( 'Sha1base36' => $sha1Hash ); + // The MD5 here will be checked within Swift against its own MD5. + $obj->set_etag( md5_file( $params['src'] ) ); + // Use the same content type as StreamFile for security + $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] ); + if ( !strlen( $obj->content_type ) ) { // special case + $obj->content_type = 'unknown/unknown'; + } + // Set the Content-Disposition header if requested + if ( isset( $params['disposition'] ) ) { + $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); + } + if ( !empty( $params['async'] ) ) { // deferred + wfSuppressWarnings(); + $fp = fopen( $params['src'], 'rb' ); + wfRestoreWarnings(); + if ( !$fp ) { + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + } else { + $op = $obj->write_async( $fp, filesize( $params['src'] ), true ); + $status->value = new SwiftFileOpHandle( $this, $params, 'Store', $op ); + $status->value->resourcesToClose[] = $fp; + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $status->value->affectedObjects[] = $obj; + } + } + } else { // actually write the object in Swift + $obj->load_from_filename( $params['src'], true ); // calls $obj->write() + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->purgeCDNCache( array( $obj ) ); + } + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( BadContentTypeException $e ) { + $status->fatal( 'backend-fail-contenttype', $params['dst'] ); + } catch ( IOException $e ) { + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see SwiftFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseStore( CF_Async_Op $cfOp, Status $status, array $params ) { + try { + $cfOp->getLastResponse(); + } catch ( BadContentTypeException $e ) { + $status->fatal( 'backend-fail-contenttype', $params['dst'] ); + } catch ( IOException $e ) { + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + } + } + + /** + * @see FileBackendStore::doCopyInternal() + * @return Status + */ + protected function doCopyInternal( array $params ) { + $status = Status::newGood(); + + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); + if ( $srcRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['src'] ); + return $status; + } + + list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); + if ( $dstRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + // (a) Check the source/destination containers and destination object + try { + $sContObj = $this->getContainer( $srcCont ); + $dContObj = $this->getContainer( $dstCont ); + if ( empty( $params['overwrite'] ) && + $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) ) + { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } catch ( NoSuchContainerException $e ) { + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + return $status; + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + // (b) Actually copy the file to the destination + try { + $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD + $hdrs = array(); // source file headers to override with new values + if ( isset( $params['disposition'] ) ) { + $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); + } + if ( !empty( $params['async'] ) ) { // deferred + $op = $sContObj->copy_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs ); + $status->value = new SwiftFileOpHandle( $this, $params, 'Copy', $op ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $status->value->affectedObjects[] = $dstObj; + } + } else { // actually write the object in Swift + $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->purgeCDNCache( array( $dstObj ) ); + } + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( NoSuchObjectException $e ) { // source object does not exist + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see SwiftFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseCopy( CF_Async_Op $cfOp, Status $status, array $params ) { + try { + $cfOp->getLastResponse(); + } catch ( NoSuchObjectException $e ) { // source object does not exist + $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); + } + } + + /** + * @see FileBackendStore::doMoveInternal() + * @return Status + */ + protected function doMoveInternal( array $params ) { + $status = Status::newGood(); + + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); + if ( $srcRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['src'] ); + return $status; + } + + list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); + if ( $dstRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); + return $status; + } + + // (a) Check the source/destination containers and destination object + try { + $sContObj = $this->getContainer( $srcCont ); + $dContObj = $this->getContainer( $dstCont ); + if ( empty( $params['overwrite'] ) && + $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) ) + { + $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); + return $status; + } + } catch ( NoSuchContainerException $e ) { + $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); + return $status; + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + // (b) Actually move the file to the destination + try { + $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD + $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD + $hdrs = array(); // source file headers to override with new values + if ( isset( $params['disposition'] ) ) { + $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); + } + if ( !empty( $params['async'] ) ) { // deferred + $op = $sContObj->move_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs ); + $status->value = new SwiftFileOpHandle( $this, $params, 'Move', $op ); + $status->value->affectedObjects[] = $srcObj; + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $status->value->affectedObjects[] = $dstObj; + } + } else { // actually write the object in Swift + $sContObj->move_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs ); + $this->purgeCDNCache( array( $srcObj ) ); + if ( !empty( $params['overwrite'] ) ) { // file possibly mutated + $this->purgeCDNCache( array( $dstObj ) ); + } + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( NoSuchObjectException $e ) { // source object does not exist + $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see SwiftFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseMove( CF_Async_Op $cfOp, Status $status, array $params ) { + try { + $cfOp->getLastResponse(); + } catch ( NoSuchObjectException $e ) { // source object does not exist + $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); + } + } + + /** + * @see FileBackendStore::doDeleteInternal() + * @return Status + */ + protected function doDeleteInternal( array $params ) { + $status = Status::newGood(); + + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); + if ( $srcRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['src'] ); + return $status; + } + + try { + $sContObj = $this->getContainer( $srcCont ); + $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD + if ( !empty( $params['async'] ) ) { // deferred + $op = $sContObj->delete_object_async( $srcRel ); + $status->value = new SwiftFileOpHandle( $this, $params, 'Delete', $op ); + $status->value->affectedObjects[] = $srcObj; + } else { // actually write the object in Swift + $sContObj->delete_object( $srcRel ); + $this->purgeCDNCache( array( $srcObj ) ); + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( NoSuchContainerException $e ) { + $status->fatal( 'backend-fail-delete', $params['src'] ); + } catch ( NoSuchObjectException $e ) { + if ( empty( $params['ignoreMissingSource'] ) ) { + $status->fatal( 'backend-fail-delete', $params['src'] ); + } + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see SwiftFileBackend::doExecuteOpHandlesInternal() + */ + protected function _getResponseDelete( CF_Async_Op $cfOp, Status $status, array $params ) { + try { + $cfOp->getLastResponse(); + } catch ( NoSuchContainerException $e ) { + $status->fatal( 'backend-fail-delete', $params['src'] ); + } catch ( NoSuchObjectException $e ) { + if ( empty( $params['ignoreMissingSource'] ) ) { + $status->fatal( 'backend-fail-delete', $params['src'] ); + } + } + } + + /** + * @see FileBackendStore::doPrepareInternal() + * @return Status + */ + protected function doPrepareInternal( $fullCont, $dir, array $params ) { + $status = Status::newGood(); + + // (a) Check if container already exists + try { + $contObj = $this->getContainer( $fullCont ); + // NoSuchContainerException not thrown: container must exist + return $status; // already exists + } catch ( NoSuchContainerException $e ) { + // NoSuchContainerException thrown: container does not exist + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + // (b) Create container as needed + try { + $contObj = $this->createContainer( $fullCont ); + if ( !empty( $params['noAccess'] ) ) { + // Make container private to end-users... + $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) ); + } else { + // Make container public to end-users... + $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) ); + } + if ( $this->swiftUseCDN ) { // Rackspace style CDN + $contObj->make_public( $this->swiftCDNExpiry ); + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + return $status; + } + + /** + * @see FileBackendStore::doSecureInternal() + * @return Status + */ + protected function doSecureInternal( $fullCont, $dir, array $params ) { + $status = Status::newGood(); + if ( empty( $params['noAccess'] ) ) { + return $status; // nothing to do + } + + // Restrict container from end-users... + try { + // doPrepareInternal() should have been called, + // so the Swift container should already exist... + $contObj = $this->getContainer( $fullCont ); // normally a cache hit + // NoSuchContainerException not thrown: container must exist + + // Make container private to end-users... + $status->merge( $this->setContainerAccess( + $contObj, + array( $this->auth->username ), // read + array( $this->auth->username ) // write + ) ); + if ( $this->swiftUseCDN && $contObj->is_public() ) { // Rackspace style CDN + $contObj->make_private(); + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see FileBackendStore::doPublishInternal() + * @return Status + */ + protected function doPublishInternal( $fullCont, $dir, array $params ) { + $status = Status::newGood(); + + // Unrestrict container from end-users... + try { + // doPrepareInternal() should have been called, + // so the Swift container should already exist... + $contObj = $this->getContainer( $fullCont ); // normally a cache hit + // NoSuchContainerException not thrown: container must exist + + // Make container public to end-users... + if ( $this->swiftAnonUser != '' ) { + $status->merge( $this->setContainerAccess( + $contObj, + array( $this->auth->username, $this->swiftAnonUser ), // read + array( $this->auth->username, $this->swiftAnonUser ) // write + ) ); + } else { + $status->merge( $this->setContainerAccess( + $contObj, + array( $this->auth->username, '.r:*' ), // read + array( $this->auth->username ) // write + ) ); + } + if ( $this->swiftUseCDN && !$contObj->is_public() ) { // Rackspace style CDN + $contObj->make_public(); + } + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see FileBackendStore::doCleanInternal() + * @return Status + */ + protected function doCleanInternal( $fullCont, $dir, array $params ) { + $status = Status::newGood(); + + // Only containers themselves can be removed, all else is virtual + if ( $dir != '' ) { + return $status; // nothing to do + } + + // (a) Check the container + try { + $contObj = $this->getContainer( $fullCont, true ); + } catch ( NoSuchContainerException $e ) { + return $status; // ok, nothing to do + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + // (b) Delete the container if empty + if ( $contObj->object_count == 0 ) { + try { + $this->deleteContainer( $fullCont ); + } catch ( NoSuchContainerException $e ) { + return $status; // race? + } catch ( NonEmptyContainerException $e ) { + return $status; // race? consistency delay? + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + } + + return $status; + } + + /** + * @see FileBackendStore::doFileExists() + * @return array|bool|null + */ + protected function doGetFileStat( array $params ) { + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); + if ( $srcRel === null ) { + return false; // invalid storage path + } + + $stat = false; + try { + $contObj = $this->getContainer( $srcCont ); + $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) ); + $this->addMissingMetadata( $srcObj, $params['src'] ); + $stat = array( + // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW + 'mtime' => wfTimestamp( TS_MW, $srcObj->last_modified ), + 'size' => (int)$srcObj->content_length, + 'sha1' => $srcObj->metadata['Sha1base36'] + ); + } catch ( NoSuchContainerException $e ) { + } catch ( NoSuchObjectException $e ) { + } catch ( CloudFilesException $e ) { // some other exception? + $stat = null; + $this->handleException( $e, null, __METHOD__, $params ); + } + + return $stat; + } + + /** + * Fill in any missing object metadata and save it to Swift + * + * @param $obj CF_Object + * @param $path string Storage path to object + * @return bool Success + * @throws Exception cloudfiles exceptions + */ + protected function addMissingMetadata( CF_Object $obj, $path ) { + if ( isset( $obj->metadata['Sha1base36'] ) ) { + return true; // nothing to do + } + wfProfileIn( __METHOD__ ); + $status = Status::newGood(); + $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status ); + if ( $status->isOK() ) { + # Do not stat the file in getLocalCopy() to avoid infinite loops + $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1, 'nostat' => 1 ) ); + if ( $tmpFile ) { + $hash = $tmpFile->getSha1Base36(); + if ( $hash !== false ) { + $obj->metadata['Sha1base36'] = $hash; + $obj->sync_metadata(); // save to Swift + wfProfileOut( __METHOD__ ); + return true; // success + } + } + } + $obj->metadata['Sha1base36'] = false; + wfProfileOut( __METHOD__ ); + return false; // failed + } + + /** + * @see FileBackend::getFileContents() + * @return bool|null|string + */ + public function getFileContents( array $params ) { + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); + if ( $srcRel === null ) { + return false; // invalid storage path + } + + if ( !$this->fileExists( $params ) ) { + return null; + } + + $data = false; + try { + $sContObj = $this->getContainer( $srcCont ); + $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD + $data = $obj->read( $this->headersFromParams( $params ) ); + } catch ( NoSuchContainerException $e ) { + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, null, __METHOD__, $params ); + } + + return $data; + } + + /** + * @see FileBackendStore::doDirectoryExists() + * @return bool|null + */ + protected function doDirectoryExists( $fullCont, $dir, array $params ) { + try { + $container = $this->getContainer( $fullCont ); + $prefix = ( $dir == '' ) ? null : "{$dir}/"; + return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 ); + } catch ( NoSuchContainerException $e ) { + return false; + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, null, __METHOD__, + array( 'cont' => $fullCont, 'dir' => $dir ) ); + } + + return null; // error + } + + /** + * @see FileBackendStore::getDirectoryListInternal() + * @return SwiftFileBackendDirList + */ + public function getDirectoryListInternal( $fullCont, $dir, array $params ) { + return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params ); + } + + /** + * @see FileBackendStore::getFileListInternal() + * @return SwiftFileBackendFileList + */ + public function getFileListInternal( $fullCont, $dir, array $params ) { + return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params ); + } + + /** + * Do not call this function outside of SwiftFileBackendFileList + * + * @param $fullCont string Resolved container name + * @param $dir string Resolved storage directory with no trailing slash + * @param $after string|null Storage path of file to list items after + * @param $limit integer Max number of items to list + * @param $params Array Includes flag for 'topOnly' + * @return Array List of relative paths of dirs directly under $dir + */ + public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) { + $dirs = array(); + if ( $after === INF ) { + return $dirs; // nothing more + } + wfProfileIn( __METHOD__ . '-' . $this->name ); + + try { + $container = $this->getContainer( $fullCont ); + $prefix = ( $dir == '' ) ? null : "{$dir}/"; + // Non-recursive: only list dirs right under $dir + if ( !empty( $params['topOnly'] ) ) { + $objects = $container->list_objects( $limit, $after, $prefix, null, '/' ); + foreach ( $objects as $object ) { // files and dirs + if ( substr( $object, -1 ) === '/' ) { + $dirs[] = $object; // directories end in '/' + } + } + // Recursive: list all dirs under $dir and its subdirs + } else { + // Get directory from last item of prior page + $lastDir = $this->getParentDir( $after ); // must be first page + $objects = $container->list_objects( $limit, $after, $prefix ); + foreach ( $objects as $object ) { // files + $objectDir = $this->getParentDir( $object ); // directory of object + if ( $objectDir !== false ) { // file has a parent dir + // Swift stores paths in UTF-8, using binary sorting. + // See function "create_container_table" in common/db.py. + // If a directory is not "greater" than the last one, + // then it was already listed by the calling iterator. + if ( strcmp( $objectDir, $lastDir ) > 0 ) { + $pDir = $objectDir; + do { // add dir and all its parent dirs + $dirs[] = "{$pDir}/"; + $pDir = $this->getParentDir( $pDir ); + } while ( $pDir !== false // sanity + && strcmp( $pDir, $lastDir ) > 0 // not done already + && strlen( $pDir ) > strlen( $dir ) // within $dir + ); + } + $lastDir = $objectDir; + } + } + } + if ( count( $objects ) < $limit ) { + $after = INF; // avoid a second RTT + } else { + $after = end( $objects ); // update last item + } + } catch ( NoSuchContainerException $e ) { + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, null, __METHOD__, + array( 'cont' => $fullCont, 'dir' => $dir ) ); + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + return $dirs; + } + + protected function getParentDir( $path ) { + return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false; + } + + /** + * Do not call this function outside of SwiftFileBackendFileList + * + * @param $fullCont string Resolved container name + * @param $dir string Resolved storage directory with no trailing slash + * @param $after string|null Storage path of file to list items after + * @param $limit integer Max number of items to list + * @param $params Array Includes flag for 'topOnly' + * @return Array List of relative paths of files under $dir + */ + public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) { + $files = array(); + if ( $after === INF ) { + return $files; // nothing more + } + wfProfileIn( __METHOD__ . '-' . $this->name ); + + try { + $container = $this->getContainer( $fullCont ); + $prefix = ( $dir == '' ) ? null : "{$dir}/"; + // Non-recursive: only list files right under $dir + if ( !empty( $params['topOnly'] ) ) { // files and dirs + $objects = $container->list_objects( $limit, $after, $prefix, null, '/' ); + foreach ( $objects as $object ) { + if ( substr( $object, -1 ) !== '/' ) { + $files[] = $object; // directories end in '/' + } + } + // Recursive: list all files under $dir and its subdirs + } else { // files + $objects = $container->list_objects( $limit, $after, $prefix ); + $files = $objects; + } + if ( count( $objects ) < $limit ) { + $after = INF; // avoid a second RTT + } else { + $after = end( $objects ); // update last item + } + } catch ( NoSuchContainerException $e ) { + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, null, __METHOD__, + array( 'cont' => $fullCont, 'dir' => $dir ) ); + } + + wfProfileOut( __METHOD__ . '-' . $this->name ); + return $files; + } + + /** + * @see FileBackendStore::doGetFileSha1base36() + * @return bool + */ + protected function doGetFileSha1base36( array $params ) { + $stat = $this->getFileStat( $params ); + if ( $stat ) { + return $stat['sha1']; + } else { + return false; + } + } + + /** + * @see FileBackendStore::doStreamFile() + * @return Status + */ + protected function doStreamFile( array $params ) { + $status = Status::newGood(); + + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); + if ( $srcRel === null ) { + $status->fatal( 'backend-fail-invalidpath', $params['src'] ); + } + + try { + $cont = $this->getContainer( $srcCont ); + } catch ( NoSuchContainerException $e ) { + $status->fatal( 'backend-fail-stream', $params['src'] ); + return $status; + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + return $status; + } + + try { + $output = fopen( 'php://output', 'wb' ); + $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD + $obj->stream( $output, $this->headersFromParams( $params ) ); + } catch ( NoSuchObjectException $e ) { + $status->fatal( 'backend-fail-stream', $params['src'] ); + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, __METHOD__, $params ); + } + + return $status; + } + + /** + * @see FileBackendStore::getLocalCopy() + * @return null|TempFSFile + */ + public function getLocalCopy( array $params ) { + list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); + if ( $srcRel === null ) { + return null; + } + + // Blindly create a tmp file and stream to it, catching any exception if the file does + // not exist. Also, doing a stat here will cause infinite loops when filling metadata. + $tmpFile = null; + try { + $sContObj = $this->getContainer( $srcCont ); + $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD + // Get source file extension + $ext = FileBackend::extensionFromPath( $srcRel ); + // Create a new temporary file... + $tmpFile = TempFSFile::factory( 'localcopy_', $ext ); + if ( $tmpFile ) { + $handle = fopen( $tmpFile->getPath(), 'wb' ); + if ( $handle ) { + $obj->stream( $handle, $this->headersFromParams( $params ) ); + fclose( $handle ); + } else { + $tmpFile = null; // couldn't open temp file + } + } + } catch ( NoSuchContainerException $e ) { + $tmpFile = null; + } catch ( NoSuchObjectException $e ) { + $tmpFile = null; + } catch ( CloudFilesException $e ) { // some other exception? + $tmpFile = null; + $this->handleException( $e, null, __METHOD__, $params ); + } + + return $tmpFile; + } + + /** + * @see FileBackendStore::directoriesAreVirtual() + * @return bool + */ + protected function directoriesAreVirtual() { + return true; + } + + /** + * Get headers to send to Swift when reading a file based + * on a FileBackend params array, e.g. that of getLocalCopy(). + * $params is currently only checked for a 'latest' flag. + * + * @param $params Array + * @return Array + */ + protected function headersFromParams( array $params ) { + $hdrs = array(); + if ( !empty( $params['latest'] ) ) { + $hdrs[] = 'X-Newest: true'; + } + return $hdrs; + } + + /** + * @see FileBackendStore::doExecuteOpHandlesInternal() + * @return Array List of corresponding Status objects + */ + protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { + $statuses = array(); + + $cfOps = array(); // list of CF_Async_Op objects + foreach ( $fileOpHandles as $index => $fileOpHandle ) { + $cfOps[$index] = $fileOpHandle->cfOp; + } + $batch = new CF_Async_Op_Batch( $cfOps ); + + $cfOps = $batch->execute(); + foreach ( $cfOps as $index => $cfOp ) { + $status = Status::newGood(); + try { // catch exceptions; update status + $function = '_getResponse' . $fileOpHandles[$index]->call; + $this->$function( $cfOp, $status, $fileOpHandles[$index]->params ); + $this->purgeCDNCache( $fileOpHandles[$index]->affectedObjects ); + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, $status, + __CLASS__ . ":$function", $fileOpHandles[$index]->params ); + } + $statuses[$index] = $status; + } + + return $statuses; + } + + /** + * Set read/write permissions for a Swift container. + * + * $readGrps is a list of the possible criteria for a request to have + * access to read a container. Each item is one of the following formats: + * - account:user : Grants access if the request is by the given user + * - .r:<regex> : Grants access if the request is from a referrer host that + * matches the expression and the request is not for a listing. + * Setting this to '*' effectively makes a container public. + * - .rlistings:<regex> : Grants access if the request is from a referrer host that + * matches the expression and the request for a listing. + * + * $writeGrps is a list of the possible criteria for a request to have + * access to write to a container. Each item is of the following format: + * - account:user : Grants access if the request is by the given user + * + * @see http://swift.openstack.org/misc.html#acls + * + * In general, we don't allow listings to end-users. It's not useful, isn't well-defined + * (lists are truncated to 10000 item with no way to page), and is just a performance risk. + * + * @param $contObj CF_Container Swift container + * @param $readGrps Array List of read access routes + * @param $writeGrps Array List of write access routes + * @return Status + */ + protected function setContainerAccess( + CF_Container $contObj, array $readGrps, array $writeGrps + ) { + $creds = $contObj->cfs_auth->export_credentials(); + + $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name ); + + // Note: 10 second timeout consistent with php-cloudfiles + $req = MWHttpRequest::factory( $url, array( 'method' => 'POST', 'timeout' => 10 ) ); + $req->setHeader( 'X-Auth-Token', $creds['auth_token'] ); + $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) ); + $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) ); + + return $req->execute(); // should return 204 + } + + /** + * Purge the CDN cache of affected objects if CDN caching is enabled. + * This is for Rackspace/Akamai CDNs. + * + * @param $objects Array List of CF_Object items + * @return void + */ + public function purgeCDNCache( array $objects ) { + if ( $this->swiftUseCDN && $this->swiftCDNPurgable ) { + foreach ( $objects as $object ) { + try { + $object->purge_from_cdn(); + } catch ( CDNNotEnabledException $e ) { + // CDN not enabled; nothing to see here + } catch ( CloudFilesException $e ) { + $this->handleException( $e, null, __METHOD__, + array( 'cont' => $object->container->name, 'obj' => $object->name ) ); + } + } + } + } + + /** + * Get an authenticated connection handle to the Swift proxy + * + * @return CF_Connection|bool False on failure + * @throws CloudFilesException + */ + protected function getConnection() { + if ( $this->connException instanceof CloudFilesException ) { + if ( ( time() - $this->connErrorTime ) < 60 ) { + throw $this->connException; // failed last attempt; don't bother + } else { // actually retry this time + $this->connException = null; + $this->connErrorTime = 0; + } + } + // Session keys expire after a while, so we renew them periodically + $reAuth = ( ( time() - $this->sessionStarted ) > $this->authTTL ); + // Authenticate with proxy and get a session key... + if ( !$this->conn || $reAuth ) { + $this->sessionStarted = 0; + $this->connContainerCache->clear(); + $cacheKey = $this->getCredsCacheKey( $this->auth->username ); + $creds = $this->srvCache->get( $cacheKey ); // credentials + if ( is_array( $creds ) ) { // cache hit + $this->auth->load_cached_credentials( + $creds['auth_token'], $creds['storage_url'], $creds['cdnm_url'] ); + $this->sessionStarted = time() - ceil( $this->authTTL/2 ); // skew for worst case + } else { // cache miss + try { + $this->auth->authenticate(); + $creds = $this->auth->export_credentials(); + $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL/2 ) ); // cache + $this->sessionStarted = time(); + } catch ( CloudFilesException $e ) { + $this->connException = $e; // don't keep re-trying + $this->connErrorTime = time(); + throw $e; // throw it back + } + } + if ( $this->conn ) { // re-authorizing? + $this->conn->close(); // close active cURL handles in CF_Http object + } + $this->conn = new CF_Connection( $this->auth ); + } + return $this->conn; + } + + /** + * Close the connection to the Swift proxy + * + * @return void + */ + protected function closeConnection() { + if ( $this->conn ) { + $this->conn->close(); // close active cURL handles in CF_Http object + $this->sessionStarted = 0; + $this->connContainerCache->clear(); + } + } + + /** + * Get the cache key for a container + * + * @param $username string + * @return string + */ + private function getCredsCacheKey( $username ) { + return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username ); + } + + /** + * @see FileBackendStore::doClearCache() + */ + protected function doClearCache( array $paths = null ) { + $this->connContainerCache->clear(); // clear container object cache + } + + /** + * Get a Swift container object, possibly from process cache. + * Use $reCache if the file count or byte count is needed. + * + * @param $container string Container name + * @param $bypassCache bool Bypass all caches and load from Swift + * @return CF_Container + * @throws CloudFilesException + */ + protected function getContainer( $container, $bypassCache = false ) { + $conn = $this->getConnection(); // Swift proxy connection + if ( $bypassCache ) { // purge cache + $this->connContainerCache->clear( $container ); + } elseif ( !$this->connContainerCache->has( $container, 'obj' ) ) { + $this->primeContainerCache( array( $container ) ); // check persistent cache + } + if ( !$this->connContainerCache->has( $container, 'obj' ) ) { + $contObj = $conn->get_container( $container ); + // NoSuchContainerException not thrown: container must exist + $this->connContainerCache->set( $container, 'obj', $contObj ); // cache it + if ( !$bypassCache ) { + $this->setContainerCache( $container, // update persistent cache + array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count ) + ); + } + } + return $this->connContainerCache->get( $container, 'obj' ); + } + + /** + * Create a Swift container + * + * @param $container string Container name + * @return CF_Container + * @throws CloudFilesException + */ + protected function createContainer( $container ) { + $conn = $this->getConnection(); // Swift proxy connection + $contObj = $conn->create_container( $container ); + $this->connContainerCache->set( $container, 'obj', $contObj ); // cache + return $contObj; + } + + /** + * Delete a Swift container + * + * @param $container string Container name + * @return void + * @throws CloudFilesException + */ + protected function deleteContainer( $container ) { + $conn = $this->getConnection(); // Swift proxy connection + $this->connContainerCache->clear( $container ); // purge + $conn->delete_container( $container ); + } + + /** + * @see FileBackendStore::doPrimeContainerCache() + * @return void + */ + protected function doPrimeContainerCache( array $containerInfo ) { + try { + $conn = $this->getConnection(); // Swift proxy connection + foreach ( $containerInfo as $container => $info ) { + $contObj = new CF_Container( $conn->cfs_auth, $conn->cfs_http, + $container, $info['count'], $info['bytes'] ); + $this->connContainerCache->set( $container, 'obj', $contObj ); + } + } catch ( CloudFilesException $e ) { // some other exception? + $this->handleException( $e, null, __METHOD__, array() ); + } + } + + /** + * Log an unexpected exception for this backend. + * This also sets the Status object to have a fatal error. + * + * @param $e Exception + * @param $status Status|null + * @param $func string + * @param $params Array + * @return void + */ + protected function handleException( Exception $e, $status, $func, array $params ) { + if ( $status instanceof Status ) { + if ( $e instanceof AuthenticationException ) { + $status->fatal( 'backend-fail-connect', $this->name ); + } else { + $status->fatal( 'backend-fail-internal', $this->name ); + } + } + if ( $e->getMessage() ) { + trigger_error( "$func: " . $e->getMessage(), E_USER_WARNING ); + } + if ( $e instanceof InvalidResponseException ) { // possibly a stale token + $this->srvCache->delete( $this->getCredsCacheKey( $this->auth->username ) ); + $this->closeConnection(); // force a re-connect and re-auth next time + } + wfDebugLog( 'SwiftBackend', + get_class( $e ) . " in '{$func}' (given '" . FormatJson::encode( $params ) . "')" . + ( $e->getMessage() ? ": {$e->getMessage()}" : "" ) + ); + } +} + +/** + * @see FileBackendStoreOpHandle + */ +class SwiftFileOpHandle extends FileBackendStoreOpHandle { + /** @var CF_Async_Op */ + public $cfOp; + /** @var Array */ + public $affectedObjects = array(); + + public function __construct( $backend, array $params, $call, CF_Async_Op $cfOp ) { + $this->backend = $backend; + $this->params = $params; + $this->call = $call; + $this->cfOp = $cfOp; + } +} + +/** + * SwiftFileBackend helper class to page through listings. + * Swift also has a listing limit of 10,000 objects for sanity. + * Do not use this class from places outside SwiftFileBackend. + * + * @ingroup FileBackend + */ +abstract class SwiftFileBackendList implements Iterator { + /** @var Array */ + protected $bufferIter = array(); + protected $bufferAfter = null; // string; list items *after* this path + protected $pos = 0; // integer + /** @var Array */ + protected $params = array(); + + /** @var SwiftFileBackend */ + protected $backend; + protected $container; // string; container name + protected $dir; // string; storage directory + protected $suffixStart; // integer + + const PAGE_SIZE = 9000; // file listing buffer size + + /** + * @param $backend SwiftFileBackend + * @param $fullCont string Resolved container name + * @param $dir string Resolved directory relative to container + * @param $params Array + */ + public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) { + $this->backend = $backend; + $this->container = $fullCont; + $this->dir = $dir; + if ( substr( $this->dir, -1 ) === '/' ) { + $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash + } + if ( $this->dir == '' ) { // whole container + $this->suffixStart = 0; + } else { // dir within container + $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/" + } + $this->params = $params; + } + + /** + * @see Iterator::key() + * @return integer + */ + public function key() { + return $this->pos; + } + + /** + * @see Iterator::next() + * @return void + */ + public function next() { + // Advance to the next file in the page + next( $this->bufferIter ); + ++$this->pos; + // Check if there are no files left in this page and + // advance to the next page if this page was not empty. + if ( !$this->valid() && count( $this->bufferIter ) ) { + $this->bufferIter = $this->pageFromList( + $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params + ); // updates $this->bufferAfter + } + } + + /** + * @see Iterator::rewind() + * @return void + */ + public function rewind() { + $this->pos = 0; + $this->bufferAfter = null; + $this->bufferIter = $this->pageFromList( + $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params + ); // updates $this->bufferAfter + } + + /** + * @see Iterator::valid() + * @return bool + */ + public function valid() { + if ( $this->bufferIter === null ) { + return false; // some failure? + } else { + return ( current( $this->bufferIter ) !== false ); // no paths can have this value + } + } + + /** + * Get the given list portion (page) + * + * @param $container string Resolved container name + * @param $dir string Resolved path relative to container + * @param $after string|null + * @param $limit integer + * @param $params Array + * @return Traversable|Array|null Returns null on failure + */ + abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params ); +} + +/** + * Iterator for listing directories + */ +class SwiftFileBackendDirList extends SwiftFileBackendList { + /** + * @see Iterator::current() + * @return string|bool String (relative path) or false + */ + public function current() { + return substr( current( $this->bufferIter ), $this->suffixStart, -1 ); + } + + /** + * @see SwiftFileBackendList::pageFromList() + * @return Array|null + */ + protected function pageFromList( $container, $dir, &$after, $limit, array $params ) { + return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params ); + } +} + +/** + * Iterator for listing regular files + */ +class SwiftFileBackendFileList extends SwiftFileBackendList { + /** + * @see Iterator::current() + * @return string|bool String (relative path) or false + */ + public function current() { + return substr( current( $this->bufferIter ), $this->suffixStart ); + } + + /** + * @see SwiftFileBackendList::pageFromList() + * @return Array|null + */ + protected function pageFromList( $container, $dir, &$after, $limit, array $params ) { + return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params ); + } +} diff --git a/includes/filerepo/backend/TempFSFile.php b/includes/filebackend/TempFSFile.php index 7843d6cd..5032bf68 100644 --- a/includes/filerepo/backend/TempFSFile.php +++ b/includes/filebackend/TempFSFile.php @@ -1,12 +1,29 @@ <?php /** + * Location holder of files stored temporarily + * + * 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 FileBackend */ /** * This class is used to hold the location and do limited manipulation - * of files stored temporarily (usually this will be $wgTmpDirectory) + * of files stored temporarily (this will be whatever wfTempDir() returns) * * @ingroup FileBackend */ @@ -19,13 +36,14 @@ class TempFSFile extends FSFile { /** * Make a new temporary file on the file system. * Temporary files may be purged when the file object falls out of scope. - * + * * @param $prefix string * @param $extension string - * @return TempFSFile|null + * @return TempFSFile|null */ public static function factory( $prefix, $extension = '' ) { - $base = wfTempDir() . '/' . $prefix . dechex( mt_rand( 0, 99999999 ) ); + wfProfileIn( __METHOD__ ); + $base = wfTempDir() . '/' . $prefix . wfRandomString( 12 ); $ext = ( $extension != '' ) ? ".{$extension}" : ""; for ( $attempt = 1; true; $attempt++ ) { $path = "{$base}-{$attempt}{$ext}"; @@ -36,18 +54,20 @@ class TempFSFile extends FSFile { fclose( $newFileHandle ); break; // got it } - if ( $attempt >= 15 ) { + if ( $attempt >= 5 ) { + wfProfileOut( __METHOD__ ); return null; // give up } } $tmpFile = new self( $path ); $tmpFile->canDelete = true; // safely instantiated + wfProfileOut( __METHOD__ ); return $tmpFile; } /** * Purge this file off the file system - * + * * @return bool Success */ public function purge() { @@ -80,6 +100,15 @@ class TempFSFile extends FSFile { } /** + * Set flag clean up after the temporary file + * + * @return void + */ + public function autocollect() { + $this->canDelete = true; + } + + /** * Cleans up after the temporary file by deleting it */ function __destruct() { diff --git a/includes/filebackend/filejournal/DBFileJournal.php b/includes/filebackend/filejournal/DBFileJournal.php new file mode 100644 index 00000000..f6268c25 --- /dev/null +++ b/includes/filebackend/filejournal/DBFileJournal.php @@ -0,0 +1,152 @@ +<?php +/** + * Version of FileJournal that logs to a DB table. + * + * 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 FileJournal + * @author Aaron Schulz + */ + +/** + * Version of FileJournal that logs to a DB table + * @since 1.20 + */ +class DBFileJournal extends FileJournal { + /** @var DatabaseBase */ + protected $dbw; + + protected $wiki = false; // string; wiki DB name + + /** + * Construct a new instance from configuration. + * $config includes: + * 'wiki' : wiki name to use for LoadBalancer + * + * @param $config Array + */ + protected function __construct( array $config ) { + parent::__construct( $config ); + + $this->wiki = $config['wiki']; + } + + /** + * @see FileJournal::logChangeBatch() + * @return Status + */ + protected function doLogChangeBatch( array $entries, $batchId ) { + $status = Status::newGood(); + + try { + $dbw = $this->getMasterDB(); + } catch ( DBError $e ) { + $status->fatal( 'filejournal-fail-dbconnect', $this->backend ); + return $status; + } + + $now = wfTimestamp( TS_UNIX ); + + $data = array(); + foreach ( $entries as $entry ) { + $data[] = array( + 'fj_batch_uuid' => $batchId, + 'fj_backend' => $this->backend, + 'fj_op' => $entry['op'], + 'fj_path' => $entry['path'], + 'fj_new_sha1' => $entry['newSha1'], + 'fj_timestamp' => $dbw->timestamp( $now ) + ); + } + + try { + $dbw->insert( 'filejournal', $data, __METHOD__ ); + } catch ( DBError $e ) { + $status->fatal( 'filejournal-fail-dbquery', $this->backend ); + return $status; + } + + return $status; + } + + /** + * @see FileJournal::doGetChangeEntries() + * @return Array + * @throws DBError + */ + protected function doGetChangeEntries( $start, $limit ) { + $dbw = $this->getMasterDB(); + + $res = $dbw->select( 'filejournal', '*', + array( + 'fj_backend' => $this->backend, + 'fj_id >= ' . $dbw->addQuotes( (int)$start ) ), // $start may be 0 + __METHOD__, + array_merge( array( 'ORDER BY' => 'fj_id ASC' ), + $limit ? array( 'LIMIT' => $limit ) : array() ) + ); + + $entries = array(); + foreach ( $res as $row ) { + $item = array(); + foreach ( (array)$row as $key => $value ) { + $item[substr( $key, 3 )] = $value; // "fj_op" => "op" + } + $entries[] = $item; + } + + return $entries; + } + + /** + * @see FileJournal::purgeOldLogs() + * @return Status + * @throws DBError + */ + protected function doPurgeOldLogs() { + $status = Status::newGood(); + if ( $this->ttlDays <= 0 ) { + return $status; // nothing to do + } + + $dbw = $this->getMasterDB(); + $dbCutoff = $dbw->timestamp( time() - 86400 * $this->ttlDays ); + + $dbw->delete( 'filejournal', + array( 'fj_timestamp < ' . $dbw->addQuotes( $dbCutoff ) ), + __METHOD__ + ); + + return $status; + } + + /** + * Get a master connection to the logging DB + * + * @return DatabaseBase + * @throws DBError + */ + protected function getMasterDB() { + if ( !$this->dbw ) { + // Get a separate connection in autocommit mode + $lb = wfGetLBFactory()->newMainLB(); + $this->dbw = $lb->getConnection( DB_MASTER, array(), $this->wiki ); + $this->dbw->clearFlag( DBO_TRX ); + } + return $this->dbw; + } +} diff --git a/includes/filebackend/filejournal/FileJournal.php b/includes/filebackend/filejournal/FileJournal.php new file mode 100644 index 00000000..ce029bbe --- /dev/null +++ b/includes/filebackend/filejournal/FileJournal.php @@ -0,0 +1,196 @@ +<?php +/** + * @defgroup FileJournal File journal + * @ingroup FileBackend + */ + +/** + * File operation journaling. + * + * 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 FileJournal + * @author Aaron Schulz + */ + +/** + * @brief Class for handling file operation journaling. + * + * Subclasses should avoid throwing exceptions at all costs. + * + * @ingroup FileJournal + * @since 1.20 + */ +abstract class FileJournal { + protected $backend; // string + protected $ttlDays; // integer + + /** + * Construct a new instance from configuration. + * $config includes: + * 'ttlDays' : days to keep log entries around (false means "forever") + * + * @param $config Array + */ + protected function __construct( array $config ) { + $this->ttlDays = isset( $config['ttlDays'] ) ? $config['ttlDays'] : false; + } + + /** + * Create an appropriate FileJournal object from config + * + * @param $config Array + * @param $backend string A registered file backend name + * @throws MWException + * @return FileJournal + */ + final public static function factory( array $config, $backend ) { + $class = $config['class']; + $jrn = new $class( $config ); + if ( !$jrn instanceof self ) { + throw new MWException( "Class given is not an instance of FileJournal." ); + } + $jrn->backend = $backend; + return $jrn; + } + + /** + * Get a statistically unique ID string + * + * @return string <9 char TS_MW timestamp in base 36><22 random base 36 chars> + */ + final public function getTimestampedUUID() { + $s = ''; + for ( $i = 0; $i < 5; $i++ ) { + $s .= mt_rand( 0, 2147483647 ); + } + $s = wfBaseConvert( sha1( $s ), 16, 36, 31 ); + return substr( wfBaseConvert( wfTimestamp( TS_MW ), 10, 36, 9 ) . $s, 0, 31 ); + } + + /** + * Log changes made by a batch file operation. + * $entries is an array of log entries, each of which contains: + * op : Basic operation name (create, store, copy, delete) + * path : The storage path of the file + * newSha1 : The final base 36 SHA-1 of the file + * Note that 'false' should be used as the SHA-1 for non-existing files. + * + * @param $entries Array List of file operations (each an array of parameters) + * @param $batchId string UUID string that identifies the operation batch + * @return Status + */ + final public function logChangeBatch( array $entries, $batchId ) { + if ( !count( $entries ) ) { + return Status::newGood(); + } + return $this->doLogChangeBatch( $entries, $batchId ); + } + + /** + * @see FileJournal::logChangeBatch() + * + * @param $entries Array List of file operations (each an array of parameters) + * @param $batchId string UUID string that identifies the operation batch + * @return Status + */ + abstract protected function doLogChangeBatch( array $entries, $batchId ); + + /** + * Get an array of file change log entries. + * A starting change ID and/or limit can be specified. + * + * The result as a list of associative arrays, each having: + * id : unique, monotonic, ID for this change + * batch_uuid : UUID for an operation batch + * backend : the backend name + * op : primitive operation (create,update,delete,null) + * path : affected storage path + * new_sha1 : base 36 sha1 of the new file had the operation succeeded + * timestamp : TS_MW timestamp of the batch change + + * Also, $next is updated to the ID of the next entry. + * + * @param $start integer Starting change ID or null + * @param $limit integer Maximum number of items to return + * @param &$next string + * @return Array + */ + final public function getChangeEntries( $start = null, $limit = 0, &$next = null ) { + $entries = $this->doGetChangeEntries( $start, $limit ? $limit + 1 : 0 ); + if ( $limit && count( $entries ) > $limit ) { + $last = array_pop( $entries ); // remove the extra entry + $next = $last['id']; // update for next call + } else { + $next = null; // end of list + } + return $entries; + } + + /** + * @see FileJournal::getChangeEntries() + * @return Array + */ + abstract protected function doGetChangeEntries( $start, $limit ); + + /** + * Purge any old log entries + * + * @return Status + */ + final public function purgeOldLogs() { + return $this->doPurgeOldLogs(); + } + + /** + * @see FileJournal::purgeOldLogs() + * @return Status + */ + abstract protected function doPurgeOldLogs(); +} + +/** + * Simple version of FileJournal that does nothing + * @since 1.20 + */ +class NullFileJournal extends FileJournal { + /** + * @see FileJournal::logChangeBatch() + * @param $entries array + * @param $batchId string + * @return Status + */ + protected function doLogChangeBatch( array $entries, $batchId ) { + return Status::newGood(); + } + + /** + * @see FileJournal::doGetChangeEntries() + * @return Array + */ + protected function doGetChangeEntries( $start, $limit ) { + return array(); + } + + /** + * @see FileJournal::purgeOldLogs() + * @return Status + */ + protected function doPurgeOldLogs() { + return Status::newGood(); + } +} diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php new file mode 100644 index 00000000..a8fe258b --- /dev/null +++ b/includes/filebackend/lockmanager/DBLockManager.php @@ -0,0 +1,374 @@ +<?php +/** + * Version of LockManager based on using DB table locks. + * + * 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 LockManager + */ + +/** + * Version of LockManager based on using DB table locks. + * This is meant for multi-wiki systems that may share files. + * All locks are blocking, so it might be useful to set a small + * lock-wait timeout via server config to curtail deadlocks. + * + * All lock requests for a resource, identified by a hash string, will map + * to one bucket. Each bucket maps to one or several peer DBs, each on their + * own server, all having the filelocks.sql tables (with row-level locking). + * A majority of peer DBs must agree for a lock to be acquired. + * + * Caching is used to avoid hitting servers that are down. + * + * @ingroup LockManager + * @since 1.19 + */ +class DBLockManager extends QuorumLockManager { + /** @var Array Map of DB names to server config */ + protected $dbServers; // (DB name => server config array) + /** @var BagOStuff */ + protected $statusCache; + + protected $lockExpiry; // integer number of seconds + protected $safeDelay; // integer number of seconds + + protected $session = 0; // random integer + /** @var Array Map Database connections (DB name => Database) */ + protected $conns = array(); + + /** + * Construct a new instance from configuration. + * + * $config paramaters include: + * - dbServers : Associative array of DB names to server configuration. + * Configuration is an associative array that includes: + * - host : DB server name + * - dbname : DB name + * - type : DB type (mysql,postgres,...) + * - user : DB user + * - password : DB user password + * - tablePrefix : DB table prefix + * - flags : DB flags (see DatabaseBase) + * - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0, + * each having an odd-numbered list of DB names (peers) as values. + * Any DB named 'localDBMaster' will automatically use the DB master + * settings for this wiki (without the need for a dbServers entry). + * - lockExpiry : Lock timeout (seconds) for dropped connections. [optional] + * This tells the DB server how long to wait before assuming + * connection failure and releasing all the locks for a session. + * + * @param Array $config + */ + public function __construct( array $config ) { + parent::__construct( $config ); + + $this->dbServers = isset( $config['dbServers'] ) + ? $config['dbServers'] + : array(); // likely just using 'localDBMaster' + // Sanitize srvsByBucket config to prevent PHP errors + $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' ); + $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive + + if ( isset( $config['lockExpiry'] ) ) { + $this->lockExpiry = $config['lockExpiry']; + } else { + $met = ini_get( 'max_execution_time' ); + $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0 + } + $this->safeDelay = ( $this->lockExpiry <= 0 ) + ? 60 // pick a safe-ish number to match DB timeout default + : $this->lockExpiry; // cover worst case + + foreach ( $this->srvsByBucket as $bucket ) { + if ( count( $bucket ) > 1 ) { // multiple peers + // Tracks peers that couldn't be queried recently to avoid lengthy + // connection timeouts. This is useless if each bucket has one peer. + try { + $this->statusCache = ObjectCache::newAccelerator( array() ); + } catch ( MWException $e ) { + trigger_error( __CLASS__ . + " using multiple DB peers without apc, xcache, or wincache." ); + } + break; + } + } + + $this->session = wfRandomString( 31 ); + } + + /** + * Get a connection to a lock DB and acquire locks on $paths. + * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118. + * + * @see QuorumLockManager::getLocksOnServer() + * @return Status + */ + protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + if ( $type == self::LOCK_EX ) { // writer locks + try { + $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); + # Build up values for INSERT clause + $data = array(); + foreach ( $keys as $key ) { + $data[] = array( 'fle_key' => $key ); + } + # Wait on any existing writers and block new ones if we get in + $db = $this->getConnection( $lockSrv ); // checked in isServerUp() + $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); + } catch ( DBError $e ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } + } + + return $status; + } + + /** + * @see QuorumLockManager::freeLocksOnServer() + * @return Status + */ + protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { + return Status::newGood(); // not supported + } + + /** + * @see QuorumLockManager::releaseAllLocks() + * @return Status + */ + protected function releaseAllLocks() { + $status = Status::newGood(); + + foreach ( $this->conns as $lockDb => $db ) { + if ( $db->trxLevel() ) { // in transaction + try { + $db->rollback( __METHOD__ ); // finish transaction and kill any rows + } catch ( DBError $e ) { + $status->fatal( 'lockmanager-fail-db-release', $lockDb ); + } + } + } + + return $status; + } + + /** + * @see QuorumLockManager::isServerUp() + * @return bool + */ + protected function isServerUp( $lockSrv ) { + if ( !$this->cacheCheckFailures( $lockSrv ) ) { + return false; // recent failure to connect + } + try { + $this->getConnection( $lockSrv ); + } catch ( DBError $e ) { + $this->cacheRecordFailure( $lockSrv ); + return false; // failed to connect + } + return true; + } + + /** + * Get (or reuse) a connection to a lock DB + * + * @param $lockDb string + * @return DatabaseBase + * @throws DBError + */ + protected function getConnection( $lockDb ) { + if ( !isset( $this->conns[$lockDb] ) ) { + $db = null; + if ( $lockDb === 'localDBMaster' ) { + $lb = wfGetLBFactory()->newMainLB(); + $db = $lb->getConnection( DB_MASTER ); + } elseif ( isset( $this->dbServers[$lockDb] ) ) { + $config = $this->dbServers[$lockDb]; + $db = DatabaseBase::factory( $config['type'], $config ); + } + if ( !$db ) { + return null; // config error? + } + $this->conns[$lockDb] = $db; + $this->conns[$lockDb]->clearFlag( DBO_TRX ); + # If the connection drops, try to avoid letting the DB rollback + # and release the locks before the file operations are finished. + # This won't handle the case of DB server restarts however. + $options = array(); + if ( $this->lockExpiry > 0 ) { + $options['connTimeout'] = $this->lockExpiry; + } + $this->conns[$lockDb]->setSessionOptions( $options ); + $this->initConnection( $lockDb, $this->conns[$lockDb] ); + } + if ( !$this->conns[$lockDb]->trxLevel() ) { + $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction + } + return $this->conns[$lockDb]; + } + + /** + * Do additional initialization for new lock DB connection + * + * @param $lockDb string + * @param $db DatabaseBase + * @return void + * @throws DBError + */ + protected function initConnection( $lockDb, DatabaseBase $db ) {} + + /** + * Checks if the DB has not recently had connection/query errors. + * This just avoids wasting time on doomed connection attempts. + * + * @param $lockDb string + * @return bool + */ + protected function cacheCheckFailures( $lockDb ) { + return ( $this->statusCache && $this->safeDelay > 0 ) + ? !$this->statusCache->get( $this->getMissKey( $lockDb ) ) + : true; + } + + /** + * Log a lock request failure to the cache + * + * @param $lockDb string + * @return bool Success + */ + protected function cacheRecordFailure( $lockDb ) { + return ( $this->statusCache && $this->safeDelay > 0 ) + ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay ) + : true; + } + + /** + * Get a cache key for recent query misses for a DB + * + * @param $lockDb string + * @return string + */ + protected function getMissKey( $lockDb ) { + $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative + return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb ); + } + + /** + * Make sure remaining locks get cleared for sanity + */ + function __destruct() { + foreach ( $this->conns as $db ) { + if ( $db->trxLevel() ) { // in transaction + try { + $db->rollback( __METHOD__ ); // finish transaction and kill any rows + } catch ( DBError $e ) { + // oh well + } + } + $db->close(); + } + } +} + +/** + * MySQL version of DBLockManager that supports shared locks. + * All locks are non-blocking, which avoids deadlocks. + * + * @ingroup LockManager + */ +class MySqlLockManager extends DBLockManager { + /** @var Array Mapping of lock types to the type actually used */ + protected $lockTypeMap = array( + self::LOCK_SH => self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ); + + /** + * @param $lockDb string + * @param $db DatabaseBase + */ + protected function initConnection( $lockDb, DatabaseBase $db ) { + # Let this transaction see lock rows from other transactions + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); + } + + /** + * Get a connection to a lock DB and acquire locks on $paths. + * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118. + * + * @see DBLockManager::getLocksOnServer() + * @return Status + */ + protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + $db = $this->getConnection( $lockSrv ); // checked in isServerUp() + $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); + # Build up values for INSERT clause + $data = array(); + foreach ( $keys as $key ) { + $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session ); + } + # Block new writers... + $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) ); + # Actually do the locking queries... + if ( $type == self::LOCK_SH ) { // reader locks + # Bail if there are any existing writers... + $blocked = $db->selectField( 'filelocks_exclusive', '1', + array( 'fle_key' => $keys ), + __METHOD__ + ); + # Prospective writers that haven't yet updated filelocks_exclusive + # will recheck filelocks_shared after doing so and bail due to our entry. + } else { // writer locks + $encSession = $db->addQuotes( $this->session ); + # Bail if there are any existing writers... + # The may detect readers, but the safe check for them is below. + # Note: if two writers come at the same time, both bail :) + $blocked = $db->selectField( 'filelocks_shared', '1', + array( 'fls_key' => $keys, "fls_session != $encSession" ), + __METHOD__ + ); + if ( !$blocked ) { + # Build up values for INSERT clause + $data = array(); + foreach ( $keys as $key ) { + $data[] = array( 'fle_key' => $key ); + } + # Block new readers/writers... + $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); + # Bail if there are any existing readers... + $blocked = $db->selectField( 'filelocks_shared', '1', + array( 'fls_key' => $keys, "fls_session != $encSession" ), + __METHOD__ + ); + } + } + + if ( $blocked ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } + + return $status; + } +} diff --git a/includes/filerepo/backend/lockmanager/FSLockManager.php b/includes/filebackend/lockmanager/FSLockManager.php index 42074fd3..9a6206fd 100644 --- a/includes/filerepo/backend/lockmanager/FSLockManager.php +++ b/includes/filebackend/lockmanager/FSLockManager.php @@ -1,4 +1,25 @@ <?php +/** + * Simple version of LockManager based on using FS lock files. + * + * 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 LockManager + */ /** * Simple version of LockManager based on using FS lock files. @@ -27,17 +48,24 @@ class FSLockManager extends LockManager { /** * Construct a new instance from configuration. - * + * * $config includes: - * 'lockDirectory' : Directory containing the lock files + * - lockDirectory : Directory containing the lock files * * @param array $config */ function __construct( array $config ) { parent::__construct( $config ); + $this->lockDir = $config['lockDirectory']; } + /** + * @see LockManager::doLock() + * @param $paths array + * @param $type int + * @return Status + */ protected function doLock( array $paths, $type ) { $status = Status::newGood(); @@ -56,6 +84,12 @@ class FSLockManager extends LockManager { return $status; } + /** + * @see LockManager::doUnlock() + * @param $paths array + * @param $type int + * @return Status + */ protected function doUnlock( array $paths, $type ) { $status = Status::newGood(); @@ -71,7 +105,7 @@ class FSLockManager extends LockManager { * * @param $path string * @param $type integer - * @return Status + * @return Status */ protected function doSingleLock( $path, $type ) { $status = Status::newGood(); @@ -109,10 +143,10 @@ class FSLockManager extends LockManager { /** * Unlock a single resource key - * + * * @param $path string * @param $type integer - * @return Status + * @return Status */ protected function doSingleUnlock( $path, $type ) { $status = Status::newGood(); @@ -129,11 +163,22 @@ class FSLockManager extends LockManager { // If a LOCK_SH comes in while we have a LOCK_EX, we don't // actually add a handler, so check for handler existence. if ( isset( $this->handles[$path][$type] ) ) { - // Mark this handle to be unlocked and closed - $handlesToClose[] = $this->handles[$path][$type]; + if ( $type === self::LOCK_EX + && isset( $this->locksHeld[$path][self::LOCK_SH] ) + && !isset( $this->handles[$path][self::LOCK_SH] ) ) + { + // EX lock came first: move this handle to the SH one + $this->handles[$path][self::LOCK_SH] = $this->handles[$path][$type]; + } else { + // Mark this handle to be unlocked and closed + $handlesToClose[] = $this->handles[$path][$type]; + } unset( $this->handles[$path][$type] ); } } + if ( !count( $this->locksHeld[$path] ) ) { + unset( $this->locksHeld[$path] ); // no locks on this path + } // Unlock handles to release locks and delete // any lock files that end up with no locks on them... if ( wfIsWindows() ) { @@ -142,7 +187,7 @@ class FSLockManager extends LockManager { $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); $status->merge( $this->pruneKeyLockFiles( $path ) ); } else { - // Unix: unlink() can be used on files currently open by this + // Unix: unlink() can be used on files currently open by this // process and we must do so in order to avoid race conditions $status->merge( $this->pruneKeyLockFiles( $path ) ); $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); @@ -152,31 +197,35 @@ class FSLockManager extends LockManager { return $status; } + /** + * @param $path string + * @param $handlesToClose array + * @return Status + */ private function closeLockHandles( $path, array $handlesToClose ) { $status = Status::newGood(); foreach ( $handlesToClose as $handle ) { - wfSuppressWarnings(); if ( !flock( $handle, LOCK_UN ) ) { $status->fatal( 'lockmanager-fail-releaselock', $path ); } if ( !fclose( $handle ) ) { $status->warning( 'lockmanager-fail-closelock', $path ); } - wfRestoreWarnings(); } return $status; } + /** + * @param $path string + * @return Status + */ private function pruneKeyLockFiles( $path ) { $status = Status::newGood(); - if ( !count( $this->locksHeld[$path] ) ) { - wfSuppressWarnings(); + if ( !isset( $this->locksHeld[$path] ) ) { # No locks are held for the lock file anymore if ( !unlink( $this->getLockPath( $path ) ) ) { $status->warning( 'lockmanager-fail-deletelock', $path ); } - wfRestoreWarnings(); - unset( $this->locksHeld[$path] ); unset( $this->handles[$path] ); } return $status; @@ -192,11 +241,15 @@ class FSLockManager extends LockManager { return "{$this->lockDir}/{$hash}.lock"; } + /** + * Make sure remaining locks get cleared for sanity + */ function __destruct() { - // Make sure remaining locks get cleared for sanity - foreach ( $this->locksHeld as $path => $locks ) { - $this->doSingleUnlock( $path, self::LOCK_EX ); - $this->doSingleUnlock( $path, self::LOCK_SH ); + while ( count( $this->locksHeld ) ) { + foreach ( $this->locksHeld as $path => $locks ) { + $this->doSingleUnlock( $path, self::LOCK_EX ); + $this->doSingleUnlock( $path, self::LOCK_SH ); + } } } } diff --git a/includes/filebackend/lockmanager/LSLockManager.php b/includes/filebackend/lockmanager/LSLockManager.php new file mode 100644 index 00000000..89428182 --- /dev/null +++ b/includes/filebackend/lockmanager/LSLockManager.php @@ -0,0 +1,218 @@ +<?php +/** + * Version of LockManager based on using lock daemon servers. + * + * 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 LockManager + */ + +/** + * Manage locks using a lock daemon server. + * + * Version of LockManager based on using lock daemon servers. + * This is meant for multi-wiki systems that may share files. + * All locks are non-blocking, which avoids deadlocks. + * + * All lock requests for a resource, identified by a hash string, will map + * to one bucket. Each bucket maps to one or several peer servers, each + * running LockServerDaemon.php, listening on a designated TCP port. + * A majority of peers must agree for a lock to be acquired. + * + * @ingroup LockManager + * @since 1.19 + */ +class LSLockManager extends QuorumLockManager { + /** @var Array Mapping of lock types to the type actually used */ + protected $lockTypeMap = array( + self::LOCK_SH => self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ); + + /** @var Array Map of server names to server config */ + protected $lockServers; // (server name => server config array) + + /** @var Array Map Server connections (server name => resource) */ + protected $conns = array(); + + protected $connTimeout; // float number of seconds + protected $session = ''; // random SHA-1 string + + /** + * Construct a new instance from configuration. + * + * $config paramaters include: + * - lockServers : Associative array of server names to configuration. + * Configuration is an associative array that includes: + * - host : IP address/hostname + * - port : TCP port + * - authKey : Secret string the lock server uses + * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0, + * each having an odd-numbered list of server names (peers) as values. + * - connTimeout : Lock server connection attempt timeout. [optional] + * + * @param Array $config + */ + public function __construct( array $config ) { + parent::__construct( $config ); + + $this->lockServers = $config['lockServers']; + // Sanitize srvsByBucket config to prevent PHP errors + $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' ); + $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive + + if ( isset( $config['connTimeout'] ) ) { + $this->connTimeout = $config['connTimeout']; + } else { + $this->connTimeout = 3; // use some sane amount + } + + $this->session = wfRandomString( 32 ); // 128 bits + } + + /** + * @see QuorumLockManager::getLocksOnServer() + * @return Status + */ + protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + // Send out the command and get the response... + $type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX'; + $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); + $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys ); + + if ( $response !== 'ACQUIRED' ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } + + return $status; + } + + /** + * @see QuorumLockManager::freeLocksOnServer() + * @return Status + */ + protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + // Send out the command and get the response... + $type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX'; + $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); + $response = $this->sendCommand( $lockSrv, 'RELEASE', $type, $keys ); + + if ( $response !== 'RELEASED' ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } + } + + return $status; + } + + /** + * @see QuorumLockManager::releaseAllLocks() + * @return Status + */ + protected function releaseAllLocks() { + $status = Status::newGood(); + + foreach ( $this->conns as $lockSrv => $conn ) { + $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() ); + if ( $response !== 'RELEASED_ALL' ) { + $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); + } + } + + return $status; + } + + /** + * @see QuorumLockManager::isServerUp() + * @return bool + */ + protected function isServerUp( $lockSrv ) { + return (bool)$this->getConnection( $lockSrv ); + } + + /** + * Send a command and get back the response + * + * @param $lockSrv string + * @param $action string + * @param $type string + * @param $values Array + * @return string|bool + */ + protected function sendCommand( $lockSrv, $action, $type, $values ) { + $conn = $this->getConnection( $lockSrv ); + if ( !$conn ) { + return false; // no connection + } + $authKey = $this->lockServers[$lockSrv]['authKey']; + // Build of the command as a flat string... + $values = implode( '|', $values ); + $key = sha1( $this->session . $action . $type . $values . $authKey ); + // Send out the command... + if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) { + return false; + } + // Get the response... + $response = fgets( $conn ); + if ( $response === false ) { + return false; + } + return trim( $response ); + } + + /** + * Get (or reuse) a connection to a lock server + * + * @param $lockSrv string + * @return resource + */ + protected function getConnection( $lockSrv ) { + if ( !isset( $this->conns[$lockSrv] ) ) { + $cfg = $this->lockServers[$lockSrv]; + wfSuppressWarnings(); + $errno = $errstr = ''; + $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout ); + wfRestoreWarnings(); + if ( $conn === false ) { + return null; + } + $sec = floor( $this->connTimeout ); + $usec = floor( ( $this->connTimeout - floor( $this->connTimeout ) ) * 1e6 ); + stream_set_timeout( $conn, $sec, $usec ); + $this->conns[$lockSrv] = $conn; + } + return $this->conns[$lockSrv]; + } + + /** + * Make sure remaining locks get cleared for sanity + */ + function __destruct() { + $this->releaseAllLocks(); + foreach ( $this->conns as $conn ) { + fclose( $conn ); + } + } +} diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php new file mode 100644 index 00000000..07853f87 --- /dev/null +++ b/includes/filebackend/lockmanager/LockManager.php @@ -0,0 +1,425 @@ +<?php +/** + * @defgroup LockManager Lock management + * @ingroup FileBackend + */ + +/** + * Resource locking handling. + * + * 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 LockManager + * @author Aaron Schulz + */ + +/** + * @brief Class for handling resource locking. + * + * Locks on resource keys can either be shared or exclusive. + * + * Implementations must keep track of what is locked by this proccess + * in-memory and support nested locking calls (using reference counting). + * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op. + * Locks should either be non-blocking or have low wait timeouts. + * + * Subclasses should avoid throwing exceptions at all costs. + * + * @ingroup LockManager + * @since 1.19 + */ +abstract class LockManager { + /** @var Array Mapping of lock types to the type actually used */ + protected $lockTypeMap = array( + self::LOCK_SH => self::LOCK_SH, + self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH + self::LOCK_EX => self::LOCK_EX + ); + + /** @var Array Map of (resource path => lock type => count) */ + protected $locksHeld = array(); + + /* Lock types; stronger locks have higher values */ + const LOCK_SH = 1; // shared lock (for reads) + const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) + const LOCK_EX = 3; // exclusive lock (for writes) + + /** + * Construct a new instance from configuration + * + * @param $config Array + */ + public function __construct( array $config ) {} + + /** + * Lock the resources at the given abstract paths + * + * @param $paths Array List of resource names + * @param $type integer LockManager::LOCK_* constant + * @return Status + */ + final public function lock( array $paths, $type = self::LOCK_EX ) { + wfProfileIn( __METHOD__ ); + $status = $this->doLock( array_unique( $paths ), $this->lockTypeMap[$type] ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * Unlock the resources at the given abstract paths + * + * @param $paths Array List of storage paths + * @param $type integer LockManager::LOCK_* constant + * @return Status + */ + final public function unlock( array $paths, $type = self::LOCK_EX ) { + wfProfileIn( __METHOD__ ); + $status = $this->doUnlock( array_unique( $paths ), $this->lockTypeMap[$type] ); + wfProfileOut( __METHOD__ ); + return $status; + } + + /** + * Get the base 36 SHA-1 of a string, padded to 31 digits + * + * @param $path string + * @return string + */ + final protected static function sha1Base36( $path ) { + return wfBaseConvert( sha1( $path ), 16, 36, 31 ); + } + + /** + * Lock resources with the given keys and lock type + * + * @param $paths Array List of storage paths + * @param $type integer LockManager::LOCK_* constant + * @return string + */ + abstract protected function doLock( array $paths, $type ); + + /** + * Unlock resources with the given keys and lock type + * + * @param $paths Array List of storage paths + * @param $type integer LockManager::LOCK_* constant + * @return string + */ + abstract protected function doUnlock( array $paths, $type ); +} + +/** + * Self-releasing locks + * + * LockManager helper class to handle scoped locks, which + * release when an object is destroyed or goes out of scope. + * + * @ingroup LockManager + * @since 1.19 + */ +class ScopedLock { + /** @var LockManager */ + protected $manager; + /** @var Status */ + protected $status; + /** @var Array List of resource paths*/ + protected $paths; + + protected $type; // integer lock type + + /** + * @param $manager LockManager + * @param $paths Array List of storage paths + * @param $type integer LockManager::LOCK_* constant + * @param $status Status + */ + protected function __construct( + LockManager $manager, array $paths, $type, Status $status + ) { + $this->manager = $manager; + $this->paths = $paths; + $this->status = $status; + $this->type = $type; + } + + /** + * Get a ScopedLock object representing a lock on resource paths. + * Any locks are released once this object goes out of scope. + * The status object is updated with any errors or warnings. + * + * @param $manager LockManager + * @param $paths Array List of storage paths + * @param $type integer LockManager::LOCK_* constant + * @param $status Status + * @return ScopedLock|null Returns null on failure + */ + public static function factory( + LockManager $manager, array $paths, $type, Status $status + ) { + $lockStatus = $manager->lock( $paths, $type ); + $status->merge( $lockStatus ); + if ( $lockStatus->isOK() ) { + return new self( $manager, $paths, $type, $status ); + } + return null; + } + + function __destruct() { + $wasOk = $this->status->isOK(); + $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) ); + if ( $wasOk ) { + // Make sure status is OK, despite any unlockFiles() fatals + $this->status->setResult( true, $this->status->value ); + } + } +} + +/** + * Version of LockManager that uses a quorum from peer servers for locks. + * The resource space can also be sharded into separate peer groups. + * + * @ingroup LockManager + * @since 1.20 + */ +abstract class QuorumLockManager extends LockManager { + /** @var Array Map of bucket indexes to peer server lists */ + protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...)) + + /** + * @see LockManager::doLock() + * @param $paths array + * @param $type int + * @return Status + */ + final protected function doLock( array $paths, $type ) { + $status = Status::newGood(); + + $pathsToLock = array(); // (bucket => paths) + // Get locks that need to be acquired (buckets => locks)... + foreach ( $paths as $path ) { + if ( isset( $this->locksHeld[$path][$type] ) ) { + ++$this->locksHeld[$path][$type]; + } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { + $this->locksHeld[$path][$type] = 1; + } else { + $bucket = $this->getBucketFromKey( $path ); + $pathsToLock[$bucket][] = $path; + } + } + + $lockedPaths = array(); // files locked in this attempt + // Attempt to acquire these locks... + foreach ( $pathsToLock as $bucket => $paths ) { + // Try to acquire the locks for this bucket + $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) ); + if ( !$status->isOK() ) { + $status->merge( $this->doUnlock( $lockedPaths, $type ) ); + return $status; + } + // Record these locks as active + foreach ( $paths as $path ) { + $this->locksHeld[$path][$type] = 1; // locked + } + // Keep track of what locks were made in this attempt + $lockedPaths = array_merge( $lockedPaths, $paths ); + } + + return $status; + } + + /** + * @see LockManager::doUnlock() + * @param $paths array + * @param $type int + * @return Status + */ + final protected function doUnlock( array $paths, $type ) { + $status = Status::newGood(); + + $pathsToUnlock = array(); + foreach ( $paths as $path ) { + if ( !isset( $this->locksHeld[$path][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } else { + --$this->locksHeld[$path][$type]; + // Reference count the locks held and release locks when zero + if ( $this->locksHeld[$path][$type] <= 0 ) { + unset( $this->locksHeld[$path][$type] ); + $bucket = $this->getBucketFromKey( $path ); + $pathsToUnlock[$bucket][] = $path; + } + if ( !count( $this->locksHeld[$path] ) ) { + unset( $this->locksHeld[$path] ); // no SH or EX locks left for key + } + } + } + + // Remove these specific locks if possible, or at least release + // all locks once this process is currently not holding any locks. + foreach ( $pathsToUnlock as $bucket => $paths ) { + $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) ); + } + if ( !count( $this->locksHeld ) ) { + $status->merge( $this->releaseAllLocks() ); + } + + return $status; + } + + /** + * Attempt to acquire locks with the peers for a bucket. + * This is all or nothing; if any key is locked then this totally fails. + * + * @param $bucket integer + * @param $paths Array List of resource keys to lock + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH + * @return Status + */ + final protected function doLockingRequestBucket( $bucket, array $paths, $type ) { + $status = Status::newGood(); + + $yesVotes = 0; // locks made on trustable servers + $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers + $quorum = floor( $votesLeft/2 + 1 ); // simple majority + // Get votes for each peer, in order, until we have enough... + foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { + if ( !$this->isServerUp( $lockSrv ) ) { + --$votesLeft; + $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv ); + continue; // server down? + } + // Attempt to acquire the lock on this peer + $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) ); + if ( !$status->isOK() ) { + return $status; // vetoed; resource locked + } + ++$yesVotes; // success for this peer + if ( $yesVotes >= $quorum ) { + return $status; // lock obtained + } + --$votesLeft; + $votesNeeded = $quorum - $yesVotes; + if ( $votesNeeded > $votesLeft ) { + break; // short-circuit + } + } + // At this point, we must not have met the quorum + $status->setResult( false ); + + return $status; + } + + /** + * Attempt to release locks with the peers for a bucket + * + * @param $bucket integer + * @param $paths Array List of resource keys to lock + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH + * @return Status + */ + final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) { + $status = Status::newGood(); + + foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { + if ( !$this->isServerUp( $lockSrv ) ) { + $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); + // Attempt to release the lock on this peer + } else { + $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) ); + } + } + + return $status; + } + + /** + * Get the bucket for resource path. + * This should avoid throwing any exceptions. + * + * @param $path string + * @return integer + */ + protected function getBucketFromKey( $path ) { + $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) + return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket ); + } + + /** + * Check if a lock server is up + * + * @param $lockSrv string + * @return bool + */ + abstract protected function isServerUp( $lockSrv ); + + /** + * Get a connection to a lock server and acquire locks on $paths + * + * @param $lockSrv string + * @param $paths array + * @param $type integer + * @return Status + */ + abstract protected function getLocksOnServer( $lockSrv, array $paths, $type ); + + /** + * Get a connection to a lock server and release locks on $paths. + * + * Subclasses must effectively implement this or releaseAllLocks(). + * + * @param $lockSrv string + * @param $paths array + * @param $type integer + * @return Status + */ + abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type ); + + /** + * Release all locks that this session is holding. + * + * Subclasses must effectively implement this or freeLocksOnServer(). + * + * @return Status + */ + abstract protected function releaseAllLocks(); +} + +/** + * Simple version of LockManager that does nothing + * @since 1.19 + */ +class NullLockManager extends LockManager { + /** + * @see LockManager::doLock() + * @param $paths array + * @param $type int + * @return Status + */ + protected function doLock( array $paths, $type ) { + return Status::newGood(); + } + + /** + * @see LockManager::doUnlock() + * @param $paths array + * @param $type int + * @return Status + */ + protected function doUnlock( array $paths, $type ) { + return Status::newGood(); + } +} diff --git a/includes/filerepo/backend/lockmanager/LockManagerGroup.php b/includes/filebackend/lockmanager/LockManagerGroup.php index 11e77972..8c8c940a 100644 --- a/includes/filerepo/backend/lockmanager/LockManagerGroup.php +++ b/includes/filebackend/lockmanager/LockManagerGroup.php @@ -1,13 +1,34 @@ <?php /** + * Lock manager registration handling. + * + * 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 LockManager + */ + +/** * Class to handle file lock manager registration - * + * * @ingroup LockManager * @author Aaron Schulz * @since 1.19 */ class LockManagerGroup { - /** * @var LockManagerGroup */ @@ -17,7 +38,6 @@ class LockManagerGroup { protected $managers = array(); protected function __construct() {} - protected function __clone() {} /** * @return LockManagerGroup @@ -31,8 +51,16 @@ class LockManagerGroup { } /** + * Destroy the singleton instance, so that a new one will be created next + * time singleton() is called. + */ + public static function destroySingleton() { + self::$instance = null; + } + + /** * Register lock managers from the global variables - * + * * @return void */ protected function initFromGlobals() { @@ -86,4 +114,30 @@ class LockManagerGroup { } return $this->managers[$name]['instance']; } + + /** + * Get the default lock manager configured for the site. + * Returns NullLockManager if no lock manager could be found. + * + * @return LockManager + */ + public function getDefault() { + return isset( $this->managers['default'] ) + ? $this->get( 'default' ) + : new NullLockManager( array() ); + } + + /** + * Get the default lock manager configured for the site + * or at least some other effective configured lock manager. + * Throws an exception if no lock manager could be found. + * + * @return LockManager + * @throws MWException + */ + public function getAny() { + return isset( $this->managers['default'] ) + ? $this->get( 'default' ) + : $this->get( 'fsLockManager' ); + } } diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php new file mode 100644 index 00000000..57c0463d --- /dev/null +++ b/includes/filebackend/lockmanager/MemcLockManager.php @@ -0,0 +1,319 @@ +<?php +/** + * Version of LockManager based on using memcached servers. + * + * 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 LockManager + */ + +/** + * Manage locks using memcached servers. + * + * Version of LockManager based on using memcached servers. + * This is meant for multi-wiki systems that may share files. + * All locks are non-blocking, which avoids deadlocks. + * + * All lock requests for a resource, identified by a hash string, will map + * to one bucket. Each bucket maps to one or several peer servers, each running memcached. + * A majority of peers must agree for a lock to be acquired. + * + * @ingroup LockManager + * @since 1.20 + */ +class MemcLockManager extends QuorumLockManager { + /** @var Array Mapping of lock types to the type actually used */ + protected $lockTypeMap = array( + self::LOCK_SH => self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ); + + /** @var Array Map server names to MemcachedBagOStuff objects */ + protected $bagOStuffs = array(); + /** @var Array */ + protected $serversUp = array(); // (server name => bool) + + protected $lockExpiry; // integer; maximum time locks can be held + protected $session = ''; // string; random SHA-1 UUID + protected $wikiId = ''; // string + + /** + * Construct a new instance from configuration. + * + * $config paramaters include: + * - lockServers : Associative array of server names to "<IP>:<port>" strings. + * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0, + * each having an odd-numbered list of server names (peers) as values. + * - memcConfig : Configuration array for ObjectCache::newFromParams. [optional] + * If set, this must use one of the memcached classes. + * - wikiId : Wiki ID string that all resources are relative to. [optional] + * + * @param Array $config + */ + public function __construct( array $config ) { + parent::__construct( $config ); + + // Sanitize srvsByBucket config to prevent PHP errors + $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' ); + $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive + + $memcConfig = isset( $config['memcConfig'] ) + ? $config['memcConfig'] + : array( 'class' => 'MemcachedPhpBagOStuff' ); + + foreach ( $config['lockServers'] as $name => $address ) { + $params = array( 'servers' => array( $address ) ) + $memcConfig; + $cache = ObjectCache::newFromParams( $params ); + if ( $cache instanceof MemcachedBagOStuff ) { + $this->bagOStuffs[$name] = $cache; + } else { + throw new MWException( + 'Only MemcachedBagOStuff classes are supported by MemcLockManager.' ); + } + } + + $this->wikiId = isset( $config['wikiId'] ) ? $config['wikiId'] : wfWikiID(); + + $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode + $this->lockExpiry = $met ? 2*(int)$met : 2*3600; + + $this->session = wfRandomString( 32 ); + } + + /** + * @see QuorumLockManager::getLocksOnServer() + * @return Status + */ + protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + $memc = $this->getCache( $lockSrv ); + $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records + + // Lock all of the active lock record keys... + if ( !$this->acquireMutexes( $memc, $keys ) ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + return; + } + + // Fetch all the existing lock records... + $lockRecords = $memc->getMulti( $keys ); + + $now = time(); + // Check if the requested locks conflict with existing ones... + foreach ( $paths as $path ) { + $locksKey = $this->recordKeyForPath( $path ); + $locksHeld = isset( $lockRecords[$locksKey] ) + ? $lockRecords[$locksKey] + : array( self::LOCK_SH => array(), self::LOCK_EX => array() ); // init + foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) { + if ( $expiry < $now ) { // stale? + unset( $locksHeld[self::LOCK_EX][$session] ); + } elseif ( $session !== $this->session ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } + if ( $type === self::LOCK_EX ) { + foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) { + if ( $expiry < $now ) { // stale? + unset( $locksHeld[self::LOCK_SH][$session] ); + } elseif ( $session !== $this->session ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } + } + if ( $status->isOK() ) { + // Register the session in the lock record array + $locksHeld[$type][$this->session] = $now + $this->lockExpiry; + // We will update this record if none of the other locks conflict + $lockRecords[$locksKey] = $locksHeld; + } + } + + // If there were no lock conflicts, update all the lock records... + if ( $status->isOK() ) { + foreach ( $lockRecords as $locksKey => $locksHeld ) { + $memc->set( $locksKey, $locksHeld ); + wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" ); + } + } + + // Unlock all of the active lock record keys... + $this->releaseMutexes( $memc, $keys ); + + return $status; + } + + /** + * @see QuorumLockManager::freeLocksOnServer() + * @return Status + */ + protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + $memc = $this->getCache( $lockSrv ); + $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records + + // Lock all of the active lock record keys... + if ( !$this->acquireMutexes( $memc, $keys ) ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } + return; + } + + // Fetch all the existing lock records... + $lockRecords = $memc->getMulti( $keys ); + + // Remove the requested locks from all records... + foreach ( $paths as $path ) { + $locksKey = $this->recordKeyForPath( $path ); // lock record + if ( !isset( $lockRecords[$locksKey] ) ) { + continue; // nothing to do + } + $locksHeld = $lockRecords[$locksKey]; + if ( is_array( $locksHeld ) && isset( $locksHeld[$type] ) ) { + unset( $locksHeld[$type][$this->session] ); + $ok = $memc->set( $locksKey, $locksHeld ); + } else { + $ok = true; + } + if ( !$ok ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } + wfDebug( __METHOD__ . ": released lock on key $locksKey.\n" ); + } + + // Unlock all of the active lock record keys... + $this->releaseMutexes( $memc, $keys ); + + return $status; + } + + /** + * @see QuorumLockManager::releaseAllLocks() + * @return Status + */ + protected function releaseAllLocks() { + return Status::newGood(); // not supported + } + + /** + * @see QuorumLockManager::isServerUp() + * @return bool + */ + protected function isServerUp( $lockSrv ) { + return (bool)$this->getCache( $lockSrv ); + } + + /** + * Get the MemcachedBagOStuff object for a $lockSrv + * + * @param $lockSrv string Server name + * @return MemcachedBagOStuff|null + */ + protected function getCache( $lockSrv ) { + $memc = null; + if ( isset( $this->bagOStuffs[$lockSrv] ) ) { + $memc = $this->bagOStuffs[$lockSrv]; + if ( !isset( $this->serversUp[$lockSrv] ) ) { + $this->serversUp[$lockSrv] = $memc->set( 'MemcLockManager:ping', 1, 1 ); + if ( !$this->serversUp[$lockSrv] ) { + trigger_error( __METHOD__ . ": Could not contact $lockSrv.", E_USER_WARNING ); + } + } + if ( !$this->serversUp[$lockSrv] ) { + return null; // server appears to be down + } + } + return $memc; + } + + /** + * @param $path string + * @return string + */ + protected function recordKeyForPath( $path ) { + $hash = LockManager::sha1Base36( $path ); + list( $db, $prefix ) = wfSplitWikiID( $this->wikiId ); + return wfForeignMemcKey( $db, $prefix, __CLASS__, 'locks', $hash ); + } + + /** + * @param $memc MemcachedBagOStuff + * @param $keys Array List of keys to acquire + * @return bool + */ + protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) { + $lockedKeys = array(); + + // Acquire the keys in lexicographical order, to avoid deadlock problems. + // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has. + sort( $keys ); + + // Try to quickly loop to acquire the keys, but back off after a few rounds. + // This reduces memcached spam, especially in the rare case where a server acquires + // some lock keys and dies without releasing them. Lock keys expire after a few minutes. + $rounds = 0; + $start = microtime( true ); + do { + if ( ( ++$rounds % 4 ) == 0 ) { + usleep( 1000*50 ); // 50 ms + } + foreach ( array_diff( $keys, $lockedKeys ) as $key ) { + if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record + $lockedKeys[] = $key; + } else { + continue; // acquire in order + } + } + } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 6 ); + + if ( count( $lockedKeys ) != count( $keys ) ) { + $this->releaseMutexes( $lockedKeys ); // failed; release what was locked + return false; + } + + return true; + } + + /** + * @param $memc MemcachedBagOStuff + * @param $keys Array List of acquired keys + * @return void + */ + protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) { + foreach ( $keys as $key ) { + $memc->delete( "$key:mutex" ); + } + } + + /** + * Make sure remaining locks get cleared for sanity + */ + function __destruct() { + while ( count( $this->locksHeld ) ) { + foreach ( $this->locksHeld as $path => $locks ) { + $this->doUnlock( array( $path ), self::LOCK_EX ); + $this->doUnlock( array( $path ), self::LOCK_SH ); + } + } + } +} diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index 22dbdefc..9c8d85dc 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -2,6 +2,21 @@ /** * A repository for files accessible via the local filesystem. * + * 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 FileRepo */ @@ -16,6 +31,11 @@ * @deprecated since 1.19 */ class FSRepo extends FileRepo { + + /** + * @param $info array + * @throws MWException + */ function __construct( array $info ) { if ( !isset( $info['backend'] ) ) { // B/C settings... diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index 8d4f2bd9..a31b148a 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -10,6 +10,21 @@ /** * Base code for file repositories. * + * 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 FileRepo */ @@ -20,8 +35,6 @@ * @ingroup FileRepo */ class FileRepo { - const FILES_ONLY = 1; - const DELETE_SOURCE = 1; const OVERWRITE = 2; const OVERWRITE_SAME = 4; @@ -38,6 +51,7 @@ class FileRepo { var $pathDisclosureProtection = 'simple'; // 'paranoid' var $descriptionCacheExpiry, $url, $thumbUrl; var $hashLevels, $deletedHashLevels; + protected $abbrvThreshold; /** * Factory functions for creating new files @@ -47,7 +61,11 @@ class FileRepo { var $oldFileFactory = false; var $fileFactoryKey = false, $oldFileFactoryKey = false; - function __construct( Array $info = null ) { + /** + * @param $info array|null + * @throws MWException + */ + public function __construct( array $info = null ) { // Verify required settings presence if( $info === null @@ -96,22 +114,24 @@ class FileRepo { ? $info['deletedHashLevels'] : $this->hashLevels; $this->transformVia404 = !empty( $info['transformVia404'] ); - $this->zones = isset( $info['zones'] ) - ? $info['zones'] - : array(); + $this->abbrvThreshold = isset( $info['abbrvThreshold'] ) + ? $info['abbrvThreshold'] + : 255; + $this->isPrivate = !empty( $info['isPrivate'] ); // Give defaults for the basic zones... + $this->zones = isset( $info['zones'] ) ? $info['zones'] : array(); foreach ( array( 'public', 'thumb', 'temp', 'deleted' ) as $zone ) { - if ( !isset( $this->zones[$zone] ) ) { - $this->zones[$zone] = array( - 'container' => "{$this->name}-{$zone}", - 'directory' => '' // container root - ); + if ( !isset( $this->zones[$zone]['container'] ) ) { + $this->zones[$zone]['container'] = "{$this->name}-{$zone}"; + } + if ( !isset( $this->zones[$zone]['directory'] ) ) { + $this->zones[$zone]['directory'] = ''; } } } /** - * Get the file backend instance + * Get the file backend instance. Use this function wisely. * * @return FileBackend */ @@ -120,10 +140,20 @@ class FileRepo { } /** - * Prepare a single zone or list of zones for usage. - * See initDeletedDir() for additional setup needed for the 'deleted' zone. - * + * Get an explanatory message if this repo is read-only. + * This checks if an administrator disabled writes to the backend. + * + * @return string|bool Returns false if the repo is not read-only + */ + public function getReadOnlyReason() { + return $this->backend->getReadOnlyReason(); + } + + /** + * Check if a single zone or list of zones is defined for usage + * * @param $doZones Array Only do a particular zones + * @throws MWException * @return Status */ protected function initZones( $doZones = array() ) { @@ -138,18 +168,6 @@ class FileRepo { } /** - * Take all available measures to prevent web accessibility of new deleted - * directories, in case the user has not configured offline storage - * - * @param $dir string - * @return void - */ - protected function initDeletedDir( $dir ) { - $this->backend->secure( // prevent web access & dir listings - array( 'dir' => $dir, 'noAccess' => true, 'noListing' => true ) ); - } - - /** * Determine if a string is an mwrepo:// URL * * @param $url string @@ -164,7 +182,7 @@ class FileRepo { * The suffix, if supplied, is considered to be unencoded, and will be * URL-encoded before being returned. * - * @param $suffix string + * @param $suffix string|bool * @return string */ public function getVirtualUrl( $suffix = false ) { @@ -182,6 +200,11 @@ class FileRepo { * @return String or false */ public function getZoneUrl( $zone ) { + if ( isset( $this->zones[$zone]['url'] ) + && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) ) + { + return $this->zones[$zone]['url']; // custom URL + } switch ( $zone ) { case 'public': return $this->url; @@ -197,12 +220,36 @@ class FileRepo { } /** - * Get the backend storage path corresponding to a virtual URL + * Get the thumb zone URL configured to be handled by scripts like thumb_handler.php. + * This is probably only useful for internal requests, such as from a fast frontend server + * to a slower backend server. + * + * Large sites may use a different host name for uploads than for wikis. In any case, the + * wiki configuration is needed in order to use thumb.php. To avoid extracting the wiki ID + * from the URL path, one can configure thumb_handler.php to recognize a special path on the + * same host name as the wiki that is used for viewing thumbnails. + * + * @param $zone String: one of: public, deleted, temp, thumb + * @return String or false + */ + public function getZoneHandlerUrl( $zone ) { + if ( isset( $this->zones[$zone]['handlerUrl'] ) + && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) ) + { + return $this->zones[$zone]['handlerUrl']; + } + return false; + } + + /** + * Get the backend storage path corresponding to a virtual URL. + * Use this function wisely. * * @param $url string + * @throws MWException * @return string */ - function resolveVirtualUrl( $url ) { + public function resolveVirtualUrl( $url ) { if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { throw new MWException( __METHOD__.': unknown protocol' ); } @@ -223,7 +270,7 @@ class FileRepo { /** * The the storage container and base path of a zone - * + * * @param $zone string * @return Array (container, base path) or (null, null) */ @@ -238,7 +285,7 @@ class FileRepo { * Get the storage path corresponding to one of the zones * * @param $zone string - * @return string|null + * @return string|null Returns null if the zone is not defined */ public function getZonePath( $zone ) { list( $container, $base ) = $this->getZoneLocation( $zone ); @@ -286,16 +333,16 @@ class FileRepo { * * @param $title Mixed: Title object or string * @param $options array Associative array of options: - * time: requested time for an archived image, or false for the + * time: requested time for a specific file version, or false for the * current version. An image object will be returned which was - * created at the specified time. + * created at the specified time (which may be archived or current). * * ignoreRedirect: If true, do not follow file redirects * * private: If true, return restricted (deleted) files if the current * user is allowed to view them. Otherwise, such files will not * be found. - * @return File|false + * @return File|bool False on failure */ public function findFile( $title, $options = array() ) { $title = File::normalizeTitle( $title ); @@ -344,7 +391,7 @@ class FileRepo { /** * Find many files at once. * - * @param $items An array of titles, or an array of findFile() options with + * @param $items array An array of titles, or an array of findFile() options with * the "title" option giving the title. Example: * * $findItem = array( 'title' => $title, 'private' => true ); @@ -352,7 +399,7 @@ class FileRepo { * $repo->findFiles( $findBatch ); * @return array */ - public function findFiles( $items ) { + public function findFiles( array $items ) { $result = array(); foreach ( $items as $item ) { if ( is_array( $item ) ) { @@ -377,12 +424,11 @@ class FileRepo { * version control should return false if the time is specified. * * @param $sha1 String base 36 SHA-1 hash - * @param $options Option array, same as findFile(). - * @return File|false + * @param $options array Option array, same as findFile(). + * @return File|bool False on failure */ public function findFileFromKey( $sha1, $options = array() ) { $time = isset( $options['time'] ) ? $options['time'] : false; - # First try to find a matching current version of a file... if ( $this->fileFactoryKey ) { $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time ); @@ -411,27 +457,39 @@ class FileRepo { * SHA-1 content hash. * * STUB + * @param $hash + * @return array */ public function findBySha1( $hash ) { return array(); } /** - * Get the public root URL of the repository + * Get an array of arrays or iterators of file objects for files that + * have the given SHA-1 content hashes. * - * @return string|false + * @param $hashes array An array of hashes + * @return array An Array of arrays or iterators of file objects and the hash as key */ - public function getRootUrl() { - return $this->url; + public function findBySha1s( array $hashes ) { + $result = array(); + foreach ( $hashes as $hash ) { + $files = $this->findBySha1( $hash ); + if ( count( $files ) ) { + $result[$hash] = $files; + } + } + return $result; } /** - * Returns true if the repository uses a multi-level directory structure + * Get the public root URL of the repository * + * @deprecated since 1.20 * @return string */ - public function isHashed() { - return (bool)$this->hashLevels; + public function getRootUrl() { + return $this->getZoneUrl( 'public' ); } /** @@ -456,6 +514,7 @@ class FileRepo { * Get the name of an image from its title object * * @param $title Title + * @return String */ public function getNameFromTitle( Title $title ) { global $wgContLang; @@ -483,7 +542,7 @@ class FileRepo { * Get a relative path including trailing slash, e.g. f/fa/ * If the repo is not hashed, returns an empty string * - * @param $name string + * @param $name string Name of file * @return string */ public function getHashPath( $name ) { @@ -491,11 +550,24 @@ class FileRepo { } /** + * Get a relative path including trailing slash, e.g. f/fa/ + * If the repo is not hashed, returns an empty string + * + * @param $suffix string Basename of file from FileRepo::storeTemp() + * @return string + */ + public function getTempHashPath( $suffix ) { + $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name> + $name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp + return self::getHashPathForLevel( $name, $this->hashLevels ); + } + + /** * @param $name * @param $levels * @return string */ - static function getHashPathForLevel( $name, $levels ) { + protected static function getHashPathForLevel( $name, $levels ) { if ( $levels == 0 ) { return ''; } else { @@ -531,7 +603,7 @@ class FileRepo { * * @param $query mixed Query string to append * @param $entry string Entry point; defaults to index - * @return string|false + * @return string|bool False on failure */ public function makeUrl( $query = '', $entry = 'index' ) { if ( isset( $this->scriptDirUrl ) ) { @@ -611,7 +683,7 @@ class FileRepo { /** * Get the URL of the stylesheet to apply to description pages * - * @return string|false + * @return string|bool False on failure */ public function getDescriptionStylesheetUrl() { if ( isset( $this->scriptDirUrl ) ) { @@ -624,7 +696,7 @@ class FileRepo { /** * Store a file to a given destination. * - * @param $srcPath String: source FS path, storage path, or virtual URL + * @param $srcPath String: source file system path, storage path, or virtual URL * @param $dstZone String: destination zone * @param $dstRel String: destination relative path * @param $flags Integer: bitwise combination of the following flags: @@ -636,10 +708,13 @@ class FileRepo { * @return FileRepoStatus */ public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { + $this->assertWritableRepo(); // fail out if read-only + $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags ); if ( $status->successCount == 0 ) { $status->ok = false; } + return $status; } @@ -653,12 +728,14 @@ class FileRepo { * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the * same contents as the source * self::SKIP_LOCKING Skip any file locking when doing the store + * @throws MWException * @return FileRepoStatus */ - public function storeBatch( $triplets, $flags = 0 ) { - $backend = $this->backend; // convenience + public function storeBatch( array $triplets, $flags = 0 ) { + $this->assertWritableRepo(); // fail out if read-only $status = $this->newGood(); + $backend = $this->backend; // convenience $operations = array(); $sourceFSFilesToDelete = array(); // cleanup for disk source files @@ -680,18 +757,12 @@ class FileRepo { $dstPath = "$root/$dstRel"; $dstDir = dirname( $dstPath ); // Create destination directories for this triplet - if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) { + if ( !$this->initDirectory( $dstDir )->isOK() ) { return $this->newFatal( 'directorycreateerror', $dstDir ); } - if ( $dstZone == 'deleted' ) { - $this->initDeletedDir( $dstDir ); - } - // Resolve source to a storage path if virtual - if ( self::isVirtualUrl( $srcPath ) ) { - $srcPath = $this->resolveVirtualUrl( $srcPath ); - } + $srcPath = $this->resolveToStoragePath( $srcPath ); // Get the appropriate file operation if ( FileBackend::isStoragePath( $srcPath ) ) { @@ -729,54 +800,136 @@ class FileRepo { /** * Deletes a batch of files. - * Each file can be a (zone, rel) pair, virtual url, storage path, or FS path. + * Each file can be a (zone, rel) pair, virtual url, storage path. * It will try to delete each file, but ignores any errors that may occur. * - * @param $pairs array List of files to delete + * @param $files array List of files to delete * @param $flags Integer: bitwise combination of the following flags: * self::SKIP_LOCKING Skip any file locking when doing the deletions - * @return void + * @return FileRepoStatus */ - public function cleanupBatch( $files, $flags = 0 ) { + public function cleanupBatch( array $files, $flags = 0 ) { + $this->assertWritableRepo(); // fail out if read-only + + $status = $this->newGood(); + $operations = array(); - $sourceFSFilesToDelete = array(); // cleanup for disk source files - foreach ( $files as $file ) { - if ( is_array( $file ) ) { + foreach ( $files as $path ) { + if ( is_array( $path ) ) { // This is a pair, extract it - list( $zone, $rel ) = $file; - $root = $this->getZonePath( $zone ); - $path = "$root/$rel"; + list( $zone, $rel ) = $path; + $path = $this->getZonePath( $zone ) . "/$rel"; } else { - if ( self::isVirtualUrl( $file ) ) { - // This is a virtual url, resolve it - $path = $this->resolveVirtualUrl( $file ); - } else { - // This is a full file name - $path = $file; - } - } - // Get a file operation if needed - if ( FileBackend::isStoragePath( $path ) ) { - $operations[] = array( - 'op' => 'delete', - 'src' => $path, - ); - } else { - $sourceFSFilesToDelete[] = $path; + // Resolve source to a storage path if virtual + $path = $this->resolveToStoragePath( $path ); } + $operations[] = array( 'op' => 'delete', 'src' => $path ); } // Actually delete files from storage... $opts = array( 'force' => true ); if ( $flags & self::SKIP_LOCKING ) { $opts['nonLocking'] = true; } - $this->backend->doOperations( $operations, $opts ); - // Cleanup for disk source files... - foreach ( $sourceFSFilesToDelete as $file ) { - wfSuppressWarnings(); - unlink( $file ); // FS cleanup - wfRestoreWarnings(); + $status->merge( $this->backend->doOperations( $operations, $opts ) ); + + return $status; + } + + /** + * Import a file from the local file system into the repo. + * This does no locking nor journaling and overrides existing files. + * This function can be used to write to otherwise read-only foreign repos. + * This is intended for copying generated thumbnails into the repo. + * + * @param $src string Source file system path, storage path, or virtual URL + * @param $dst string Virtual URL or storage path + * @param $disposition string|null Content-Disposition if given and supported + * @return FileRepoStatus + */ + final public function quickImport( $src, $dst, $disposition = null ) { + return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) ); + } + + /** + * Purge a file from the repo. This does no locking nor journaling. + * This function can be used to write to otherwise read-only foreign repos. + * This is intended for purging thumbnails. + * + * @param $path string Virtual URL or storage path + * @return FileRepoStatus + */ + final public function quickPurge( $path ) { + return $this->quickPurgeBatch( array( $path ) ); + } + + /** + * Deletes a directory if empty. + * This function can be used to write to otherwise read-only foreign repos. + * + * @param $dir string Virtual URL (or storage path) of directory to clean + * @return Status + */ + public function quickCleanDir( $dir ) { + $status = $this->newGood(); + $status->merge( $this->backend->clean( + array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) ); + + return $status; + } + + /** + * Import a batch of files from the local file system into the repo. + * This does no locking nor journaling and overrides existing files. + * This function can be used to write to otherwise read-only foreign repos. + * This is intended for copying generated thumbnails into the repo. + * + * All path parameters may be a file system path, storage path, or virtual URL. + * When "dispositions" are given they are used as Content-Disposition if supported. + * + * @param $triples Array List of (source path, destination path, disposition) + * @return FileRepoStatus + */ + public function quickImportBatch( array $triples ) { + $status = $this->newGood(); + $operations = array(); + foreach ( $triples as $triple ) { + list( $src, $dst ) = $triple; + $src = $this->resolveToStoragePath( $src ); + $dst = $this->resolveToStoragePath( $dst ); + $operations[] = array( + 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store', + 'src' => $src, + 'dst' => $dst, + 'disposition' => isset( $triple[2] ) ? $triple[2] : null + ); + $status->merge( $this->initDirectory( dirname( $dst ) ) ); } + $status->merge( $this->backend->doQuickOperations( $operations ) ); + + return $status; + } + + /** + * Purge a batch of files from the repo. + * This function can be used to write to otherwise read-only foreign repos. + * This does no locking nor journaling and is intended for purging thumbnails. + * + * @param $paths Array List of virtual URLs or storage paths + * @return FileRepoStatus + */ + public function quickPurgeBatch( array $paths ) { + $status = $this->newGood(); + $operations = array(); + foreach ( $paths as $path ) { + $operations[] = array( + 'op' => 'delete', + 'src' => $this->resolveToStoragePath( $path ), + 'ignoreMissingSource' => true + ); + } + $status->merge( $this->backend->doQuickOperations( $operations ) ); + + return $status; } /** @@ -784,44 +937,63 @@ class FileRepo { * Returns a FileRepoStatus object with the file Virtual URL in the value, * file can later be disposed using FileRepo::freeTemp(). * - * * @param $originalName String: the base name of the file as specified * by the user. The file extension will be maintained. * @param $srcPath String: the current location of the file. * @return FileRepoStatus object with the URL in the value. */ public function storeTemp( $originalName, $srcPath ) { - $date = gmdate( "YmdHis" ); - $hashPath = $this->getHashPath( $originalName ); - $dstRel = "{$hashPath}{$date}!{$originalName}"; - $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); + $this->assertWritableRepo(); // fail out if read-only + + $date = gmdate( "YmdHis" ); + $hashPath = $this->getHashPath( $originalName ); + $dstRel = "{$hashPath}{$date}!{$originalName}"; + $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); + $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; + + $result = $this->quickImport( $srcPath, $virtualUrl ); + $result->value = $virtualUrl; - $result = $this->store( $srcPath, 'temp', $dstRel, self::SKIP_LOCKING ); - $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; return $result; } /** - * Concatenate a list of files into a target file location. - * + * Remove a temporary file or mark it for garbage collection + * + * @param $virtualUrl String: the virtual URL returned by FileRepo::storeTemp() + * @return Boolean: true on success, false on failure + */ + public function freeTemp( $virtualUrl ) { + $this->assertWritableRepo(); // fail out if read-only + + $temp = $this->getVirtualUrl( 'temp' ); + if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { + wfDebug( __METHOD__.": Invalid temp virtual URL\n" ); + return false; + } + + return $this->quickPurge( $virtualUrl )->isOK(); + } + + /** + * Concatenate a list of temporary files into a target file location. + * * @param $srcPaths Array Ordered list of source virtual URLs/storage paths * @param $dstPath String Target file system path * @param $flags Integer: bitwise combination of the following flags: * self::DELETE_SOURCE Delete the source files * @return FileRepoStatus */ - function concatenate( $srcPaths, $dstPath, $flags = 0 ) { + public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) { + $this->assertWritableRepo(); // fail out if read-only + $status = $this->newGood(); $sources = array(); - $deleteOperations = array(); // post-concatenate ops foreach ( $srcPaths as $srcPath ) { // Resolve source to a storage path if virtual $source = $this->resolveToStoragePath( $srcPath ); $sources[] = $source; // chunk to merge - if ( $flags & self::DELETE_SOURCE ) { - $deleteOperations[] = array( 'op' => 'delete', 'src' => $source ); - } } // Concatenate the chunks into one FS file @@ -832,50 +1004,34 @@ class FileRepo { } // Delete the sources if required - if ( $deleteOperations ) { - $opts = array( 'force' => true ); - $status->merge( $this->backend->doOperations( $deleteOperations, $opts ) ); + if ( $flags & self::DELETE_SOURCE ) { + $status->merge( $this->quickPurgeBatch( $srcPaths ) ); } - // Make sure status is OK, despite any $deleteOperations fatals + // Make sure status is OK, despite any quickPurgeBatch() fatals $status->setResult( true ); return $status; } /** - * Remove a temporary file or mark it for garbage collection - * - * @param $virtualUrl String: the virtual URL returned by FileRepo::storeTemp() - * @return Boolean: true on success, false on failure - */ - public function freeTemp( $virtualUrl ) { - $temp = "mwrepo://{$this->name}/temp"; - if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { - wfDebug( __METHOD__.": Invalid temp virtual URL\n" ); - return false; - } - $path = $this->resolveVirtualUrl( $virtualUrl ); - $op = array( 'op' => 'delete', 'src' => $path ); - $status = $this->backend->doOperation( $op ); - return $status->isOK(); - } - - /** * Copy or move a file either from a storage path, virtual URL, - * or FS path, into this repository at the specified destination location. + * or file system path, into this repository at the specified destination location. * * Returns a FileRepoStatus object. On success, the value contains "new" or * "archived", to indicate whether the file was new with that name. * - * @param $srcPath String: the source FS path, storage path, or URL + * @param $srcPath String: the source file system path, storage path, or URL * @param $dstRel String: the destination relative path * @param $archiveRel String: the relative path where the existing file is to * be archived, if there is one. Relative to the public zone root. * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source file should be deleted if possible + * @return FileRepoStatus */ public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { + $this->assertWritableRepo(); // fail out if read-only + $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags ); if ( $status->successCount == 0 ) { $status->ok = false; @@ -885,6 +1041,7 @@ class FileRepo { } else { $status->value = false; } + return $status; } @@ -894,11 +1051,13 @@ class FileRepo { * @param $triplets Array: (source, dest, archive) triplets as per publish() * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source files should be deleted if possible + * @throws MWException * @return FileRepoStatus */ - public function publishBatch( $triplets, $flags = 0 ) { - $backend = $this->backend; // convenience + public function publishBatch( array $triplets, $flags = 0 ) { + $this->assertWritableRepo(); // fail out if read-only + $backend = $this->backend; // convenience // Try creating directories $status = $this->initZones( 'public' ); if ( !$status->isOK() ) { @@ -913,9 +1072,7 @@ class FileRepo { foreach ( $triplets as $i => $triplet ) { list( $srcPath, $dstRel, $archiveRel ) = $triplet; // Resolve source to a storage path if virtual - if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) { - $srcPath = $this->resolveVirtualUrl( $srcPath ); - } + $srcPath = $this->resolveToStoragePath( $srcPath ); if ( !$this->validateFilename( $dstRel ) ) { throw new MWException( 'Validation error in $dstRel' ); } @@ -930,10 +1087,10 @@ class FileRepo { $dstDir = dirname( $dstPath ); $archiveDir = dirname( $archivePath ); // Abort immediately on directory creation errors since they're likely to be repetitive - if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) { + if ( !$this->initDirectory( $dstDir )->isOK() ) { return $this->newFatal( 'directorycreateerror', $dstDir ); } - if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) { + if ( !$this->initDirectory($archiveDir )->isOK() ) { return $this->newFatal( 'directorycreateerror', $archiveDir ); } @@ -999,15 +1156,50 @@ class FileRepo { } /** + * Creates a directory with the appropriate zone permissions. + * Callers are responsible for doing read-only and "writable repo" checks. + * + * @param $dir string Virtual URL (or storage path) of directory to clean + * @return Status + */ + protected function initDirectory( $dir ) { + $path = $this->resolveToStoragePath( $dir ); + list( $b, $container, $r ) = FileBackend::splitStoragePath( $path ); + + $params = array( 'dir' => $path ); + if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) { + # Take all available measures to prevent web accessibility of new deleted + # directories, in case the user has not configured offline storage + $params = array( 'noAccess' => true, 'noListing' => true ) + $params; + } + + return $this->backend->prepare( $params ); + } + + /** + * Deletes a directory if empty. + * + * @param $dir string Virtual URL (or storage path) of directory to clean + * @return Status + */ + public function cleanDir( $dir ) { + $this->assertWritableRepo(); // fail out if read-only + + $status = $this->newGood(); + $status->merge( $this->backend->clean( + array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) ); + + return $status; + } + + /** * Checks existence of a a file * - * @param $file Virtual URL (or storage path) of file to check - * @param $flags Integer: bitwise combination of the following flags: - * self::FILES_ONLY Mark file as existing only if it is a file (not directory) + * @param $file string Virtual URL (or storage path) of file to check * @return bool */ - public function fileExists( $file, $flags = 0 ) { - $result = $this->fileExistsBatch( array( $file ), $flags ); + public function fileExists( $file ) { + $result = $this->fileExistsBatch( array( $file ) ); return $result[0]; } @@ -1015,27 +1207,14 @@ class FileRepo { * Checks existence of an array of files. * * @param $files Array: Virtual URLs (or storage paths) of files to check - * @param $flags Integer: bitwise combination of the following flags: - * self::FILES_ONLY Mark file as existing only if it is a file (not directory) - * @return Either array of files and existence flags, or false + * @return array|bool Either array of files and existence flags, or false */ - public function fileExistsBatch( $files, $flags = 0 ) { + public function fileExistsBatch( array $files ) { $result = array(); foreach ( $files as $key => $file ) { - if ( self::isVirtualUrl( $file ) ) { - $file = $this->resolveVirtualUrl( $file ); - } - if ( FileBackend::isStoragePath( $file ) ) { - $result[$key] = $this->backend->fileExists( array( 'src' => $file ) ); - } else { - if ( $flags & self::FILES_ONLY ) { - $result[$key] = is_file( $file ); // FS only - } else { - $result[$key] = file_exists( $file ); // FS only - } - } + $file = $this->resolveToStoragePath( $file ); + $result[$key] = $this->backend->fileExists( array( 'src' => $file ) ); } - return $result; } @@ -1050,6 +1229,8 @@ class FileRepo { * @return FileRepoStatus object */ public function delete( $srcRel, $archiveRel ) { + $this->assertWritableRepo(); // fail out if read-only + return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) ); } @@ -1067,10 +1248,11 @@ class FileRepo { * is a two-element array containing the source file path relative to the * public root in the first element, and the archive file path relative * to the deleted zone root in the second element. + * @throws MWException * @return FileRepoStatus */ - public function deleteBatch( $sourceDestPairs ) { - $backend = $this->backend; // convenience + public function deleteBatch( array $sourceDestPairs ) { + $this->assertWritableRepo(); // fail out if read-only // Try creating directories $status = $this->initZones( array( 'public', 'deleted' ) ); @@ -1080,14 +1262,14 @@ class FileRepo { $status = $this->newGood(); + $backend = $this->backend; // convenience $operations = array(); // Validate filenames and create archive directories foreach ( $sourceDestPairs as $pair ) { list( $srcRel, $archiveRel ) = $pair; if ( !$this->validateFilename( $srcRel ) ) { throw new MWException( __METHOD__.':Validation error in $srcRel' ); - } - if ( !$this->validateFilename( $archiveRel ) ) { + } elseif ( !$this->validateFilename( $archiveRel ) ) { throw new MWException( __METHOD__.':Validation error in $archiveRel' ); } @@ -1099,10 +1281,9 @@ class FileRepo { $archiveDir = dirname( $archivePath ); // does not touch FS // Create destination directories - if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) { + if ( !$this->initDirectory( $archiveDir )->isOK() ) { return $this->newFatal( 'directorycreateerror', $archiveDir ); } - $this->initDeletedDir( $archiveDir ); $operations[] = array( 'op' => 'move', @@ -1124,9 +1305,19 @@ class FileRepo { } /** + * Delete files in the deleted directory if they are not referenced in the filearchive table + * + * STUB + */ + public function cleanupDeletedBatch( array $storageKeys ) { + $this->assertWritableRepo(); + } + + /** * Get a relative path for a deletion archive key, * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg * + * @param $key string * @return string */ public function getDeletedHashPath( $key ) { @@ -1155,7 +1346,7 @@ class FileRepo { /** * Get a local FS copy of a file with a given virtual URL/storage path. * Temporary files may be purged when the file object falls out of scope. - * + * * @param $virtualUrl string * @return TempFSFile|null Returns null on failure */ @@ -1168,7 +1359,7 @@ class FileRepo { * Get a local FS file with a given virtual URL/storage path. * The file is either an original or a copy. It should not be changed. * Temporary files may be purged when the file object falls out of scope. - * + * * @param $virtualUrl string * @return FSFile|null Returns null on failure. */ @@ -1193,7 +1384,7 @@ class FileRepo { * Get the timestamp of a file with a given virtual URL/storage path * * @param $virtualUrl string - * @return string|false + * @return string|bool False on failure */ public function getFileTimestamp( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); @@ -1201,18 +1392,14 @@ class FileRepo { } /** - * Get the sha1 of a file with a given virtual URL/storage path + * Get the sha1 (base 36) of a file with a given virtual URL/storage path * * @param $virtualUrl string - * @return string|false + * @return string|bool */ public function getFileSha1( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); - $tmpFile = $this->backend->getLocalReference( array( 'src' => $path ) ); - if ( !$tmpFile ) { - return false; - } - return $tmpFile->getSha1Base36(); + return $this->backend->getFileSha1Base36( array( 'src' => $path ) ); } /** @@ -1276,23 +1463,7 @@ class FileRepo { if ( strval( $filename ) == '' ) { return false; } - if ( wfIsWindows() ) { - $filename = strtr( $filename, '\\', '/' ); - } - /** - * Use the same traversal protection as Title::secureAndSplit() - */ - if ( strpos( $filename, '.' ) !== false && - ( $filename === '.' || $filename === '..' || - strpos( $filename, './' ) === 0 || - strpos( $filename, '../' ) === 0 || - strpos( $filename, '/./' ) !== false || - strpos( $filename, '/../' ) !== false ) ) - { - return false; - } else { - return true; - } + return FileBackend::isPathTraversalFree( $filename ); } /** @@ -1303,11 +1474,9 @@ class FileRepo { function getErrorCleanupFunction() { switch ( $this->pathDisclosureProtection ) { case 'none': + case 'simple': // b/c $callback = array( $this, 'passThrough' ); break; - case 'simple': - $callback = array( $this, 'simpleClean' ); - break; default: // 'paranoid' $callback = array( $this, 'paranoidClean' ); } @@ -1330,22 +1499,6 @@ class FileRepo { * @param $param string * @return string */ - function simpleClean( $param ) { - global $IP; - if ( !isset( $this->simpleCleanPairs ) ) { - $this->simpleCleanPairs = array( - $IP => '$IP', // sanity - ); - } - return strtr( $param, $this->simpleCleanPairs ); - } - - /** - * Path disclosure protection function - * - * @param $param string - * @return string - */ function passThrough( $param ) { return $param; } @@ -1355,7 +1508,7 @@ class FileRepo { * * @return FileRepoStatus */ - function newFatal( $message /*, parameters...*/ ) { + public function newFatal( $message /*, parameters...*/ ) { $params = func_get_args(); array_unshift( $params, $this ); return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params ); @@ -1364,20 +1517,14 @@ class FileRepo { /** * Create a new good result * + * @param $value null|string * @return FileRepoStatus */ - function newGood( $value = null ) { + public function newGood( $value = null ) { return FileRepoStatus::newGood( $this, $value ); } /** - * Delete files in the deleted directory if they are not referenced in the filearchive table - * - * STUB - */ - public function cleanupDeletedBatch( $storageKeys ) {} - - /** * Checks if there is a redirect named as $title. If there is, return the * title object. If not, return false. * STUB @@ -1413,6 +1560,21 @@ class FileRepo { } /** + * Get the portion of the file that contains the origin file name. + * If that name is too long, then the name "thumbnail.<ext>" will be given. + * + * @param $name string + * @return string + */ + public function nameForThumb( $name ) { + if ( strlen( $name ) > $this->abbrvThreshold ) { + $ext = FileBackend::extensionFromPath( $name ); + $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext"; + } + return $name; + } + + /** * Returns true if this the local file repository. * * @return bool @@ -1427,8 +1589,9 @@ class FileRepo { * The parameters are the parts of the key, as for wfMemcKey(). * * STUB + * @return bool */ - function getSharedCacheKey( /*...*/ ) { + public function getSharedCacheKey( /*...*/ ) { return false; } @@ -1439,13 +1602,43 @@ class FileRepo { * * @return string */ - function getLocalCacheKey( /*...*/ ) { + public function getLocalCacheKey( /*...*/ ) { $args = func_get_args(); array_unshift( $args, 'filerepo', $this->getName() ); return call_user_func_array( 'wfMemcKey', $args ); } /** + * Get an temporary FileRepo associated with this repo. + * Files will be created in the temp zone of this repo and + * thumbnails in a /temp subdirectory in thumb zone of this repo. + * It will have the same backend as this repo. + * + * @return TempFileRepo + */ + public function getTempRepo() { + return new TempFileRepo( array( + 'name' => "{$this->name}-temp", + 'backend' => $this->backend, + 'zones' => array( + 'public' => array( + 'container' => $this->zones['temp']['container'], + 'directory' => $this->zones['temp']['directory'] + ), + 'thumb' => array( + 'container' => $this->zones['thumb']['container'], + 'directory' => ( $this->zones['thumb']['directory'] == '' ) + ? 'temp' + : $this->zones['thumb']['directory'] . '/temp' + ) + ), + 'url' => $this->getZoneUrl( 'temp' ), + 'thumbUrl' => $this->getZoneUrl( 'thumb' ) . '/temp', + 'hashLevels' => $this->hashLevels // performance + ) ); + } + + /** * Get an UploadStash associated with this repo. * * @return UploadStash @@ -1453,4 +1646,22 @@ class FileRepo { public function getUploadStash() { return new UploadStash( $this ); } + + /** + * Throw an exception if this repo is read-only by design. + * This does not and should not check getReadOnlyReason(). + * + * @return void + * @throws MWException + */ + protected function assertWritableRepo() {} +} + +/** + * FileRepo for temporary files created via FileRepo::getTempRepo() + */ +class TempFileRepo extends FileRepo { + public function getTempRepo() { + throw new MWException( "Cannot get a temp repo from a temp repo." ); + } } diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php index 4eea9030..6f28b104 100644 --- a/includes/filerepo/FileRepoStatus.php +++ b/includes/filerepo/FileRepoStatus.php @@ -1,6 +1,21 @@ <?php /** - * Generic operation result for FileRepo-related operations + * Generic operation result for FileRepo-related operations. + * + * 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 FileRepo diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index e544defb..13de9e6b 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -2,6 +2,21 @@ /** * Foreign repository accessible through api.php requests. * + * 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 FileRepo */ @@ -36,6 +51,9 @@ class ForeignAPIRepo extends FileRepo { protected $mQueryCache = array(); protected $mFileExists = array(); + /** + * @param $info array|null + */ function __construct( $info ) { global $wgLocalFileRepo; parent::__construct( $info ); @@ -66,6 +84,8 @@ class ForeignAPIRepo extends FileRepo { * Per docs in FileRepo, this needs to return false if we don't support versioned * files. Well, we don't. * + * @param $title Title + * @param $time string|bool * @return File */ function newFile( $title, $time = false ) { @@ -76,38 +96,10 @@ class ForeignAPIRepo extends FileRepo { } /** - * No-ops + * @param $files array + * @return array */ - - function storeBatch( $triplets, $flags = 0 ) { - return false; - } - - function storeTemp( $originalName, $srcPath ) { - return false; - } - - function concatenate( $fileList, $targetPath, $flags = 0 ){ - return false; - } - - function append( $srcPath, $toAppendPath, $flags = 0 ){ - return false; - } - - function appendFinish( $toAppendPath ){ - return false; - } - - function publishBatch( $triplets, $flags = 0 ) { - return false; - } - - function deleteBatch( $sourceDestPairs ) { - return false; - } - - function fileExistsBatch( $files, $flags = 0 ) { + function fileExistsBatch( array $files ) { $results = array(); foreach ( $files as $k => $f ) { if ( isset( $this->mFileExists[$k] ) ) { @@ -119,6 +111,10 @@ class ForeignAPIRepo extends FileRepo { # same repo. $results[$k] = false; unset( $files[$k] ); + } elseif ( FileBackend::isStoragePath( $f ) ) { + $results[$k] = false; + unset( $files[$k] ); + wfWarn( "Got mwstore:// path '$f'." ); } } @@ -134,17 +130,25 @@ class ForeignAPIRepo extends FileRepo { return $results; } + /** + * @param $virtualUrl string + * @return bool + */ function getFileProps( $virtualUrl ) { return false; } + /** + * @param $query array + * @return string + */ function fetchImageQuery( $query ) { global $wgMemc; $query = array_merge( $query, array( - 'format' => 'json', - 'action' => 'query', + 'format' => 'json', + 'action' => 'query', 'redirects' => 'true' ) ); if ( $this->mApiBase ) { @@ -173,6 +177,10 @@ class ForeignAPIRepo extends FileRepo { return FormatJson::decode( $this->mQueryCache[$url], true ); } + /** + * @param $data array + * @return bool|array + */ function getImageInfo( $data ) { if( $data && isset( $data['query']['pages'] ) ) { foreach( $data['query']['pages'] as $info ) { @@ -184,6 +192,10 @@ class ForeignAPIRepo extends FileRepo { return false; } + /** + * @param $hash string + * @return array + */ function findBySha1( $hash ) { $results = $this->fetchImageQuery( array( 'aisha1base36' => $hash, @@ -202,6 +214,14 @@ class ForeignAPIRepo extends FileRepo { return $ret; } + /** + * @param $name string + * @param $width int + * @param $height int + * @param $result null + * @param $otherParams string + * @return bool + */ function getThumbUrl( $name, $width = -1, $height = -1, &$result = null, $otherParams = '' ) { $data = $this->fetchImageQuery( array( 'titles' => 'File:' . $name, @@ -230,10 +250,14 @@ class ForeignAPIRepo extends FileRepo { * @param $name String is a dbkey form of a title * @param $width * @param $height - * @param String $param Other rendering parameters (page number, etc) from handler's makeParamString. + * @param String $params Other rendering parameters (page number, etc) from handler's makeParamString. + * @return bool|string */ - function getThumbUrlFromCache( $name, $width, $height, $params="" ) { + function getThumbUrlFromCache( $name, $width, $height, $params = "" ) { global $wgMemc; + // We can't check the local cache using FileRepo functions because + // we override fileExistsBatch(). We have to use the FileBackend directly. + $backend = $this->getBackend(); // convenience if ( !$this->canCacheThumbs() ) { $result = null; // can't pass "null" by reference, but it's ok as default value @@ -274,9 +298,11 @@ class ForeignAPIRepo extends FileRepo { $localFilename = $localPath . "/" . $fileName; $localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . rawurlencode( $name ) . "/" . rawurlencode( $fileName ); - if( $this->fileExists( $localFilename ) && isset( $metadata['timestamp'] ) ) { + if( $backend->fileExists( array( 'src' => $localFilename ) ) + && isset( $metadata['timestamp'] ) ) + { wfDebug( __METHOD__ . " Thumbnail was already downloaded before\n" ); - $modified = $this->getFileTimestamp( $localFilename ); + $modified = $backend->getFileTimestamp( array( 'src' => $localFilename ) ); $remoteModified = strtotime( $metadata['timestamp'] ); $current = time(); $diff = abs( $modified - $current ); @@ -294,16 +320,14 @@ class ForeignAPIRepo extends FileRepo { return false; } + # @todo FIXME: Delete old thumbs that aren't being used. Maintenance script? - wfSuppressWarnings(); - $backend = $this->getBackend(); - $op = array( 'op' => 'create', 'dst' => $localFilename, 'content' => $thumb ); - if( !$backend->doOperation( $op )->isOK() ) { - wfRestoreWarnings(); - wfDebug( __METHOD__ . " could not write to thumb path\n" ); + $backend->prepare( array( 'dir' => dirname( $localFilename ) ) ); + $params = array( 'dst' => $localFilename, 'content' => $thumb ); + if( !$backend->quickCreate( $params )->isOK() ) { + wfDebug( __METHOD__ . " could not write to thumb path '$localFilename'\n" ); return $foreignUrl; } - wfRestoreWarnings(); $knownThumbUrls[$sizekey] = $localUrl; $wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry ); wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" ); @@ -312,6 +336,8 @@ class ForeignAPIRepo extends FileRepo { /** * @see FileRepo::getZoneUrl() + * @param $zone String + * @return String */ function getZoneUrl( $zone ) { switch ( $zone ) { @@ -326,6 +352,8 @@ class ForeignAPIRepo extends FileRepo { /** * Get the local directory corresponding to one of the basic zones + * @param $zone string + * @return bool|null|string */ function getZonePath( $zone ) { $supported = array( 'public', 'thumb' ); @@ -345,6 +373,7 @@ class ForeignAPIRepo extends FileRepo { /** * The user agent the ForeignAPIRepo will use. + * @return string */ public static function getUserAgent() { return Http::userAgent() . " ForeignAPIRepo/" . self::VERSION; @@ -353,6 +382,10 @@ class ForeignAPIRepo extends FileRepo { /** * Like a Http:get request, but with custom User-Agent. * @see Http:get + * @param $url string + * @param $timeout string + * @param $options array + * @return bool|String */ public static function httpGet( $url, $timeout = 'default', $options = array() ) { $options['timeout'] = $timeout; @@ -362,7 +395,7 @@ class ForeignAPIRepo extends FileRepo { $options['method'] = "GET"; if ( !isset( $options['timeout'] ) ) { - $options['timeout'] = 'default'; + $options['timeout'] = 'default'; } $req = MWHttpRequest::factory( $url, $options ); @@ -370,13 +403,24 @@ class ForeignAPIRepo extends FileRepo { $status = $req->execute(); if ( $status->isOK() ) { - return $req->getContent(); + return $req->getContent(); } else { - return false; + return false; } } + /** + * @param $callback Array|string + * @throws MWException + */ function enumFiles( $callback ) { throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) ); } + + /** + * @throws MWException + */ + protected function assertWritableRepo() { + throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + } } diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index 0311ebcd..4b206c3d 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -1,6 +1,21 @@ <?php /** - * A foreign repository with an accessible MediaWiki database + * A foreign repository with an accessible MediaWiki database. + * + * 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 FileRepo @@ -21,6 +36,9 @@ class ForeignDBRepo extends LocalRepo { var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' ); var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' ); + /** + * @param $info array|null + */ function __construct( $info ) { parent::__construct( $info ); $this->dbType = $info['dbType']; @@ -33,6 +51,9 @@ class ForeignDBRepo extends LocalRepo { $this->hasSharedCache = $info['hasSharedCache']; } + /** + * @return DatabaseBase + */ function getMasterDB() { if ( !isset( $this->dbConn ) ) { $this->dbConn = DatabaseBase::factory( $this->dbType, @@ -49,10 +70,16 @@ class ForeignDBRepo extends LocalRepo { return $this->dbConn; } + /** + * @return DatabaseBase + */ function getSlaveDB() { return $this->getMasterDB(); } + /** + * @return bool + */ function hasSharedCache() { return $this->hasSharedCache; } @@ -61,6 +88,7 @@ class ForeignDBRepo extends LocalRepo { * Get a key on the primary cache for this repository. * Returns false if the repository's cache is not accessible at this site. * The parameters are the parts of the key, as for wfMemcKey(). + * @return bool|mixed */ function getSharedCacheKey( /*...*/ ) { if ( $this->hasSharedCache() ) { @@ -72,13 +100,7 @@ class ForeignDBRepo extends LocalRepo { } } - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { - throw new MWException( get_class($this) . ': write operations are not supported' ); - } - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { - throw new MWException( get_class($this) . ': write operations are not supported' ); - } - function deleteBatch( $sourceDestPairs ) { - throw new MWException( get_class($this) . ': write operations are not supported' ); + protected function assertWritableRepo() { + throw new MWException( get_class( $this ) . ': write operations are not supported.' ); } } diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index 28b48b5e..bd76fce7 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -1,6 +1,21 @@ <?php /** - * A foreign repository with a MediaWiki database accessible via the configured LBFactory + * A foreign repository with a MediaWiki database accessible via the configured LBFactory. + * + * 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 FileRepo @@ -16,6 +31,9 @@ class ForeignDBViaLBRepo extends LocalRepo { var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' ); var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' ); + /** + * @param $info array|null + */ function __construct( $info ) { parent::__construct( $info ); $this->wiki = $info['wiki']; @@ -23,10 +41,16 @@ class ForeignDBViaLBRepo extends LocalRepo { $this->hasSharedCache = $info['hasSharedCache']; } + /** + * @return DatabaseBase + */ function getMasterDB() { return wfGetDB( DB_MASTER, array(), $this->wiki ); } + /** + * @return DatabaseBase + */ function getSlaveDB() { return wfGetDB( DB_SLAVE, array(), $this->wiki ); } @@ -39,6 +63,7 @@ class ForeignDBViaLBRepo extends LocalRepo { * Get a key on the primary cache for this repository. * Returns false if the repository's cache is not accessible at this site. * The parameters are the parts of the key, as for wfMemcKey(). + * @return bool|string */ function getSharedCacheKey( /*...*/ ) { if ( $this->hasSharedCache() ) { @@ -50,13 +75,7 @@ class ForeignDBViaLBRepo extends LocalRepo { } } - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { - throw new MWException( get_class($this) . ': write operations are not supported' ); - } - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { - throw new MWException( get_class($this) . ': write operations are not supported' ); - } - function deleteBatch( $fileMap ) { - throw new MWException( get_class($this) . ': write operations are not supported' ); + protected function assertWritableRepo() { + throw new MWException( get_class( $this ) . ': write operations are not supported.' ); } } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index cc23fa31..0954422d 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -3,6 +3,21 @@ * Local repository that stores files in the local filesystem and registers them * in the wiki's own database. * + * 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 FileRepo */ @@ -24,7 +39,7 @@ class LocalRepo extends FileRepo { /** * @throws MWException * @param $row - * @return File + * @return LocalFile */ function newFileFromRow( $row ) { if ( isset( $row->img_name ) ) { @@ -55,7 +70,7 @@ class LocalRepo extends FileRepo { * * @return FileRepoStatus */ - function cleanupDeletedBatch( $storageKeys ) { + function cleanupDeletedBatch( array $storageKeys ) { $backend = $this->backend; // convenience $root = $this->getZonePath( 'deleted' ); $dbw = $this->getMasterDB(); @@ -64,7 +79,7 @@ class LocalRepo extends FileRepo { foreach ( $storageKeys as $key ) { $hashPath = $this->getDeletedHashPath( $key ); $path = "$root/$hashPath$key"; - $dbw->begin(); + $dbw->begin( __METHOD__ ); // Check for usage in deleted/hidden files and pre-emptively // lock the key to avoid any future use until we are finished. $deleted = $this->deletedFileHasKey( $key, 'lock' ); @@ -80,7 +95,7 @@ class LocalRepo extends FileRepo { wfDebug( __METHOD__ . ": $key still in use\n" ); $status->successCount++; } - $dbw->commit(); + $dbw->commit( __METHOD__ ); } return $status; } @@ -133,7 +148,7 @@ class LocalRepo extends FileRepo { public static function getHashFromKey( $key ) { return strtok( $key, '.' ); } - + /** * Checks if there is a redirect named as $title * @@ -183,12 +198,12 @@ class LocalRepo extends FileRepo { } } - /** * Function link Title::getArticleID(). * We can't say Title object, what database it should use, so we duplicate that function here. * * @param $title Title + * @return bool|int|mixed */ protected function getArticleID( $title ) { if( !$title instanceof Title ) { @@ -219,7 +234,9 @@ class LocalRepo extends FileRepo { $res = $dbr->select( 'image', LocalFile::selectFields(), - array( 'img_sha1' => $hash ) + array( 'img_sha1' => $hash ), + __METHOD__, + array( 'ORDER BY' => 'img_name' ) ); $result = array(); @@ -232,7 +249,41 @@ class LocalRepo extends FileRepo { } /** + * Get an array of arrays or iterators of file objects for files that + * have the given SHA-1 content hashes. + * + * Overrides generic implementation in FileRepo for performance reason + * + * @param $hashes array An array of hashes + * @return array An Array of arrays or iterators of file objects and the hash as key + */ + function findBySha1s( array $hashes ) { + if( !count( $hashes ) ) { + return array(); //empty parameter + } + + $dbr = $this->getSlaveDB(); + $res = $dbr->select( + 'image', + LocalFile::selectFields(), + array( 'img_sha1' => $hashes ), + __METHOD__, + array( 'ORDER BY' => 'img_name' ) + ); + + $result = array(); + foreach ( $res as $row ) { + $file = $this->newFileFromRow( $row ); + $result[$file->getSha1()][] = $file; + } + $res->free(); + + return $result; + } + + /** * Get a connection to the slave DB + * @return DatabaseBase */ function getSlaveDB() { return wfGetDB( DB_SLAVE ); @@ -240,6 +291,7 @@ class LocalRepo extends FileRepo { /** * Get a connection to the master DB + * @return DatabaseBase */ function getMasterDB() { return wfGetDB( DB_MASTER ); diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index 65318f40..dda51cea 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -2,6 +2,21 @@ /** * File repository with no files. * + * 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 FileRepo */ @@ -11,40 +26,13 @@ * @ingroup FileRepo */ class NullRepo extends FileRepo { - function __construct( $info ) {} - function storeBatch( $triplets, $flags = 0 ) { - return false; - } + /** + * @param $info array|null + */ + function __construct( $info ) {} - function storeTemp( $originalName, $srcPath ) { - return false; - } - function append( $srcPath, $toAppendPath, $flags = 0 ){ - return false; - } - function appendFinish( $toAppendPath ){ - return false; - } - function publishBatch( $triplets, $flags = 0 ) { - return false; - } - function deleteBatch( $sourceDestPairs ) { - return false; - } - function fileExistsBatch( $files, $flags = 0 ) { - return false; - } - function getFileProps( $virtualUrl ) { - return false; - } - function newFile( $title, $time = false ) { - return false; - } - function findFile( $title, $options = array() ) { - return false; - } - function concatenate( $fileList, $targetPath, $flags = 0 ) { - return false; + protected function assertWritableRepo() { + throw new MWException( get_class( $this ) . ': write operations are not supported.' ); } } diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index 334ef2b8..f9e57599 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -1,6 +1,21 @@ <?php /** - * Prioritized list of file repositories + * Prioritized list of file repositories. + * + * 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 FileRepo @@ -12,7 +27,6 @@ * @ingroup FileRepo */ class RepoGroup { - /** * @var LocalRepo */ @@ -26,7 +40,7 @@ class RepoGroup { * @var RepoGroup */ protected static $instance; - const MAX_CACHE_SIZE = 1000; + const MAX_CACHE_SIZE = 500; /** * Get a RepoGroup instance. At present only one instance of RepoGroup is @@ -65,7 +79,7 @@ class RepoGroup { /** * Construct a group of file repositories. * - * @param $localInfo Associative array for local repo's info + * @param $localInfo array Associative array for local repo's info * @param $foreignInfo Array of repository info arrays. * Each info array is an associative array with the 'class' member * giving the class name. The entire array is passed to the repository @@ -114,50 +128,47 @@ class RepoGroup { && empty( $options['private'] ) && empty( $options['bypassCache'] ) ) { - $useCache = true; $time = isset( $options['time'] ) ? $options['time'] : ''; $dbkey = $title->getDBkey(); if ( isset( $this->cache[$dbkey][$time] ) ) { wfDebug( __METHOD__.": got File:$dbkey from process cache\n" ); # Move it to the end of the list so that we can delete the LRU entry later - $tmp = $this->cache[$dbkey]; - unset( $this->cache[$dbkey] ); - $this->cache[$dbkey] = $tmp; + $this->pingCache( $dbkey ); # Return the entry return $this->cache[$dbkey][$time]; - } else { - # Add a negative cache entry, may be overridden - $this->trimCache(); - $this->cache[$dbkey][$time] = false; - $cacheEntry =& $this->cache[$dbkey][$time]; } + $useCache = true; } else { $useCache = false; } # Check the local repo $image = $this->localRepo->findFile( $title, $options ); - if ( $image ) { - if ( $useCache ) { - $cacheEntry = $image; - } - return $image; - } # Check the foreign repos - foreach ( $this->foreignRepos as $repo ) { - $image = $repo->findFile( $title, $options ); - if ( $image ) { - if ( $useCache ) { - $cacheEntry = $image; + if ( !$image ) { + foreach ( $this->foreignRepos as $repo ) { + $image = $repo->findFile( $title, $options ); + if ( $image ) { + break; } - return $image; } } - # Not found, do not override negative cache - return false; + + $image = $image ? $image : false; // type sanity + # Cache file existence or non-existence + if ( $useCache && ( !$image || $image->isCacheable() ) ) { + $this->trimCache(); + $this->cache[$dbkey][$time] = $image; + } + + return $image; } + /** + * @param $inputItems array + * @return array + */ function findFiles( $inputItems ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); @@ -189,6 +200,8 @@ class RepoGroup { /** * Interface for FileRepo::checkRedirect() + * @param $title Title + * @return bool */ function checkRedirect( Title $title ) { if ( !$this->reposInitialised ) { @@ -213,7 +226,7 @@ class RepoGroup { * Returns false if the file does not exist. * * @param $hash String base 36 SHA-1 hash - * @param $options Option array, same as findFile() + * @param $options array Option array, same as findFile() * @return File object or false if it is not found */ function findFileFromKey( $hash, $options = array() ) { @@ -246,11 +259,36 @@ class RepoGroup { foreach ( $this->foreignRepos as $repo ) { $result = array_merge( $result, $repo->findBySha1( $hash ) ); } + usort( $result, 'File::compare' ); + return $result; + } + + /** + * Find all instances of files with this keys + * + * @param $hashes Array base 36 SHA-1 hashes + * @return Array of array of File objects + */ + function findBySha1s( array $hashes ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + + $result = $this->localRepo->findBySha1s( $hashes ); + foreach ( $this->foreignRepos as $repo ) { + $result = array_merge_recursive( $result, $repo->findBySha1s( $hashes ) ); + } + //sort the merged (and presorted) sublist of each hash + foreach( $result as $hash => $files ) { + usort( $result[$hash], 'File::compare' ); + } return $result; } /** * Get the repo instance with a given key. + * @param $index string|int + * @return bool|LocalRepo */ function getRepo( $index ) { if ( !$this->reposInitialised ) { @@ -264,16 +302,20 @@ class RepoGroup { return false; } } + /** * Get the repo instance by its name + * @param $name string + * @return bool */ function getRepoByName( $name ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } foreach ( $this->foreignRepos as $repo ) { - if ( $repo->name == $name) + if ( $repo->name == $name ) { return $repo; + } } return false; } @@ -294,6 +336,7 @@ class RepoGroup { * * @param $callback Callback: the function to call * @param $params Array: optional additional parameters to pass to the function + * @return bool */ function forEachForeignRepo( $callback, $params = array() ) { foreach( $this->foreignRepos as $repo ) { @@ -339,7 +382,9 @@ class RepoGroup { /** * Split a virtual URL into repo, zone and rel parts - * @return an array containing repo, zone and rel + * @param $url string + * @throws MWException + * @return array containing repo, zone and rel */ function splitVirtualUrl( $url ) { if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { @@ -353,6 +398,10 @@ class RepoGroup { return $bits; } + /** + * @param $fileName string + * @return array + */ function getFileProps( $fileName ) { if ( FileRepo::isVirtualUrl( $fileName ) ) { list( $repoName, /* $zone */, /* $rel */ ) = $this->splitVirtualUrl( $fileName ); @@ -367,6 +416,17 @@ class RepoGroup { } /** + * Move a cache entry to the top (such as when accessed) + */ + protected function pingCache( $key ) { + if ( isset( $this->cache[$key] ) ) { + $tmp = $this->cache[$key]; + unset( $this->cache[$key] ); + $this->cache[$key] = $tmp; + } + } + + /** * Limit cache memory */ protected function trimCache() { diff --git a/includes/filerepo/backend/FSFileBackend.php b/includes/filerepo/backend/FSFileBackend.php deleted file mode 100644 index 1a4c44ad..00000000 --- a/includes/filerepo/backend/FSFileBackend.php +++ /dev/null @@ -1,600 +0,0 @@ -<?php -/** - * @file - * @ingroup FileBackend - * @author Aaron Schulz - */ - -/** - * Class for a file system (FS) based file backend. - * - * All "containers" each map to a directory under the backend's base directory. - * For backwards-compatibility, some container paths can be set to custom paths. - * The wiki ID will not be used in any custom paths, so this should be avoided. - * - * Having directories with thousands of files will diminish performance. - * Sharding can be accomplished by using FileRepo-style hash paths. - * - * Status messages should avoid mentioning the internal FS paths. - * PHP warnings are assumed to be logged rather than output. - * - * @ingroup FileBackend - * @since 1.19 - */ -class FSFileBackend extends FileBackendStore { - protected $basePath; // string; directory holding the container directories - /** @var Array Map of container names to root paths */ - protected $containerPaths = array(); // for custom container paths - protected $fileMode; // integer; file permission mode - - protected $hadWarningErrors = array(); - - /** - * @see FileBackendStore::__construct() - * Additional $config params include: - * basePath : File system directory that holds containers. - * containerPaths : Map of container names to custom file system directories. - * This should only be used for backwards-compatibility. - * fileMode : Octal UNIX file permissions to use on files stored. - */ - public function __construct( array $config ) { - parent::__construct( $config ); - - // Remove any possible trailing slash from directories - if ( isset( $config['basePath'] ) ) { - $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash - } else { - $this->basePath = null; // none; containers must have explicit paths - } - - if ( isset( $config['containerPaths'] ) ) { - $this->containerPaths = (array)$config['containerPaths']; - foreach ( $this->containerPaths as &$path ) { - $path = rtrim( $path, '/' ); // remove trailing slash - } - } - - $this->fileMode = isset( $config['fileMode'] ) - ? $config['fileMode'] - : 0644; - } - - /** - * @see FileBackendStore::resolveContainerPath() - */ - protected function resolveContainerPath( $container, $relStoragePath ) { - // Check that container has a root directory - if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) { - // Check for sane relative paths (assume the base paths are OK) - if ( $this->isLegalRelPath( $relStoragePath ) ) { - return $relStoragePath; - } - } - return null; - } - - /** - * Sanity check a relative file system path for validity - * - * @param $path string Normalized relative path - * @return bool - */ - protected function isLegalRelPath( $path ) { - // Check for file names longer than 255 chars - if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS - return false; - } - if ( wfIsWindows() ) { // NTFS - return !preg_match( '![:*?"<>|]!', $path ); - } else { - return true; - } - } - - /** - * Given the short (unresolved) and full (resolved) name of - * a container, return the file system path of the container. - * - * @param $shortCont string - * @param $fullCont string - * @return string|null - */ - protected function containerFSRoot( $shortCont, $fullCont ) { - if ( isset( $this->containerPaths[$shortCont] ) ) { - return $this->containerPaths[$shortCont]; - } elseif ( isset( $this->basePath ) ) { - return "{$this->basePath}/{$fullCont}"; - } - return null; // no container base path defined - } - - /** - * Get the absolute file system path for a storage path - * - * @param $storagePath string Storage path - * @return string|null - */ - protected function resolveToFSPath( $storagePath ) { - list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath ); - if ( $relPath === null ) { - return null; // invalid - } - list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath ); - $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid - if ( $relPath != '' ) { - $fsPath .= "/{$relPath}"; - } - return $fsPath; - } - - /** - * @see FileBackendStore::isPathUsableInternal() - */ - public function isPathUsableInternal( $storagePath ) { - $fsPath = $this->resolveToFSPath( $storagePath ); - if ( $fsPath === null ) { - return false; // invalid - } - $parentDir = dirname( $fsPath ); - - if ( file_exists( $fsPath ) ) { - $ok = is_file( $fsPath ) && is_writable( $fsPath ); - } else { - $ok = is_dir( $parentDir ) && is_writable( $parentDir ); - } - - return $ok; - } - - /** - * @see FileBackendStore::doStoreInternal() - */ - protected function doStoreInternal( array $params ) { - $status = Status::newGood(); - - $dest = $this->resolveToFSPath( $params['dst'] ); - if ( $dest === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); - return $status; - } - - if ( file_exists( $dest ) ) { - if ( !empty( $params['overwrite'] ) ) { - $ok = unlink( $dest ); - if ( !$ok ) { - $status->fatal( 'backend-fail-delete', $params['dst'] ); - return $status; - } - } else { - $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); - return $status; - } - } - - $ok = copy( $params['src'], $dest ); - if ( !$ok ) { - $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] ); - return $status; - } - - $this->chmod( $dest ); - - return $status; - } - - /** - * @see FileBackendStore::doCopyInternal() - */ - protected function doCopyInternal( array $params ) { - $status = Status::newGood(); - - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['src'] ); - return $status; - } - - $dest = $this->resolveToFSPath( $params['dst'] ); - if ( $dest === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); - return $status; - } - - if ( file_exists( $dest ) ) { - if ( !empty( $params['overwrite'] ) ) { - $ok = unlink( $dest ); - if ( !$ok ) { - $status->fatal( 'backend-fail-delete', $params['dst'] ); - return $status; - } - } else { - $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); - return $status; - } - } - - $ok = copy( $source, $dest ); - if ( !$ok ) { - $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); - return $status; - } - - $this->chmod( $dest ); - - return $status; - } - - /** - * @see FileBackendStore::doMoveInternal() - */ - protected function doMoveInternal( array $params ) { - $status = Status::newGood(); - - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['src'] ); - return $status; - } - - $dest = $this->resolveToFSPath( $params['dst'] ); - if ( $dest === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); - return $status; - } - - if ( file_exists( $dest ) ) { - if ( !empty( $params['overwrite'] ) ) { - // Windows does not support moving over existing files - if ( wfIsWindows() ) { - $ok = unlink( $dest ); - if ( !$ok ) { - $status->fatal( 'backend-fail-delete', $params['dst'] ); - return $status; - } - } - } else { - $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); - return $status; - } - } - - $ok = rename( $source, $dest ); - clearstatcache(); // file no longer at source - if ( !$ok ) { - $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); - return $status; - } - - return $status; - } - - /** - * @see FileBackendStore::doDeleteInternal() - */ - protected function doDeleteInternal( array $params ) { - $status = Status::newGood(); - - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['src'] ); - return $status; - } - - if ( !is_file( $source ) ) { - if ( empty( $params['ignoreMissingSource'] ) ) { - $status->fatal( 'backend-fail-delete', $params['src'] ); - } - return $status; // do nothing; either OK or bad status - } - - $ok = unlink( $source ); - if ( !$ok ) { - $status->fatal( 'backend-fail-delete', $params['src'] ); - return $status; - } - - return $status; - } - - /** - * @see FileBackendStore::doCreateInternal() - */ - protected function doCreateInternal( array $params ) { - $status = Status::newGood(); - - $dest = $this->resolveToFSPath( $params['dst'] ); - if ( $dest === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); - return $status; - } - - if ( file_exists( $dest ) ) { - if ( !empty( $params['overwrite'] ) ) { - $ok = unlink( $dest ); - if ( !$ok ) { - $status->fatal( 'backend-fail-delete', $params['dst'] ); - return $status; - } - } else { - $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); - return $status; - } - } - - $bytes = file_put_contents( $dest, $params['content'] ); - if ( $bytes === false ) { - $status->fatal( 'backend-fail-create', $params['dst'] ); - return $status; - } - - $this->chmod( $dest ); - - return $status; - } - - /** - * @see FileBackendStore::doPrepareInternal() - */ - protected function doPrepareInternal( $fullCont, $dirRel, array $params ) { - $status = Status::newGood(); - list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); - $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid - $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; - if ( !wfMkdirParents( $dir ) ) { // make directory and its parents - $status->fatal( 'directorycreateerror', $params['dir'] ); - } elseif ( !is_writable( $dir ) ) { - $status->fatal( 'directoryreadonlyerror', $params['dir'] ); - } elseif ( !is_readable( $dir ) ) { - $status->fatal( 'directorynotreadableerror', $params['dir'] ); - } - return $status; - } - - /** - * @see FileBackendStore::doSecureInternal() - */ - protected function doSecureInternal( $fullCont, $dirRel, array $params ) { - $status = Status::newGood(); - list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); - $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid - $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; - // Seed new directories with a blank index.html, to prevent crawling... - if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) { - $bytes = file_put_contents( "{$dir}/index.html", '' ); - if ( !$bytes ) { - $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' ); - return $status; - } - } - // Add a .htaccess file to the root of the container... - if ( !empty( $params['noAccess'] ) ) { - if ( !file_exists( "{$contRoot}/.htaccess" ) ) { - $bytes = file_put_contents( "{$contRoot}/.htaccess", "Deny from all\n" ); - if ( !$bytes ) { - $storeDir = "mwstore://{$this->name}/{$shortCont}"; - $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" ); - return $status; - } - } - } - return $status; - } - - /** - * @see FileBackendStore::doCleanInternal() - */ - protected function doCleanInternal( $fullCont, $dirRel, array $params ) { - $status = Status::newGood(); - list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); - $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid - $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; - wfSuppressWarnings(); - if ( is_dir( $dir ) ) { - rmdir( $dir ); // remove directory if empty - } - wfRestoreWarnings(); - return $status; - } - - /** - * @see FileBackendStore::doFileExists() - */ - protected function doGetFileStat( array $params ) { - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { - return false; // invalid storage path - } - - $this->trapWarnings(); // don't trust 'false' if there were errors - $stat = is_file( $source ) ? stat( $source ) : false; // regular files only - $hadError = $this->untrapWarnings(); - - if ( $stat ) { - return array( - 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ), - 'size' => $stat['size'] - ); - } elseif ( !$hadError ) { - return false; // file does not exist - } else { - return null; // failure - } - } - - /** - * @see FileBackendStore::doClearCache() - */ - protected function doClearCache( array $paths = null ) { - clearstatcache(); // clear the PHP file stat cache - } - - /** - * @see FileBackendStore::getFileListInternal() - */ - public function getFileListInternal( $fullCont, $dirRel, array $params ) { - list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); - $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid - $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; - $exists = is_dir( $dir ); - if ( !$exists ) { - wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" ); - return array(); // nothing under this dir - } - $readable = is_readable( $dir ); - if ( !$readable ) { - wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" ); - return null; // bad permissions? - } - return new FSFileBackendFileList( $dir ); - } - - /** - * @see FileBackendStore::getLocalReference() - */ - public function getLocalReference( array $params ) { - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { - return null; - } - return new FSFile( $source ); - } - - /** - * @see FileBackendStore::getLocalCopy() - */ - public function getLocalCopy( array $params ) { - $source = $this->resolveToFSPath( $params['src'] ); - if ( $source === null ) { - return null; - } - - // Create a new temporary file with the same extension... - $ext = FileBackend::extensionFromPath( $params['src'] ); - $tmpFile = TempFSFile::factory( wfBaseName( $source ) . '_', $ext ); - if ( !$tmpFile ) { - return null; - } - $tmpPath = $tmpFile->getPath(); - - // Copy the source file over the temp file - $ok = copy( $source, $tmpPath ); - if ( !$ok ) { - return null; - } - - $this->chmod( $tmpPath ); - - return $tmpFile; - } - - /** - * Chmod a file, suppressing the warnings - * - * @param $path string Absolute file system path - * @return bool Success - */ - protected function chmod( $path ) { - wfSuppressWarnings(); - $ok = chmod( $path, $this->fileMode ); - wfRestoreWarnings(); - - return $ok; - } - - /** - * Listen for E_WARNING errors and track whether any happen - * - * @return bool - */ - protected function trapWarnings() { - $this->hadWarningErrors[] = false; // push to stack - set_error_handler( array( $this, 'handleWarning' ), E_WARNING ); - return false; // invoke normal PHP error handler - } - - /** - * Stop listening for E_WARNING errors and return true if any happened - * - * @return bool - */ - protected function untrapWarnings() { - restore_error_handler(); // restore previous handler - return array_pop( $this->hadWarningErrors ); // pop from stack - } - - private function handleWarning() { - $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true; - return true; // suppress from PHP handler - } -} - -/** - * Wrapper around RecursiveDirectoryIterator that catches - * exception or does any custom behavoir that we may want. - * Do not use this class from places outside FSFileBackend. - * - * @ingroup FileBackend - */ -class FSFileBackendFileList implements Iterator { - /** @var RecursiveIteratorIterator */ - protected $iter; - protected $suffixStart; // integer - protected $pos = 0; // integer - - /** - * @param $dir string file system directory - */ - public function __construct( $dir ) { - $dir = realpath( $dir ); // normalize - $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/" - try { - # Get an iterator that will return leaf nodes (non-directories) - if ( MWInit::classExists( 'FilesystemIterator' ) ) { // PHP >= 5.3 - # RecursiveDirectoryIterator extends FilesystemIterator. - # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x. - $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS; - $this->iter = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $dir, $flags ) ); - } else { // PHP < 5.3 - # RecursiveDirectoryIterator extends DirectoryIterator - $this->iter = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $dir ) ); - } - } catch ( UnexpectedValueException $e ) { - $this->iter = null; // bad permissions? deleted? - } - } - - public function current() { - // Return only the relative path and normalize slashes to FileBackend-style - // Make sure to use the realpath since the suffix is based upon that - return str_replace( '\\', '/', - substr( realpath( $this->iter->current() ), $this->suffixStart ) ); - } - - public function key() { - return $this->pos; - } - - public function next() { - try { - $this->iter->next(); - } catch ( UnexpectedValueException $e ) { - $this->iter = null; - } - ++$this->pos; - } - - public function rewind() { - $this->pos = 0; - try { - $this->iter->rewind(); - } catch ( UnexpectedValueException $e ) { - $this->iter = null; - } - } - - public function valid() { - return $this->iter && $this->iter->valid(); - } -} diff --git a/includes/filerepo/backend/FileBackend.php b/includes/filerepo/backend/FileBackend.php deleted file mode 100644 index 9433bcb4..00000000 --- a/includes/filerepo/backend/FileBackend.php +++ /dev/null @@ -1,1739 +0,0 @@ -<?php -/** - * @defgroup FileBackend File backend - * @ingroup FileRepo - * - * This module regroup classes meant for MediaWiki to interacts with - */ - -/** - * @file - * @ingroup FileBackend - * @author Aaron Schulz - */ - -/** - * Base class for all file backend classes (including multi-write backends). - * - * This class defines the methods as abstract that subclasses must implement. - * Outside callers can assume that all backends will have these functions. - * - * All "storage paths" are of the format "mwstore://backend/container/path". - * The paths use UNIX file system (FS) notation, though any particular backend may - * not actually be using a local filesystem. Therefore, the paths are only virtual. - * - * Backend contents are stored under wiki-specific container names by default. - * For legacy reasons, this has no effect for the FS backend class, and per-wiki - * segregation must be done by setting the container paths appropriately. - * - * FS-based backends are somewhat more restrictive due to the existence of real - * directory files; a regular file cannot have the same name as a directory. Other - * backends with virtual directories may not have this limitation. Callers should - * store files in such a way that no files and directories are under the same path. - * - * Methods should avoid throwing exceptions at all costs. - * As a corollary, external dependencies should be kept to a minimum. - * - * @ingroup FileBackend - * @since 1.19 - */ -abstract class FileBackend { - protected $name; // string; unique backend name - protected $wikiId; // string; unique wiki name - protected $readOnly; // string; read-only explanation message - /** @var LockManager */ - protected $lockManager; - - /** - * Create a new backend instance from configuration. - * This should only be called from within FileBackendGroup. - * - * $config includes: - * 'name' : The unique name of this backend. - * This should consist of alphanumberic, '-', and '_' characters. - * This name should not be changed after use. - * 'wikiId' : Prefix to container names that is unique to this wiki. - * This should consist of alphanumberic, '-', and '_' characters. - * 'lockManager' : Registered name of a file lock manager to use. - * 'readOnly' : Write operations are disallowed if this is a non-empty string. - * It should be an explanation for the backend being read-only. - * - * @param $config Array - */ - public function __construct( array $config ) { - $this->name = $config['name']; - if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) { - throw new MWException( "Backend name `{$this->name}` is invalid." ); - } - $this->wikiId = isset( $config['wikiId'] ) - ? $config['wikiId'] - : wfWikiID(); // e.g. "my_wiki-en_" - $this->lockManager = ( $config['lockManager'] instanceof LockManager ) - ? $config['lockManager'] - : LockManagerGroup::singleton()->get( $config['lockManager'] ); - $this->readOnly = isset( $config['readOnly'] ) - ? (string)$config['readOnly'] - : ''; - } - - /** - * Get the unique backend name. - * We may have multiple different backends of the same type. - * For example, we can have two Swift backends using different proxies. - * - * @return string - */ - final public function getName() { - return $this->name; - } - - /** - * Check if this backend is read-only - * - * @return bool - */ - final public function isReadOnly() { - return ( $this->readOnly != '' ); - } - - /** - * Get an explanatory message if this backend is read-only - * - * @return string|false Returns falls if the backend is not read-only - */ - final public function getReadOnlyReason() { - return ( $this->readOnly != '' ) ? $this->readOnly : false; - } - - /** - * This is the main entry point into the backend for write operations. - * Callers supply an ordered list of operations to perform as a transaction. - * Files will be locked, the stat cache cleared, and then the operations attempted. - * If any serious errors occur, all attempted operations will be rolled back. - * - * $ops is an array of arrays. The outer array holds a list of operations. - * Each inner array is a set of key value pairs that specify an operation. - * - * Supported operations and their parameters: - * a) Create a new file in storage with the contents of a string - * array( - * 'op' => 'create', - * 'dst' => <storage path>, - * 'content' => <string of new file contents>, - * 'overwrite' => <boolean>, - * 'overwriteSame' => <boolean> - * ) - * b) Copy a file system file into storage - * array( - * 'op' => 'store', - * 'src' => <file system path>, - * 'dst' => <storage path>, - * 'overwrite' => <boolean>, - * 'overwriteSame' => <boolean> - * ) - * c) Copy a file within storage - * array( - * 'op' => 'copy', - * 'src' => <storage path>, - * 'dst' => <storage path>, - * 'overwrite' => <boolean>, - * 'overwriteSame' => <boolean> - * ) - * d) Move a file within storage - * array( - * 'op' => 'move', - * 'src' => <storage path>, - * 'dst' => <storage path>, - * 'overwrite' => <boolean>, - * 'overwriteSame' => <boolean> - * ) - * e) Delete a file within storage - * array( - * 'op' => 'delete', - * 'src' => <storage path>, - * 'ignoreMissingSource' => <boolean> - * ) - * f) Do nothing (no-op) - * array( - * 'op' => 'null', - * ) - * - * Boolean flags for operations (operation-specific): - * 'ignoreMissingSource' : The operation will simply succeed and do - * nothing if the source file does not exist. - * 'overwrite' : Any destination file will be overwritten. - * 'overwriteSame' : An error will not be given if a file already - * exists at the destination that has the same - * contents as the new contents to be written there. - * - * $opts is an associative of boolean flags, including: - * 'force' : Errors that would normally cause a rollback do not. - * The remaining operations are still attempted if any fail. - * 'nonLocking' : No locks are acquired for the operations. - * This can increase performance for non-critical writes. - * This has no effect unless the 'force' flag is set. - * 'allowStale' : Don't require the latest available data. - * This can increase performance for non-critical writes. - * This has no effect unless the 'force' flag is set. - * - * Remarks on locking: - * File system paths given to operations should refer to files that are - * already locked or otherwise safe from modification from other processes. - * Normally these files will be new temp files, which should be adequate. - * - * Return value: - * This returns a Status, which contains all warnings and fatals that occured - * during the operation. The 'failCount', 'successCount', and 'success' members - * will reflect each operation attempted. The status will be "OK" unless: - * a) unexpected operation errors occurred (network partitions, disk full...) - * b) significant operation errors occured and 'force' was not set - * - * @param $ops Array List of operations to execute in order - * @param $opts Array Batch operation options - * @return Status - */ - final public function doOperations( array $ops, array $opts = array() ) { - if ( $this->isReadOnly() ) { - return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); - } - if ( empty( $opts['force'] ) ) { // sanity - unset( $opts['nonLocking'] ); - unset( $opts['allowStale'] ); - } - return $this->doOperationsInternal( $ops, $opts ); - } - - /** - * @see FileBackend::doOperations() - */ - abstract protected function doOperationsInternal( array $ops, array $opts ); - - /** - * Same as doOperations() except it takes a single operation. - * If you are doing a batch of operations that should either - * all succeed or all fail, then use that function instead. - * - * @see FileBackend::doOperations() - * - * @param $op Array Operation - * @param $opts Array Operation options - * @return Status - */ - final public function doOperation( array $op, array $opts = array() ) { - return $this->doOperations( array( $op ), $opts ); - } - - /** - * Performs a single create operation. - * This sets $params['op'] to 'create' and passes it to doOperation(). - * - * @see FileBackend::doOperation() - * - * @param $params Array Operation parameters - * @param $opts Array Operation options - * @return Status - */ - final public function create( array $params, array $opts = array() ) { - $params['op'] = 'create'; - return $this->doOperation( $params, $opts ); - } - - /** - * Performs a single store operation. - * This sets $params['op'] to 'store' and passes it to doOperation(). - * - * @see FileBackend::doOperation() - * - * @param $params Array Operation parameters - * @param $opts Array Operation options - * @return Status - */ - final public function store( array $params, array $opts = array() ) { - $params['op'] = 'store'; - return $this->doOperation( $params, $opts ); - } - - /** - * Performs a single copy operation. - * This sets $params['op'] to 'copy' and passes it to doOperation(). - * - * @see FileBackend::doOperation() - * - * @param $params Array Operation parameters - * @param $opts Array Operation options - * @return Status - */ - final public function copy( array $params, array $opts = array() ) { - $params['op'] = 'copy'; - return $this->doOperation( $params, $opts ); - } - - /** - * Performs a single move operation. - * This sets $params['op'] to 'move' and passes it to doOperation(). - * - * @see FileBackend::doOperation() - * - * @param $params Array Operation parameters - * @param $opts Array Operation options - * @return Status - */ - final public function move( array $params, array $opts = array() ) { - $params['op'] = 'move'; - return $this->doOperation( $params, $opts ); - } - - /** - * Performs a single delete operation. - * This sets $params['op'] to 'delete' and passes it to doOperation(). - * - * @see FileBackend::doOperation() - * - * @param $params Array Operation parameters - * @param $opts Array Operation options - * @return Status - */ - final public function delete( array $params, array $opts = array() ) { - $params['op'] = 'delete'; - return $this->doOperation( $params, $opts ); - } - - /** - * Concatenate a list of storage files into a single file system file. - * The target path should refer to a file that is already locked or - * otherwise safe from modification from other processes. Normally, - * the file will be a new temp file, which should be adequate. - * $params include: - * srcs : ordered source storage paths (e.g. chunk1, chunk2, ...) - * dst : file system path to 0-byte temp file - * - * @param $params Array Operation parameters - * @return Status - */ - abstract public function concatenate( array $params ); - - /** - * Prepare a storage directory for usage. - * This will create any required containers and parent directories. - * Backends using key/value stores only need to create the container. - * - * $params include: - * dir : storage directory - * - * @param $params Array - * @return Status - */ - final public function prepare( array $params ) { - if ( $this->isReadOnly() ) { - return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); - } - return $this->doPrepare( $params ); - } - - /** - * @see FileBackend::prepare() - */ - abstract protected function doPrepare( array $params ); - - /** - * Take measures to block web access to a storage directory and - * the container it belongs to. FS backends might add .htaccess - * files whereas key/value store backends might restrict container - * access to the auth user that represents end-users in web request. - * This is not guaranteed to actually do anything. - * - * $params include: - * dir : storage directory - * noAccess : try to deny file access - * noListing : try to deny file listing - * - * @param $params Array - * @return Status - */ - final public function secure( array $params ) { - if ( $this->isReadOnly() ) { - return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); - } - $status = $this->doPrepare( $params ); // dir must exist to restrict it - if ( $status->isOK() ) { - $status->merge( $this->doSecure( $params ) ); - } - return $status; - } - - /** - * @see FileBackend::secure() - */ - abstract protected function doSecure( array $params ); - - /** - * Delete a storage directory if it is empty. - * Backends using key/value stores may do nothing unless the directory - * is that of an empty container, in which case it should be deleted. - * - * $params include: - * dir : storage directory - * - * @param $params Array - * @return Status - */ - final public function clean( array $params ) { - if ( $this->isReadOnly() ) { - return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); - } - return $this->doClean( $params ); - } - - /** - * @see FileBackend::clean() - */ - abstract protected function doClean( array $params ); - - /** - * Check if a file exists at a storage path in the backend. - * This returns false if only a directory exists at the path. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return bool|null Returns null on failure - */ - abstract public function fileExists( array $params ); - - /** - * Get the last-modified timestamp of the file at a storage path. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return string|false TS_MW timestamp or false on failure - */ - abstract public function getFileTimestamp( array $params ); - - /** - * Get the contents of a file at a storage path in the backend. - * This should be avoided for potentially large files. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return string|false Returns false on failure - */ - abstract public function getFileContents( array $params ); - - /** - * Get the size (bytes) of a file at a storage path in the backend. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return integer|false Returns false on failure - */ - abstract public function getFileSize( array $params ); - - /** - * Get quick information about a file at a storage path in the backend. - * If the file does not exist, then this returns false. - * Otherwise, the result is an associative array that includes: - * mtime : the last-modified timestamp (TS_MW) - * size : the file size (bytes) - * Additional values may be included for internal use only. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return Array|false|null Returns null on failure - */ - abstract public function getFileStat( array $params ); - - /** - * Get a SHA-1 hash of the file at a storage path in the backend. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return string|false Hash string or false on failure - */ - abstract public function getFileSha1Base36( array $params ); - - /** - * Get the properties of the file at a storage path in the backend. - * Returns FSFile::placeholderProps() on failure. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return Array - */ - abstract public function getFileProps( array $params ); - - /** - * Stream the file at a storage path in the backend. - * If the file does not exists, a 404 error will be given. - * Appropriate HTTP headers (Status, Content-Type, Content-Length) - * must be sent if streaming began, while none should be sent otherwise. - * Implementations should flush the output buffer before sending data. - * - * $params include: - * src : source storage path - * headers : additional HTTP headers to send on success - * latest : use the latest available data - * - * @param $params Array - * @return Status - */ - abstract public function streamFile( array $params ); - - /** - * Returns a file system file, identical to the file at a storage path. - * The file returned is either: - * a) A local copy of the file at a storage path in the backend. - * The temporary copy will have the same extension as the source. - * b) An original of the file at a storage path in the backend. - * Temporary files may be purged when the file object falls out of scope. - * - * Write operations should *never* be done on this file as some backends - * may do internal tracking or may be instances of FileBackendMultiWrite. - * In that later case, there are copies of the file that must stay in sync. - * Additionally, further calls to this function may return the same file. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return FSFile|null Returns null on failure - */ - abstract public function getLocalReference( array $params ); - - /** - * Get a local copy on disk of the file at a storage path in the backend. - * The temporary copy will have the same file extension as the source. - * Temporary files may be purged when the file object falls out of scope. - * - * $params include: - * src : source storage path - * latest : use the latest available data - * - * @param $params Array - * @return TempFSFile|null Returns null on failure - */ - abstract public function getLocalCopy( array $params ); - - /** - * Get an iterator to list out all stored files under a storage directory. - * If the directory is of the form "mwstore://backend/container", - * then all files in the container should be listed. - * If the directory is of form "mwstore://backend/container/dir", - * then all files under that container directory should be listed. - * Results should be storage paths relative to the given directory. - * - * Storage backends with eventual consistency might return stale data. - * - * $params include: - * dir : storage path directory - * - * @return Traversable|Array|null Returns null on failure - */ - abstract public function getFileList( array $params ); - - /** - * Invalidate any in-process file existence and property cache. - * If $paths is given, then only the cache for those files will be cleared. - * - * @param $paths Array Storage paths (optional) - * @return void - */ - public function clearCache( array $paths = null ) {} - - /** - * Lock the files at the given storage paths in the backend. - * This will either lock all the files or none (on failure). - * - * Callers should consider using getScopedFileLocks() instead. - * - * @param $paths Array Storage paths - * @param $type integer LockManager::LOCK_* constant - * @return Status - */ - final public function lockFiles( array $paths, $type ) { - return $this->lockManager->lock( $paths, $type ); - } - - /** - * Unlock the files at the given storage paths in the backend. - * - * @param $paths Array Storage paths - * @param $type integer LockManager::LOCK_* constant - * @return Status - */ - final public function unlockFiles( array $paths, $type ) { - return $this->lockManager->unlock( $paths, $type ); - } - - /** - * Lock the files at the given storage paths in the backend. - * This will either lock all the files or none (on failure). - * On failure, the status object will be updated with errors. - * - * Once the return value goes out scope, the locks will be released and - * the status updated. Unlock fatals will not change the status "OK" value. - * - * @param $paths Array Storage paths - * @param $type integer LockManager::LOCK_* constant - * @param $status Status Status to update on lock/unlock - * @return ScopedLock|null Returns null on failure - */ - final public function getScopedFileLocks( array $paths, $type, Status $status ) { - return ScopedLock::factory( $this->lockManager, $paths, $type, $status ); - } - - /** - * Check if a given path is a "mwstore://" path. - * This does not do any further validation or any existence checks. - * - * @param $path string - * @return bool - */ - final public static function isStoragePath( $path ) { - return ( strpos( $path, 'mwstore://' ) === 0 ); - } - - /** - * Split a storage path into a backend name, a container name, - * and a relative file path. The relative path may be the empty string. - * This does not do any path normalization or traversal checks. - * - * @param $storagePath string - * @return Array (backend, container, rel object) or (null, null, null) - */ - final public static function splitStoragePath( $storagePath ) { - if ( self::isStoragePath( $storagePath ) ) { - // Remove the "mwstore://" prefix and split the path - $parts = explode( '/', substr( $storagePath, 10 ), 3 ); - if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) { - if ( count( $parts ) == 3 ) { - return $parts; // e.g. "backend/container/path" - } else { - return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" - } - } - } - return array( null, null, null ); - } - - /** - * Normalize a storage path by cleaning up directory separators. - * Returns null if the path is not of the format of a valid storage path. - * - * @param $storagePath string - * @return string|null - */ - final public static function normalizeStoragePath( $storagePath ) { - list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); - if ( $relPath !== null ) { // must be for this backend - $relPath = self::normalizeContainerPath( $relPath ); - if ( $relPath !== null ) { - return ( $relPath != '' ) - ? "mwstore://{$backend}/{$container}/{$relPath}" - : "mwstore://{$backend}/{$container}"; - } - } - return null; - } - - /** - * Validate and normalize a relative storage path. - * Null is returned if the path involves directory traversal. - * Traversal is insecure for FS backends and broken for others. - * - * @param $path string Storage path relative to a container - * @return string|null - */ - final protected static function normalizeContainerPath( $path ) { - // Normalize directory separators - $path = strtr( $path, '\\', '/' ); - // Collapse any consecutive directory separators - $path = preg_replace( '![/]{2,}!', '/', $path ); - // Remove any leading directory separator - $path = ltrim( $path, '/' ); - // Use the same traversal protection as Title::secureAndSplit() - if ( strpos( $path, '.' ) !== false ) { - if ( - $path === '.' || - $path === '..' || - strpos( $path, './' ) === 0 || - strpos( $path, '../' ) === 0 || - strpos( $path, '/./' ) !== false || - strpos( $path, '/../' ) !== false - ) { - return null; - } - } - return $path; - } - - /** - * Get the parent storage directory of a storage path. - * This returns a path like "mwstore://backend/container", - * "mwstore://backend/container/...", or null if there is no parent. - * - * @param $storagePath string - * @return string|null - */ - final public static function parentStoragePath( $storagePath ) { - $storagePath = dirname( $storagePath ); - list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath ); - return ( $rel === null ) ? null : $storagePath; - } - - /** - * Get the final extension from a storage or FS path - * - * @param $path string - * @return string - */ - final public static function extensionFromPath( $path ) { - $i = strrpos( $path, '.' ); - return strtolower( $i ? substr( $path, $i + 1 ) : '' ); - } -} - -/** - * @brief Base class for all backends associated with a particular storage medium. - * - * This class defines the methods as abstract that subclasses must implement. - * Outside callers should *not* use functions with "Internal" in the name. - * - * The FileBackend operations are implemented using basic functions - * such as storeInternal(), copyInternal(), deleteInternal() and the like. - * This class is also responsible for path resolution and sanitization. - * - * @ingroup FileBackend - * @since 1.19 - */ -abstract class FileBackendStore extends FileBackend { - /** @var Array Map of paths to small (RAM/disk) cache items */ - protected $cache = array(); // (storage path => key => value) - protected $maxCacheSize = 100; // integer; max paths with entries - /** @var Array Map of paths to large (RAM/disk) cache items */ - protected $expensiveCache = array(); // (storage path => key => value) - protected $maxExpensiveCacheSize = 10; // integer; max paths with entries - - /** @var Array Map of container names to sharding settings */ - protected $shardViaHashLevels = array(); // (container name => config array) - - protected $maxFileSize = 1000000000; // integer bytes (1GB) - - /** - * Get the maximum allowable file size given backend - * medium restrictions and basic performance constraints. - * Do not call this function from places outside FileBackend and FileOp. - * - * @return integer Bytes - */ - final public function maxFileSizeInternal() { - return $this->maxFileSize; - } - - /** - * Check if a file can be created at a given storage path. - * FS backends should check if the parent directory exists and the file is writable. - * Backends using key/value stores should check if the container exists. - * - * @param $storagePath string - * @return bool - */ - abstract public function isPathUsableInternal( $storagePath ); - - /** - * Create a file in the backend with the given contents. - * Do not call this function from places outside FileBackend and FileOp. - * - * $params include: - * content : the raw file contents - * dst : destination storage path - * overwrite : overwrite any file that exists at the destination - * - * @param $params Array - * @return Status - */ - final public function createInternal( array $params ) { - wfProfileIn( __METHOD__ ); - if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) { - $status = Status::newFatal( 'backend-fail-create', $params['dst'] ); - } else { - $status = $this->doCreateInternal( $params ); - $this->clearCache( array( $params['dst'] ) ); - } - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::createInternal() - */ - abstract protected function doCreateInternal( array $params ); - - /** - * Store a file into the backend from a file on disk. - * Do not call this function from places outside FileBackend and FileOp. - * - * $params include: - * src : source path on disk - * dst : destination storage path - * overwrite : overwrite any file that exists at the destination - * - * @param $params Array - * @return Status - */ - final public function storeInternal( array $params ) { - wfProfileIn( __METHOD__ ); - if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) { - $status = Status::newFatal( 'backend-fail-store', $params['dst'] ); - } else { - $status = $this->doStoreInternal( $params ); - $this->clearCache( array( $params['dst'] ) ); - } - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::storeInternal() - */ - abstract protected function doStoreInternal( array $params ); - - /** - * Copy a file from one storage path to another in the backend. - * Do not call this function from places outside FileBackend and FileOp. - * - * $params include: - * src : source storage path - * dst : destination storage path - * overwrite : overwrite any file that exists at the destination - * - * @param $params Array - * @return Status - */ - final public function copyInternal( array $params ) { - wfProfileIn( __METHOD__ ); - $status = $this->doCopyInternal( $params ); - $this->clearCache( array( $params['dst'] ) ); - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::copyInternal() - */ - abstract protected function doCopyInternal( array $params ); - - /** - * Delete a file at the storage path. - * Do not call this function from places outside FileBackend and FileOp. - * - * $params include: - * src : source storage path - * ignoreMissingSource : do nothing if the source file does not exist - * - * @param $params Array - * @return Status - */ - final public function deleteInternal( array $params ) { - wfProfileIn( __METHOD__ ); - $status = $this->doDeleteInternal( $params ); - $this->clearCache( array( $params['src'] ) ); - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::deleteInternal() - */ - abstract protected function doDeleteInternal( array $params ); - - /** - * Move a file from one storage path to another in the backend. - * Do not call this function from places outside FileBackend and FileOp. - * - * $params include: - * src : source storage path - * dst : destination storage path - * overwrite : overwrite any file that exists at the destination - * - * @param $params Array - * @return Status - */ - final public function moveInternal( array $params ) { - wfProfileIn( __METHOD__ ); - $status = $this->doMoveInternal( $params ); - $this->clearCache( array( $params['src'], $params['dst'] ) ); - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::moveInternal() - */ - protected function doMoveInternal( array $params ) { - // Copy source to dest - $status = $this->copyInternal( $params ); - if ( $status->isOK() ) { - // Delete source (only fails due to races or medium going down) - $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) ); - $status->setResult( true, $status->value ); // ignore delete() errors - } - return $status; - } - - /** - * @see FileBackend::concatenate() - */ - final public function concatenate( array $params ) { - wfProfileIn( __METHOD__ ); - $status = Status::newGood(); - - // Try to lock the source files for the scope of this function - $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status ); - if ( $status->isOK() ) { - // Actually do the concatenation - $status->merge( $this->doConcatenate( $params ) ); - } - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::concatenate() - */ - protected function doConcatenate( array $params ) { - $status = Status::newGood(); - $tmpPath = $params['dst']; // convenience - - // Check that the specified temp file is valid... - wfSuppressWarnings(); - $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) ); - wfRestoreWarnings(); - if ( !$ok ) { // not present or not empty - $status->fatal( 'backend-fail-opentemp', $tmpPath ); - return $status; - } - - // Build up the temp file using the source chunks (in order)... - $tmpHandle = fopen( $tmpPath, 'ab' ); - if ( $tmpHandle === false ) { - $status->fatal( 'backend-fail-opentemp', $tmpPath ); - return $status; - } - foreach ( $params['srcs'] as $virtualSource ) { - // Get a local FS version of the chunk - $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) ); - if ( !$tmpFile ) { - $status->fatal( 'backend-fail-read', $virtualSource ); - return $status; - } - // Get a handle to the local FS version - $sourceHandle = fopen( $tmpFile->getPath(), 'r' ); - if ( $sourceHandle === false ) { - fclose( $tmpHandle ); - $status->fatal( 'backend-fail-read', $virtualSource ); - return $status; - } - // Append chunk to file (pass chunk size to avoid magic quotes) - if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) { - fclose( $sourceHandle ); - fclose( $tmpHandle ); - $status->fatal( 'backend-fail-writetemp', $tmpPath ); - return $status; - } - fclose( $sourceHandle ); - } - if ( !fclose( $tmpHandle ) ) { - $status->fatal( 'backend-fail-closetemp', $tmpPath ); - return $status; - } - - clearstatcache(); // temp file changed - - return $status; - } - - /** - * @see FileBackend::doPrepare() - */ - final protected function doPrepare( array $params ) { - wfProfileIn( __METHOD__ ); - - $status = Status::newGood(); - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); - if ( $dir === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); - wfProfileOut( __METHOD__ ); - return $status; // invalid storage path - } - - if ( $shard !== null ) { // confined to a single container/shard - $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) ); - } else { // directory is on several shards - wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); - foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { - $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) ); - } - } - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::doPrepare() - */ - protected function doPrepareInternal( $container, $dir, array $params ) { - return Status::newGood(); - } - - /** - * @see FileBackend::doSecure() - */ - final protected function doSecure( array $params ) { - wfProfileIn( __METHOD__ ); - $status = Status::newGood(); - - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); - if ( $dir === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); - wfProfileOut( __METHOD__ ); - return $status; // invalid storage path - } - - if ( $shard !== null ) { // confined to a single container/shard - $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) ); - } else { // directory is on several shards - wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); - foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { - $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) ); - } - } - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::doSecure() - */ - protected function doSecureInternal( $container, $dir, array $params ) { - return Status::newGood(); - } - - /** - * @see FileBackend::doClean() - */ - final protected function doClean( array $params ) { - wfProfileIn( __METHOD__ ); - $status = Status::newGood(); - - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); - if ( $dir === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); - wfProfileOut( __METHOD__ ); - return $status; // invalid storage path - } - - // Attempt to lock this directory... - $filesLockEx = array( $params['dir'] ); - $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); - if ( !$status->isOK() ) { - wfProfileOut( __METHOD__ ); - return $status; // abort - } - - if ( $shard !== null ) { // confined to a single container/shard - $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) ); - } else { // directory is on several shards - wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); - foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { - $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) ); - } - } - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::doClean() - */ - protected function doCleanInternal( $container, $dir, array $params ) { - return Status::newGood(); - } - - /** - * @see FileBackend::fileExists() - */ - final public function fileExists( array $params ) { - wfProfileIn( __METHOD__ ); - $stat = $this->getFileStat( $params ); - wfProfileOut( __METHOD__ ); - return ( $stat === null ) ? null : (bool)$stat; // null => failure - } - - /** - * @see FileBackend::getFileTimestamp() - */ - final public function getFileTimestamp( array $params ) { - wfProfileIn( __METHOD__ ); - $stat = $this->getFileStat( $params ); - wfProfileOut( __METHOD__ ); - return $stat ? $stat['mtime'] : false; - } - - /** - * @see FileBackend::getFileSize() - */ - final public function getFileSize( array $params ) { - wfProfileIn( __METHOD__ ); - $stat = $this->getFileStat( $params ); - wfProfileOut( __METHOD__ ); - return $stat ? $stat['size'] : false; - } - - /** - * @see FileBackend::getFileStat() - */ - final public function getFileStat( array $params ) { - wfProfileIn( __METHOD__ ); - $path = self::normalizeStoragePath( $params['src'] ); - if ( $path === null ) { - return false; // invalid storage path - } - $latest = !empty( $params['latest'] ); - if ( isset( $this->cache[$path]['stat'] ) ) { - // If we want the latest data, check that this cached - // value was in fact fetched with the latest available data. - if ( !$latest || $this->cache[$path]['stat']['latest'] ) { - wfProfileOut( __METHOD__ ); - return $this->cache[$path]['stat']; - } - } - $stat = $this->doGetFileStat( $params ); - if ( is_array( $stat ) ) { // don't cache negatives - $this->trimCache(); // limit memory - $this->cache[$path]['stat'] = $stat; - $this->cache[$path]['stat']['latest'] = $latest; - } - wfProfileOut( __METHOD__ ); - return $stat; - } - - /** - * @see FileBackendStore::getFileStat() - */ - abstract protected function doGetFileStat( array $params ); - - /** - * @see FileBackend::getFileContents() - */ - public function getFileContents( array $params ) { - wfProfileIn( __METHOD__ ); - $tmpFile = $this->getLocalReference( $params ); - if ( !$tmpFile ) { - wfProfileOut( __METHOD__ ); - return false; - } - wfSuppressWarnings(); - $data = file_get_contents( $tmpFile->getPath() ); - wfRestoreWarnings(); - wfProfileOut( __METHOD__ ); - return $data; - } - - /** - * @see FileBackend::getFileSha1Base36() - */ - final public function getFileSha1Base36( array $params ) { - wfProfileIn( __METHOD__ ); - $path = $params['src']; - if ( isset( $this->cache[$path]['sha1'] ) ) { - wfProfileOut( __METHOD__ ); - return $this->cache[$path]['sha1']; - } - $hash = $this->doGetFileSha1Base36( $params ); - if ( $hash ) { // don't cache negatives - $this->trimCache(); // limit memory - $this->cache[$path]['sha1'] = $hash; - } - wfProfileOut( __METHOD__ ); - return $hash; - } - - /** - * @see FileBackendStore::getFileSha1Base36() - */ - protected function doGetFileSha1Base36( array $params ) { - $fsFile = $this->getLocalReference( $params ); - if ( !$fsFile ) { - return false; - } else { - return $fsFile->getSha1Base36(); - } - } - - /** - * @see FileBackend::getFileProps() - */ - final public function getFileProps( array $params ) { - wfProfileIn( __METHOD__ ); - $fsFile = $this->getLocalReference( $params ); - $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps(); - wfProfileOut( __METHOD__ ); - return $props; - } - - /** - * @see FileBackend::getLocalReference() - */ - public function getLocalReference( array $params ) { - wfProfileIn( __METHOD__ ); - $path = $params['src']; - if ( isset( $this->expensiveCache[$path]['localRef'] ) ) { - wfProfileOut( __METHOD__ ); - return $this->expensiveCache[$path]['localRef']; - } - $tmpFile = $this->getLocalCopy( $params ); - if ( $tmpFile ) { // don't cache negatives - $this->trimExpensiveCache(); // limit memory - $this->expensiveCache[$path]['localRef'] = $tmpFile; - } - wfProfileOut( __METHOD__ ); - return $tmpFile; - } - - /** - * @see FileBackend::streamFile() - */ - final public function streamFile( array $params ) { - wfProfileIn( __METHOD__ ); - $status = Status::newGood(); - - $info = $this->getFileStat( $params ); - if ( !$info ) { // let StreamFile handle the 404 - $status->fatal( 'backend-fail-notexists', $params['src'] ); - } - - // Set output buffer and HTTP headers for stream - $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array(); - $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders ); - if ( $res == StreamFile::NOT_MODIFIED ) { - // do nothing; client cache is up to date - } elseif ( $res == StreamFile::READY_STREAM ) { - $status = $this->doStreamFile( $params ); - } else { - $status->fatal( 'backend-fail-stream', $params['src'] ); - } - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackendStore::streamFile() - */ - protected function doStreamFile( array $params ) { - $status = Status::newGood(); - - $fsFile = $this->getLocalReference( $params ); - if ( !$fsFile ) { - $status->fatal( 'backend-fail-stream', $params['src'] ); - } elseif ( !readfile( $fsFile->getPath() ) ) { - $status->fatal( 'backend-fail-stream', $params['src'] ); - } - - return $status; - } - - /** - * @copydoc FileBackend::getFileList() - */ - final public function getFileList( array $params ) { - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); - if ( $dir === null ) { // invalid storage path - return null; - } - if ( $shard !== null ) { - // File listing is confined to a single container/shard - return $this->getFileListInternal( $fullCont, $dir, $params ); - } else { - wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); - // File listing spans multiple containers/shards - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); - return new FileBackendStoreShardListIterator( $this, - $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params ); - } - } - - /** - * Do not call this function from places outside FileBackend - * - * @see FileBackendStore::getFileList() - * - * @param $container string Resolved container name - * @param $dir string Resolved path relative to container - * @param $params Array - * @return Traversable|Array|null - */ - abstract public function getFileListInternal( $container, $dir, array $params ); - - /** - * Get the list of supported operations and their corresponding FileOp classes. - * - * @return Array - */ - protected function supportedOperations() { - return array( - 'store' => 'StoreFileOp', - 'copy' => 'CopyFileOp', - 'move' => 'MoveFileOp', - 'delete' => 'DeleteFileOp', - 'create' => 'CreateFileOp', - 'null' => 'NullFileOp' - ); - } - - /** - * Return a list of FileOp objects from a list of operations. - * Do not call this function from places outside FileBackend. - * - * The result must have the same number of items as the input. - * An exception is thrown if an unsupported operation is requested. - * - * @param $ops Array Same format as doOperations() - * @return Array List of FileOp objects - * @throws MWException - */ - final public function getOperations( array $ops ) { - $supportedOps = $this->supportedOperations(); - - $performOps = array(); // array of FileOp objects - // Build up ordered array of FileOps... - foreach ( $ops as $operation ) { - $opName = $operation['op']; - if ( isset( $supportedOps[$opName] ) ) { - $class = $supportedOps[$opName]; - // Get params for this operation - $params = $operation; - // Append the FileOp class - $performOps[] = new $class( $this, $params ); - } else { - throw new MWException( "Operation `$opName` is not supported." ); - } - } - - return $performOps; - } - - /** - * @see FileBackend::doOperationsInternal() - */ - protected function doOperationsInternal( array $ops, array $opts ) { - wfProfileIn( __METHOD__ ); - $status = Status::newGood(); - - // Build up a list of FileOps... - $performOps = $this->getOperations( $ops ); - - // Acquire any locks as needed... - if ( empty( $opts['nonLocking'] ) ) { - // Build up a list of files to lock... - $filesLockEx = $filesLockSh = array(); - foreach ( $performOps as $fileOp ) { - $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() ); - $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() ); - } - // Optimization: if doing an EX lock anyway, don't also set an SH one - $filesLockSh = array_diff( $filesLockSh, $filesLockEx ); - // Get a shared lock on the parent directory of each path changed - $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) ); - // Try to lock those files for the scope of this function... - $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status ); - $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); - if ( !$status->isOK() ) { - wfProfileOut( __METHOD__ ); - return $status; // abort - } - } - - // Clear any cache entries (after locks acquired) - $this->clearCache(); - - // Actually attempt the operation batch... - $subStatus = FileOp::attemptBatch( $performOps, $opts ); - - // Merge errors into status fields - $status->merge( $subStatus ); - $status->success = $subStatus->success; // not done in merge() - - wfProfileOut( __METHOD__ ); - return $status; - } - - /** - * @see FileBackend::clearCache() - */ - final public function clearCache( array $paths = null ) { - if ( is_array( $paths ) ) { - $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); - $paths = array_filter( $paths, 'strlen' ); // remove nulls - } - if ( $paths === null ) { - $this->cache = array(); - $this->expensiveCache = array(); - } else { - foreach ( $paths as $path ) { - unset( $this->cache[$path] ); - unset( $this->expensiveCache[$path] ); - } - } - $this->doClearCache( $paths ); - } - - /** - * Clears any additional stat caches for storage paths - * - * @see FileBackend::clearCache() - * - * @param $paths Array Storage paths (optional) - * @return void - */ - protected function doClearCache( array $paths = null ) {} - - /** - * Prune the inexpensive cache if it is too big to add an item - * - * @return void - */ - protected function trimCache() { - if ( count( $this->cache ) >= $this->maxCacheSize ) { - reset( $this->cache ); - unset( $this->cache[key( $this->cache )] ); - } - } - - /** - * Prune the expensive cache if it is too big to add an item - * - * @return void - */ - protected function trimExpensiveCache() { - if ( count( $this->expensiveCache ) >= $this->maxExpensiveCacheSize ) { - reset( $this->expensiveCache ); - unset( $this->expensiveCache[key( $this->expensiveCache )] ); - } - } - - /** - * Check if a container name is valid. - * This checks for for length and illegal characters. - * - * @param $container string - * @return bool - */ - final protected static function isValidContainerName( $container ) { - // This accounts for Swift and S3 restrictions while leaving room - // for things like '.xxx' (hex shard chars) or '.seg' (segments). - // This disallows directory separators or traversal characters. - // Note that matching strings URL encode to the same string; - // in Swift, the length restriction is *after* URL encoding. - return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container ); - } - - /** - * Splits a storage path into an internal container name, - * an internal relative file name, and a container shard suffix. - * Any shard suffix is already appended to the internal container name. - * This also checks that the storage path is valid and within this backend. - * - * If the container is sharded but a suffix could not be determined, - * this means that the path can only refer to a directory and can only - * be scanned by looking in all the container shards. - * - * @param $storagePath string - * @return Array (container, path, container suffix) or (null, null, null) if invalid - */ - final protected function resolveStoragePath( $storagePath ) { - list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); - if ( $backend === $this->name ) { // must be for this backend - $relPath = self::normalizeContainerPath( $relPath ); - if ( $relPath !== null ) { - // Get shard for the normalized path if this container is sharded - $cShard = $this->getContainerShard( $container, $relPath ); - // Validate and sanitize the relative path (backend-specific) - $relPath = $this->resolveContainerPath( $container, $relPath ); - if ( $relPath !== null ) { - // Prepend any wiki ID prefix to the container name - $container = $this->fullContainerName( $container ); - if ( self::isValidContainerName( $container ) ) { - // Validate and sanitize the container name (backend-specific) - $container = $this->resolveContainerName( "{$container}{$cShard}" ); - if ( $container !== null ) { - return array( $container, $relPath, $cShard ); - } - } - } - } - } - return array( null, null, null ); - } - - /** - * Like resolveStoragePath() except null values are returned if - * the container is sharded and the shard could not be determined. - * - * @see FileBackendStore::resolveStoragePath() - * - * @param $storagePath string - * @return Array (container, path) or (null, null) if invalid - */ - final protected function resolveStoragePathReal( $storagePath ) { - list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath ); - if ( $cShard !== null ) { - return array( $container, $relPath ); - } - return array( null, null ); - } - - /** - * Get the container name shard suffix for a given path. - * Any empty suffix means the container is not sharded. - * - * @param $container string Container name - * @param $relStoragePath string Storage path relative to the container - * @return string|null Returns null if shard could not be determined - */ - final protected function getContainerShard( $container, $relPath ) { - list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container ); - if ( $levels == 1 || $levels == 2 ) { - // Hash characters are either base 16 or 36 - $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]'; - // Get a regex that represents the shard portion of paths. - // The concatenation of the captures gives us the shard. - if ( $levels === 1 ) { // 16 or 36 shards per container - $hashDirRegex = '(' . $char . ')'; - } else { // 256 or 1296 shards per container - if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc") - $hashDirRegex = $char . '/(' . $char . '{2})'; - } else { // short hash dir format (e.g. "a/b/c") - $hashDirRegex = '(' . $char . ')/(' . $char . ')'; - } - } - // Allow certain directories to be above the hash dirs so as - // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab"). - // They must be 2+ chars to avoid any hash directory ambiguity. - $m = array(); - if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) { - return '.' . implode( '', array_slice( $m, 1 ) ); - } - return null; // failed to match - } - return ''; // no sharding - } - - /** - * Get the sharding config for a container. - * If greater than 0, then all file storage paths within - * the container are required to be hashed accordingly. - * - * @param $container string - * @return Array (integer levels, integer base, repeat flag) or (0, 0, false) - */ - final protected function getContainerHashLevels( $container ) { - if ( isset( $this->shardViaHashLevels[$container] ) ) { - $config = $this->shardViaHashLevels[$container]; - $hashLevels = (int)$config['levels']; - if ( $hashLevels == 1 || $hashLevels == 2 ) { - $hashBase = (int)$config['base']; - if ( $hashBase == 16 || $hashBase == 36 ) { - return array( $hashLevels, $hashBase, $config['repeat'] ); - } - } - } - return array( 0, 0, false ); // no sharding - } - - /** - * Get a list of full container shard suffixes for a container - * - * @param $container string - * @return Array - */ - final protected function getContainerSuffixes( $container ) { - $shards = array(); - list( $digits, $base ) = $this->getContainerHashLevels( $container ); - if ( $digits > 0 ) { - $numShards = pow( $base, $digits ); - for ( $index = 0; $index < $numShards; $index++ ) { - $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits ); - } - } - return $shards; - } - - /** - * Get the full container name, including the wiki ID prefix - * - * @param $container string - * @return string - */ - final protected function fullContainerName( $container ) { - if ( $this->wikiId != '' ) { - return "{$this->wikiId}-$container"; - } else { - return $container; - } - } - - /** - * Resolve a container name, checking if it's allowed by the backend. - * This is intended for internal use, such as encoding illegal chars. - * Subclasses can override this to be more restrictive. - * - * @param $container string - * @return string|null - */ - protected function resolveContainerName( $container ) { - return $container; - } - - /** - * Resolve a relative storage path, checking if it's allowed by the backend. - * This is intended for internal use, such as encoding illegal chars or perhaps - * getting absolute paths (e.g. FS based backends). Note that the relative path - * may be the empty string (e.g. the path is simply to the container). - * - * @param $container string Container name - * @param $relStoragePath string Storage path relative to the container - * @return string|null Path or null if not valid - */ - protected function resolveContainerPath( $container, $relStoragePath ) { - return $relStoragePath; - } -} - -/** - * FileBackendStore helper function to handle file listings that span container shards. - * Do not use this class from places outside of FileBackendStore. - * - * @ingroup FileBackend - */ -class FileBackendStoreShardListIterator implements Iterator { - /* @var FileBackendStore */ - protected $backend; - /* @var Array */ - protected $params; - /* @var Array */ - protected $shardSuffixes; - protected $container; // string - protected $directory; // string - - /* @var Traversable */ - protected $iter; - protected $curShard = 0; // integer - protected $pos = 0; // integer - - /** - * @param $backend FileBackendStore - * @param $container string Full storage container name - * @param $dir string Storage directory relative to container - * @param $suffixes Array List of container shard suffixes - * @param $params Array - */ - public function __construct( - FileBackendStore $backend, $container, $dir, array $suffixes, array $params - ) { - $this->backend = $backend; - $this->container = $container; - $this->directory = $dir; - $this->shardSuffixes = $suffixes; - $this->params = $params; - } - - public function current() { - if ( is_array( $this->iter ) ) { - return current( $this->iter ); - } else { - return $this->iter->current(); - } - } - - public function key() { - return $this->pos; - } - - public function next() { - ++$this->pos; - if ( is_array( $this->iter ) ) { - next( $this->iter ); - } else { - $this->iter->next(); - } - // Find the next non-empty shard if no elements are left - $this->nextShardIteratorIfNotValid(); - } - - /** - * If the iterator for this container shard is out of items, - * then move on to the next container that has items. - * If there are none, then it advances to the last container. - */ - protected function nextShardIteratorIfNotValid() { - while ( !$this->valid() ) { - if ( ++$this->curShard >= count( $this->shardSuffixes ) ) { - break; // no more container shards - } - $this->setIteratorFromCurrentShard(); - } - } - - protected function setIteratorFromCurrentShard() { - $suffix = $this->shardSuffixes[$this->curShard]; - $this->iter = $this->backend->getFileListInternal( - "{$this->container}{$suffix}", $this->directory, $this->params ); - } - - public function rewind() { - $this->pos = 0; - $this->curShard = 0; - $this->setIteratorFromCurrentShard(); - // Find the next non-empty shard if this one has no elements - $this->nextShardIteratorIfNotValid(); - } - - public function valid() { - if ( $this->iter == null ) { - return false; // some failure? - } elseif ( is_array( $this->iter ) ) { - return ( current( $this->iter ) !== false ); // no paths can have this value - } else { - return $this->iter->valid(); - } - } -} diff --git a/includes/filerepo/backend/FileBackendMultiWrite.php b/includes/filerepo/backend/FileBackendMultiWrite.php deleted file mode 100644 index c0f1ac57..00000000 --- a/includes/filerepo/backend/FileBackendMultiWrite.php +++ /dev/null @@ -1,420 +0,0 @@ -<?php -/** - * @file - * @ingroup FileBackend - * @author Aaron Schulz - */ - -/** - * This class defines a multi-write backend. Multiple backends can be - * registered to this proxy backend and it will act as a single backend. - * Use this when all access to those backends is through this proxy backend. - * At least one of the backends must be declared the "master" backend. - * - * Only use this class when transitioning from one storage system to another. - * - * Read operations are only done on the 'master' backend for consistency. - * Write operations are performed on all backends, in the order defined. - * If an operation fails on one backend it will be rolled back from the others. - * - * @ingroup FileBackend - * @since 1.19 - */ -class FileBackendMultiWrite extends FileBackend { - /** @var Array Prioritized list of FileBackendStore objects */ - protected $backends = array(); // array of (backend index => backends) - protected $masterIndex = -1; // integer; index of master backend - protected $syncChecks = 0; // integer bitfield - - /* Possible internal backend consistency checks */ - const CHECK_SIZE = 1; - const CHECK_TIME = 2; - - /** - * Construct a proxy backend that consists of several internal backends. - * Additional $config params include: - * 'backends' : Array of backend config and multi-backend settings. - * Each value is the config used in the constructor of a - * FileBackendStore class, but with these additional settings: - * 'class' : The name of the backend class - * 'isMultiMaster' : This must be set for one backend. - * 'syncChecks' : Integer bitfield of internal backend sync checks to perform. - * Possible bits include self::CHECK_SIZE and self::CHECK_TIME. - * The checks are done before allowing any file operations. - * @param $config Array - */ - public function __construct( array $config ) { - parent::__construct( $config ); - $namesUsed = array(); - // Construct backends here rather than via registration - // to keep these backends hidden from outside the proxy. - foreach ( $config['backends'] as $index => $config ) { - $name = $config['name']; - if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates - throw new MWException( "Two or more backends defined with the name $name." ); - } - $namesUsed[$name] = 1; - if ( !isset( $config['class'] ) ) { - throw new MWException( 'No class given for a backend config.' ); - } - $class = $config['class']; - $this->backends[$index] = new $class( $config ); - if ( !empty( $config['isMultiMaster'] ) ) { - if ( $this->masterIndex >= 0 ) { - throw new MWException( 'More than one master backend defined.' ); - } - $this->masterIndex = $index; - } - } - if ( $this->masterIndex < 0 ) { // need backends and must have a master - throw new MWException( 'No master backend defined.' ); - } - $this->syncChecks = isset( $config['syncChecks'] ) - ? $config['syncChecks'] - : self::CHECK_SIZE; - } - - /** - * @see FileBackend::doOperationsInternal() - */ - final protected function doOperationsInternal( array $ops, array $opts ) { - $status = Status::newGood(); - - $performOps = array(); // list of FileOp objects - $filesRead = $filesChanged = array(); // storage paths used - // Build up a list of FileOps. The list will have all the ops - // for one backend, then all the ops for the next, and so on. - // These batches of ops are all part of a continuous array. - // Also build up a list of files read/changed... - foreach ( $this->backends as $index => $backend ) { - $backendOps = $this->substOpBatchPaths( $ops, $backend ); - // Add on the operation batch for this backend - $performOps = array_merge( $performOps, $backend->getOperations( $backendOps ) ); - if ( $index == 0 ) { // first batch - // Get the files used for these operations. Each backend has a batch of - // the same operations, so we only need to get them from the first batch. - foreach ( $performOps as $fileOp ) { - $filesRead = array_merge( $filesRead, $fileOp->storagePathsRead() ); - $filesChanged = array_merge( $filesChanged, $fileOp->storagePathsChanged() ); - } - // Get the paths under the proxy backend's name - $filesRead = $this->unsubstPaths( $filesRead ); - $filesChanged = $this->unsubstPaths( $filesChanged ); - } - } - - // Try to lock those files for the scope of this function... - if ( empty( $opts['nonLocking'] ) ) { - $filesLockSh = array_diff( $filesRead, $filesChanged ); // optimization - $filesLockEx = $filesChanged; - // Get a shared lock on the parent directory of each path changed - $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) ); - // Try to lock those files for the scope of this function... - $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status ); - $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); - if ( !$status->isOK() ) { - return $status; // abort - } - } - - // Clear any cache entries (after locks acquired) - $this->clearCache(); - - // Do a consistency check to see if the backends agree - if ( count( $this->backends ) > 1 ) { - $status->merge( $this->consistencyCheck( array_merge( $filesRead, $filesChanged ) ) ); - if ( !$status->isOK() ) { - return $status; // abort - } - } - - // Actually attempt the operation batch... - $subStatus = FileOp::attemptBatch( $performOps, $opts ); - - $success = array(); - $failCount = $successCount = 0; - // Make 'success', 'successCount', and 'failCount' fields reflect - // the overall operation, rather than all the batches for each backend. - // Do this by only using success values from the master backend's batch. - $batchStart = $this->masterIndex * count( $ops ); - $batchEnd = $batchStart + count( $ops ) - 1; - for ( $i = $batchStart; $i <= $batchEnd; $i++ ) { - if ( !isset( $subStatus->success[$i] ) ) { - break; // failed out before trying this op - } elseif ( $subStatus->success[$i] ) { - ++$successCount; - } else { - ++$failCount; - } - $success[] = $subStatus->success[$i]; - } - $subStatus->success = $success; - $subStatus->successCount = $successCount; - $subStatus->failCount = $failCount; - - // Merge errors into status fields - $status->merge( $subStatus ); - $status->success = $subStatus->success; // not done in merge() - - return $status; - } - - /** - * Check that a set of files are consistent across all internal backends - * - * @param $paths Array - * @return Status - */ - public function consistencyCheck( array $paths ) { - $status = Status::newGood(); - if ( $this->syncChecks == 0 ) { - return $status; // skip checks - } - - $mBackend = $this->backends[$this->masterIndex]; - foreach ( array_unique( $paths ) as $path ) { - $params = array( 'src' => $path, 'latest' => true ); - // Stat the file on the 'master' backend - $mStat = $mBackend->getFileStat( $this->substOpPaths( $params, $mBackend ) ); - // Check of all clone backends agree with the master... - foreach ( $this->backends as $index => $cBackend ) { - if ( $index === $this->masterIndex ) { - continue; // master - } - $cStat = $cBackend->getFileStat( $this->substOpPaths( $params, $cBackend ) ); - if ( $mStat ) { // file is in master - if ( !$cStat ) { // file should exist - $status->fatal( 'backend-fail-synced', $path ); - continue; - } - if ( $this->syncChecks & self::CHECK_SIZE ) { - if ( $cStat['size'] != $mStat['size'] ) { // wrong size - $status->fatal( 'backend-fail-synced', $path ); - continue; - } - } - if ( $this->syncChecks & self::CHECK_TIME ) { - $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] ); - $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] ); - if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere - $status->fatal( 'backend-fail-synced', $path ); - continue; - } - } - } else { // file is not in master - if ( $cStat ) { // file should not exist - $status->fatal( 'backend-fail-synced', $path ); - } - } - } - } - - return $status; - } - - /** - * Substitute the backend name in storage path parameters - * for a set of operations with that of a given internal backend. - * - * @param $ops Array List of file operation arrays - * @param $backend FileBackendStore - * @return Array - */ - protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) { - $newOps = array(); // operations - foreach ( $ops as $op ) { - $newOp = $op; // operation - foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) { - if ( isset( $newOp[$par] ) ) { // string or array - $newOp[$par] = $this->substPaths( $newOp[$par], $backend ); - } - } - $newOps[] = $newOp; - } - return $newOps; - } - - /** - * Same as substOpBatchPaths() but for a single operation - * - * @param $op File operation array - * @param $backend FileBackendStore - * @return Array - */ - protected function substOpPaths( array $ops, FileBackendStore $backend ) { - $newOps = $this->substOpBatchPaths( array( $ops ), $backend ); - return $newOps[0]; - } - - /** - * Substitute the backend of storage paths with an internal backend's name - * - * @param $paths Array|string List of paths or single string path - * @param $backend FileBackendStore - * @return Array|string - */ - protected function substPaths( $paths, FileBackendStore $backend ) { - return preg_replace( - '!^mwstore://' . preg_quote( $this->name ) . '/!', - StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ), - $paths // string or array - ); - } - - /** - * Substitute the backend of internal storage paths with the proxy backend's name - * - * @param $paths Array|string List of paths or single string path - * @return Array|string - */ - protected function unsubstPaths( $paths ) { - return preg_replace( - '!^mwstore://([^/]+)!', - StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ), - $paths // string or array - ); - } - - /** - * @see FileBackend::doPrepare() - */ - public function doPrepare( array $params ) { - $status = Status::newGood(); - foreach ( $this->backends as $backend ) { - $realParams = $this->substOpPaths( $params, $backend ); - $status->merge( $backend->doPrepare( $realParams ) ); - } - return $status; - } - - /** - * @see FileBackend::doSecure() - */ - public function doSecure( array $params ) { - $status = Status::newGood(); - foreach ( $this->backends as $backend ) { - $realParams = $this->substOpPaths( $params, $backend ); - $status->merge( $backend->doSecure( $realParams ) ); - } - return $status; - } - - /** - * @see FileBackend::doClean() - */ - public function doClean( array $params ) { - $status = Status::newGood(); - foreach ( $this->backends as $backend ) { - $realParams = $this->substOpPaths( $params, $backend ); - $status->merge( $backend->doClean( $realParams ) ); - } - return $status; - } - - /** - * @see FileBackend::getFileList() - */ - public function concatenate( array $params ) { - // We are writing to an FS file, so we don't need to do this per-backend - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->concatenate( $realParams ); - } - - /** - * @see FileBackend::fileExists() - */ - public function fileExists( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->fileExists( $realParams ); - } - - /** - * @see FileBackend::getFileTimestamp() - */ - public function getFileTimestamp( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams ); - } - - /** - * @see FileBackend::getFileSize() - */ - public function getFileSize( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getFileSize( $realParams ); - } - - /** - * @see FileBackend::getFileStat() - */ - public function getFileStat( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getFileStat( $realParams ); - } - - /** - * @see FileBackend::getFileContents() - */ - public function getFileContents( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getFileContents( $realParams ); - } - - /** - * @see FileBackend::getFileSha1Base36() - */ - public function getFileSha1Base36( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams ); - } - - /** - * @see FileBackend::getFileProps() - */ - public function getFileProps( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getFileProps( $realParams ); - } - - /** - * @see FileBackend::streamFile() - */ - public function streamFile( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->streamFile( $realParams ); - } - - /** - * @see FileBackend::getLocalReference() - */ - public function getLocalReference( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getLocalReference( $realParams ); - } - - /** - * @see FileBackend::getLocalCopy() - */ - public function getLocalCopy( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getLocalCopy( $realParams ); - } - - /** - * @see FileBackend::getFileList() - */ - public function getFileList( array $params ) { - $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); - return $this->backends[$this->masterIndex]->getFileList( $realParams ); - } - - /** - * @see FileBackend::clearCache() - */ - public function clearCache( array $paths = null ) { - foreach ( $this->backends as $backend ) { - $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null; - $backend->clearCache( $realPaths ); - } - } -} diff --git a/includes/filerepo/backend/SwiftFileBackend.php b/includes/filerepo/backend/SwiftFileBackend.php deleted file mode 100644 index a287f488..00000000 --- a/includes/filerepo/backend/SwiftFileBackend.php +++ /dev/null @@ -1,877 +0,0 @@ -<?php -/** - * @file - * @ingroup FileBackend - * @author Russ Nelson - * @author Aaron Schulz - */ - -/** - * Class for an OpenStack Swift based file backend. - * - * This requires the SwiftCloudFiles MediaWiki extension, which includes - * the php-cloudfiles library (https://github.com/rackspace/php-cloudfiles). - * php-cloudfiles requires the curl, fileinfo, and mb_string PHP extensions. - * - * Status messages should avoid mentioning the Swift account name. - * Likewise, error suppression should be used to avoid path disclosure. - * - * @ingroup FileBackend - * @since 1.19 - */ -class SwiftFileBackend extends FileBackendStore { - /** @var CF_Authentication */ - protected $auth; // Swift authentication handler - protected $authTTL; // integer seconds - protected $swiftAnonUser; // string; username to handle unauthenticated requests - protected $maxContCacheSize = 100; // integer; max containers with entries - - /** @var CF_Connection */ - protected $conn; // Swift connection handle - protected $connStarted = 0; // integer UNIX timestamp - protected $connContainers = array(); // container object cache - - /** - * @see FileBackendStore::__construct() - * Additional $config params include: - * swiftAuthUrl : Swift authentication server URL - * swiftUser : Swift user used by MediaWiki (account:username) - * swiftKey : Swift authentication key for the above user - * swiftAuthTTL : Swift authentication TTL (seconds) - * swiftAnonUser : Swift user used for end-user requests (account:username) - * shardViaHashLevels : Map of container names to sharding config with: - * 'base' : base of hash characters, 16 or 36 - * 'levels' : the number of hash levels (and digits) - * 'repeat' : hash subdirectories are prefixed with all the - * parent hash directory names (e.g. "a/ab/abc") - */ - public function __construct( array $config ) { - parent::__construct( $config ); - // Required settings - $this->auth = new CF_Authentication( - $config['swiftUser'], - $config['swiftKey'], - null, // account; unused - $config['swiftAuthUrl'] - ); - // Optional settings - $this->authTTL = isset( $config['swiftAuthTTL'] ) - ? $config['swiftAuthTTL'] - : 120; // some sane number - $this->swiftAnonUser = isset( $config['swiftAnonUser'] ) - ? $config['swiftAnonUser'] - : ''; - $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] ) - ? $config['shardViaHashLevels'] - : ''; - } - - /** - * @see FileBackendStore::resolveContainerPath() - */ - protected function resolveContainerPath( $container, $relStoragePath ) { - if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) { - return null; // too long for Swift - } - return $relStoragePath; - } - - /** - * @see FileBackendStore::isPathUsableInternal() - */ - public function isPathUsableInternal( $storagePath ) { - list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath ); - if ( $rel === null ) { - return false; // invalid - } - - try { - $this->getContainer( $container ); - return true; // container exists - } catch ( NoSuchContainerException $e ) { - } catch ( InvalidResponseException $e ) { - } catch ( Exception $e ) { // some other exception? - $this->logException( $e, __METHOD__, array( 'path' => $storagePath ) ); - } - - return false; - } - - /** - * @see FileBackendStore::doCreateInternal() - */ - protected function doCreateInternal( array $params ) { - $status = Status::newGood(); - - list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); - if ( $dstRel === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); - return $status; - } - - // (a) Check the destination container and object - try { - $dContObj = $this->getContainer( $dstCont ); - if ( empty( $params['overwrite'] ) && - $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) ) - { - $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); - return $status; - } - } catch ( NoSuchContainerException $e ) { - $status->fatal( 'backend-fail-create', $params['dst'] ); - return $status; - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - - // (b) Get a SHA-1 hash of the object - $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 ); - - // (c) Actually create the object - try { - // Create a fresh CF_Object with no fields preloaded. - // We don't want to preserve headers, metadata, and such. - $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD - // Note: metadata keys stored as [Upper case char][[Lower case char]...] - $obj->metadata = array( 'Sha1base36' => $sha1Hash ); - // Manually set the ETag (https://github.com/rackspace/php-cloudfiles/issues/59). - // The MD5 here will be checked within Swift against its own MD5. - $obj->set_etag( md5( $params['content'] ) ); - // Use the same content type as StreamFile for security - $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] ); - // Actually write the object in Swift - $obj->write( $params['content'] ); - } catch ( BadContentTypeException $e ) { - $status->fatal( 'backend-fail-contenttype', $params['dst'] ); - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - } - - return $status; - } - - /** - * @see FileBackendStore::doStoreInternal() - */ - protected function doStoreInternal( array $params ) { - $status = Status::newGood(); - - list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); - if ( $dstRel === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); - return $status; - } - - // (a) Check the destination container and object - try { - $dContObj = $this->getContainer( $dstCont ); - if ( empty( $params['overwrite'] ) && - $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) ) - { - $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); - return $status; - } - } catch ( NoSuchContainerException $e ) { - $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); - return $status; - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - - // (b) Get a SHA-1 hash of the object - $sha1Hash = sha1_file( $params['src'] ); - if ( $sha1Hash === false ) { // source doesn't exist? - $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); - return $status; - } - $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 ); - - // (c) Actually store the object - try { - // Create a fresh CF_Object with no fields preloaded. - // We don't want to preserve headers, metadata, and such. - $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD - // Note: metadata keys stored as [Upper case char][[Lower case char]...] - $obj->metadata = array( 'Sha1base36' => $sha1Hash ); - // The MD5 here will be checked within Swift against its own MD5. - $obj->set_etag( md5_file( $params['src'] ) ); - // Use the same content type as StreamFile for security - $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] ); - // Actually write the object in Swift - $obj->load_from_filename( $params['src'], True ); // calls $obj->write() - } catch ( BadContentTypeException $e ) { - $status->fatal( 'backend-fail-contenttype', $params['dst'] ); - } catch ( IOException $e ) { - $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - } - - return $status; - } - - /** - * @see FileBackendStore::doCopyInternal() - */ - protected function doCopyInternal( array $params ) { - $status = Status::newGood(); - - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); - if ( $srcRel === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['src'] ); - return $status; - } - - list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); - if ( $dstRel === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); - return $status; - } - - // (a) Check the source/destination containers and destination object - try { - $sContObj = $this->getContainer( $srcCont ); - $dContObj = $this->getContainer( $dstCont ); - if ( empty( $params['overwrite'] ) && - $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) ) - { - $status->fatal( 'backend-fail-alreadyexists', $params['dst'] ); - return $status; - } - } catch ( NoSuchContainerException $e ) { - $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); - return $status; - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - - // (b) Actually copy the file to the destination - try { - $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel ); - } catch ( NoSuchObjectException $e ) { // source object does not exist - $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - } - - return $status; - } - - /** - * @see FileBackendStore::doDeleteInternal() - */ - protected function doDeleteInternal( array $params ) { - $status = Status::newGood(); - - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); - if ( $srcRel === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['src'] ); - return $status; - } - - try { - $sContObj = $this->getContainer( $srcCont ); - $sContObj->delete_object( $srcRel ); - } catch ( NoSuchContainerException $e ) { - $status->fatal( 'backend-fail-delete', $params['src'] ); - } catch ( NoSuchObjectException $e ) { - if ( empty( $params['ignoreMissingSource'] ) ) { - $status->fatal( 'backend-fail-delete', $params['src'] ); - } - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - } - - return $status; - } - - /** - * @see FileBackendStore::doPrepareInternal() - */ - protected function doPrepareInternal( $fullCont, $dir, array $params ) { - $status = Status::newGood(); - - // (a) Check if container already exists - try { - $contObj = $this->getContainer( $fullCont ); - // NoSuchContainerException not thrown: container must exist - return $status; // already exists - } catch ( NoSuchContainerException $e ) { - // NoSuchContainerException thrown: container does not exist - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - - // (b) Create container as needed - try { - $contObj = $this->createContainer( $fullCont ); - if ( $this->swiftAnonUser != '' ) { - // Make container public to end-users... - $status->merge( $this->setContainerAccess( - $contObj, - array( $this->auth->username, $this->swiftAnonUser ), // read - array( $this->auth->username ) // write - ) ); - } - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - - return $status; - } - - /** - * @see FileBackendStore::doSecureInternal() - */ - protected function doSecureInternal( $fullCont, $dir, array $params ) { - $status = Status::newGood(); - - if ( $this->swiftAnonUser != '' ) { - // Restrict container from end-users... - try { - // doPrepareInternal() should have been called, - // so the Swift container should already exist... - $contObj = $this->getContainer( $fullCont ); // normally a cache hit - // NoSuchContainerException not thrown: container must exist - if ( !isset( $contObj->mw_wasSecured ) ) { - $status->merge( $this->setContainerAccess( - $contObj, - array( $this->auth->username ), // read - array( $this->auth->username ) // write - ) ); - // @TODO: when php-cloudfiles supports container - // metadata, we can make use of that to avoid RTTs - $contObj->mw_wasSecured = true; // avoid useless RTTs - } - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - } - } - - return $status; - } - - /** - * @see FileBackendStore::doCleanInternal() - */ - protected function doCleanInternal( $fullCont, $dir, array $params ) { - $status = Status::newGood(); - - // Only containers themselves can be removed, all else is virtual - if ( $dir != '' ) { - return $status; // nothing to do - } - - // (a) Check the container - try { - $contObj = $this->getContainer( $fullCont, true ); - } catch ( NoSuchContainerException $e ) { - return $status; // ok, nothing to do - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - - // (b) Delete the container if empty - if ( $contObj->object_count == 0 ) { - try { - $this->deleteContainer( $fullCont ); - } catch ( NoSuchContainerException $e ) { - return $status; // race? - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-internal', $this->name ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - } - - return $status; - } - - /** - * @see FileBackendStore::doFileExists() - */ - protected function doGetFileStat( array $params ) { - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); - if ( $srcRel === null ) { - return false; // invalid storage path - } - - $stat = false; - try { - $contObj = $this->getContainer( $srcCont ); - $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) ); - $this->addMissingMetadata( $srcObj, $params['src'] ); - $stat = array( - // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW - 'mtime' => wfTimestamp( TS_MW, $srcObj->last_modified ), - 'size' => $srcObj->content_length, - 'sha1' => $srcObj->metadata['Sha1base36'] - ); - } catch ( NoSuchContainerException $e ) { - } catch ( NoSuchObjectException $e ) { - } catch ( InvalidResponseException $e ) { - $stat = null; - } catch ( Exception $e ) { // some other exception? - $stat = null; - $this->logException( $e, __METHOD__, $params ); - } - - return $stat; - } - - /** - * Fill in any missing object metadata and save it to Swift - * - * @param $obj CF_Object - * @param $path string Storage path to object - * @return bool Success - * @throws Exception cloudfiles exceptions - */ - protected function addMissingMetadata( CF_Object $obj, $path ) { - if ( isset( $obj->metadata['Sha1base36'] ) ) { - return true; // nothing to do - } - $status = Status::newGood(); - $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status ); - if ( $status->isOK() ) { - $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) ); - if ( $tmpFile ) { - $hash = $tmpFile->getSha1Base36(); - if ( $hash !== false ) { - $obj->metadata['Sha1base36'] = $hash; - $obj->sync_metadata(); // save to Swift - return true; // success - } - } - } - $obj->metadata['Sha1base36'] = false; - return false; // failed - } - - /** - * @see FileBackend::getFileContents() - */ - public function getFileContents( array $params ) { - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); - if ( $srcRel === null ) { - return false; // invalid storage path - } - - if ( !$this->fileExists( $params ) ) { - return null; - } - - $data = false; - try { - $sContObj = $this->getContainer( $srcCont ); - $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD request - $data = $obj->read( $this->headersFromParams( $params ) ); - } catch ( NoSuchContainerException $e ) { - } catch ( InvalidResponseException $e ) { - } catch ( Exception $e ) { // some other exception? - $this->logException( $e, __METHOD__, $params ); - } - - return $data; - } - - /** - * @see FileBackendStore::getFileListInternal() - */ - public function getFileListInternal( $fullCont, $dir, array $params ) { - return new SwiftFileBackendFileList( $this, $fullCont, $dir ); - } - - /** - * Do not call this function outside of SwiftFileBackendFileList - * - * @param $fullCont string Resolved container name - * @param $dir string Resolved storage directory with no trailing slash - * @param $after string Storage path of file to list items after - * @param $limit integer Max number of items to list - * @return Array - */ - public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) { - $files = array(); - - try { - $container = $this->getContainer( $fullCont ); - $prefix = ( $dir == '' ) ? null : "{$dir}/"; - $files = $container->list_objects( $limit, $after, $prefix ); - } catch ( NoSuchContainerException $e ) { - } catch ( NoSuchObjectException $e ) { - } catch ( InvalidResponseException $e ) { - } catch ( Exception $e ) { // some other exception? - $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) ); - } - - return $files; - } - - /** - * @see FileBackendStore::doGetFileSha1base36() - */ - public function doGetFileSha1base36( array $params ) { - $stat = $this->getFileStat( $params ); - if ( $stat ) { - return $stat['sha1']; - } else { - return false; - } - } - - /** - * @see FileBackendStore::doStreamFile() - */ - protected function doStreamFile( array $params ) { - $status = Status::newGood(); - - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); - if ( $srcRel === null ) { - $status->fatal( 'backend-fail-invalidpath', $params['src'] ); - } - - try { - $cont = $this->getContainer( $srcCont ); - } catch ( NoSuchContainerException $e ) { - $status->fatal( 'backend-fail-stream', $params['src'] ); - return $status; - } catch ( InvalidResponseException $e ) { - $status->fatal( 'backend-fail-connect', $this->name ); - return $status; - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-stream', $params['src'] ); - $this->logException( $e, __METHOD__, $params ); - return $status; - } - - try { - $output = fopen( 'php://output', 'wb' ); - $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD request - $obj->stream( $output, $this->headersFromParams( $params ) ); - } catch ( InvalidResponseException $e ) { // 404? connection problem? - $status->fatal( 'backend-fail-stream', $params['src'] ); - } catch ( Exception $e ) { // some other exception? - $status->fatal( 'backend-fail-stream', $params['src'] ); - $this->logException( $e, __METHOD__, $params ); - } - - return $status; - } - - /** - * @see FileBackendStore::getLocalCopy() - */ - public function getLocalCopy( array $params ) { - list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); - if ( $srcRel === null ) { - return null; - } - - if ( !$this->fileExists( $params ) ) { - return null; - } - - $tmpFile = null; - try { - $sContObj = $this->getContainer( $srcCont ); - $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD - // Get source file extension - $ext = FileBackend::extensionFromPath( $srcRel ); - // Create a new temporary file... - $tmpFile = TempFSFile::factory( wfBaseName( $srcRel ) . '_', $ext ); - if ( $tmpFile ) { - $handle = fopen( $tmpFile->getPath(), 'wb' ); - if ( $handle ) { - $obj->stream( $handle, $this->headersFromParams( $params ) ); - fclose( $handle ); - } else { - $tmpFile = null; // couldn't open temp file - } - } - } catch ( NoSuchContainerException $e ) { - $tmpFile = null; - } catch ( InvalidResponseException $e ) { - $tmpFile = null; - } catch ( Exception $e ) { // some other exception? - $tmpFile = null; - $this->logException( $e, __METHOD__, $params ); - } - - return $tmpFile; - } - - /** - * Get headers to send to Swift when reading a file based - * on a FileBackend params array, e.g. that of getLocalCopy(). - * $params is currently only checked for a 'latest' flag. - * - * @param $params Array - * @return Array - */ - protected function headersFromParams( array $params ) { - $hdrs = array(); - if ( !empty( $params['latest'] ) ) { - $hdrs[] = 'X-Newest: true'; - } - return $hdrs; - } - - /** - * Set read/write permissions for a Swift container - * - * @param $contObj CF_Container Swift container - * @param $readGrps Array Swift users who can read (account:user) - * @param $writeGrps Array Swift users who can write (account:user) - * @return Status - */ - protected function setContainerAccess( - CF_Container $contObj, array $readGrps, array $writeGrps - ) { - $creds = $contObj->cfs_auth->export_credentials(); - - $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name ); - - // Note: 10 second timeout consistent with php-cloudfiles - $req = new CurlHttpRequest( $url, array( 'method' => 'POST', 'timeout' => 10 ) ); - $req->setHeader( 'X-Auth-Token', $creds['auth_token'] ); - $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) ); - $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) ); - - return $req->execute(); // should return 204 - } - - /** - * Get a connection to the Swift proxy - * - * @return CF_Connection|false - * @throws InvalidResponseException - */ - protected function getConnection() { - if ( $this->conn === false ) { - throw new InvalidResponseException; // failed last attempt - } - // Session keys expire after a while, so we renew them periodically - if ( $this->conn && ( time() - $this->connStarted ) > $this->authTTL ) { - $this->conn->close(); // close active cURL connections - $this->conn = null; - } - // Authenticate with proxy and get a session key... - if ( $this->conn === null ) { - $this->connContainers = array(); - try { - $this->auth->authenticate(); - $this->conn = new CF_Connection( $this->auth ); - $this->connStarted = time(); - } catch ( AuthenticationException $e ) { - $this->conn = false; // don't keep re-trying - } catch ( InvalidResponseException $e ) { - $this->conn = false; // don't keep re-trying - } - } - if ( !$this->conn ) { - throw new InvalidResponseException; // auth/connection problem - } - return $this->conn; - } - - /** - * @see FileBackendStore::doClearCache() - */ - protected function doClearCache( array $paths = null ) { - $this->connContainers = array(); // clear container object cache - } - - /** - * Get a Swift container object, possibly from process cache. - * Use $reCache if the file count or byte count is needed. - * - * @param $container string Container name - * @param $reCache bool Refresh the process cache - * @return CF_Container - */ - protected function getContainer( $container, $reCache = false ) { - $conn = $this->getConnection(); // Swift proxy connection - if ( $reCache ) { - unset( $this->connContainers[$container] ); // purge cache - } - if ( !isset( $this->connContainers[$container] ) ) { - $contObj = $conn->get_container( $container ); - // NoSuchContainerException not thrown: container must exist - if ( count( $this->connContainers ) >= $this->maxContCacheSize ) { // trim cache? - reset( $this->connContainers ); - $key = key( $this->connContainers ); - unset( $this->connContainers[$key] ); - } - $this->connContainers[$container] = $contObj; // cache it - } - return $this->connContainers[$container]; - } - - /** - * Create a Swift container - * - * @param $container string Container name - * @return CF_Container - */ - protected function createContainer( $container ) { - $conn = $this->getConnection(); // Swift proxy connection - $contObj = $conn->create_container( $container ); - $this->connContainers[$container] = $contObj; // cache it - return $contObj; - } - - /** - * Delete a Swift container - * - * @param $container string Container name - * @return void - */ - protected function deleteContainer( $container ) { - $conn = $this->getConnection(); // Swift proxy connection - $conn->delete_container( $container ); - unset( $this->connContainers[$container] ); // purge cache - } - - /** - * Log an unexpected exception for this backend - * - * @param $e Exception - * @param $func string - * @param $params Array - * @return void - */ - protected function logException( Exception $e, $func, array $params ) { - wfDebugLog( 'SwiftBackend', - get_class( $e ) . " in '{$func}' (given '" . serialize( $params ) . "')" . - ( $e instanceof InvalidResponseException - ? ": {$e->getMessage()}" - : "" - ) - ); - } -} - -/** - * SwiftFileBackend helper class to page through object listings. - * Swift also has a listing limit of 10,000 objects for sanity. - * Do not use this class from places outside SwiftFileBackend. - * - * @ingroup FileBackend - */ -class SwiftFileBackendFileList implements Iterator { - /** @var Array */ - protected $bufferIter = array(); - protected $bufferAfter = null; // string; list items *after* this path - protected $pos = 0; // integer - - /** @var SwiftFileBackend */ - protected $backend; - protected $container; // - protected $dir; // string storage directory - protected $suffixStart; // integer - - const PAGE_SIZE = 5000; // file listing buffer size - - /** - * @param $backend SwiftFileBackend - * @param $fullCont string Resolved container name - * @param $dir string Resolved directory relative to container - */ - public function __construct( SwiftFileBackend $backend, $fullCont, $dir ) { - $this->backend = $backend; - $this->container = $fullCont; - $this->dir = $dir; - if ( substr( $this->dir, -1 ) === '/' ) { - $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash - } - if ( $this->dir == '' ) { // whole container - $this->suffixStart = 0; - } else { // dir within container - $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/" - } - } - - public function current() { - return substr( current( $this->bufferIter ), $this->suffixStart ); - } - - public function key() { - return $this->pos; - } - - public function next() { - // Advance to the next file in the page - next( $this->bufferIter ); - ++$this->pos; - // Check if there are no files left in this page and - // advance to the next page if this page was not empty. - if ( !$this->valid() && count( $this->bufferIter ) ) { - $this->bufferAfter = end( $this->bufferIter ); - $this->bufferIter = $this->backend->getFileListPageInternal( - $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE - ); - } - } - - public function rewind() { - $this->pos = 0; - $this->bufferAfter = null; - $this->bufferIter = $this->backend->getFileListPageInternal( - $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE - ); - } - - public function valid() { - return ( current( $this->bufferIter ) !== false ); // no paths can have this value - } -} diff --git a/includes/filerepo/backend/lockmanager/DBLockManager.php b/includes/filerepo/backend/lockmanager/DBLockManager.php deleted file mode 100644 index 045056ea..00000000 --- a/includes/filerepo/backend/lockmanager/DBLockManager.php +++ /dev/null @@ -1,469 +0,0 @@ -<?php - -/** - * Version of LockManager based on using DB table locks. - * This is meant for multi-wiki systems that may share files. - * All locks are blocking, so it might be useful to set a small - * lock-wait timeout via server config to curtail deadlocks. - * - * All lock requests for a resource, identified by a hash string, will map - * to one bucket. Each bucket maps to one or several peer DBs, each on their - * own server, all having the filelocks.sql tables (with row-level locking). - * A majority of peer DBs must agree for a lock to be acquired. - * - * Caching is used to avoid hitting servers that are down. - * - * @ingroup LockManager - * @since 1.19 - */ -class DBLockManager extends LockManager { - /** @var Array Map of DB names to server config */ - protected $dbServers; // (DB name => server config array) - /** @var Array Map of bucket indexes to peer DB lists */ - protected $dbsByBucket; // (bucket index => (ldb1, ldb2, ...)) - /** @var BagOStuff */ - protected $statusCache; - - protected $lockExpiry; // integer number of seconds - protected $safeDelay; // integer number of seconds - - protected $session = 0; // random integer - /** @var Array Map Database connections (DB name => Database) */ - protected $conns = array(); - - /** - * Construct a new instance from configuration. - * - * $config paramaters include: - * 'dbServers' : Associative array of DB names to server configuration. - * Configuration is an associative array that includes: - * 'host' - DB server name - * 'dbname' - DB name - * 'type' - DB type (mysql,postgres,...) - * 'user' - DB user - * 'password' - DB user password - * 'tablePrefix' - DB table prefix - * 'flags' - DB flags (see DatabaseBase) - * 'dbsByBucket' : Array of 1-16 consecutive integer keys, starting from 0, - * each having an odd-numbered list of DB names (peers) as values. - * Any DB named 'localDBMaster' will automatically use the DB master - * settings for this wiki (without the need for a dbServers entry). - * 'lockExpiry' : Lock timeout (seconds) for dropped connections. [optional] - * This tells the DB server how long to wait before assuming - * connection failure and releasing all the locks for a session. - * - * @param Array $config - */ - public function __construct( array $config ) { - $this->dbServers = isset( $config['dbServers'] ) - ? $config['dbServers'] - : array(); // likely just using 'localDBMaster' - // Sanitize dbsByBucket config to prevent PHP errors - $this->dbsByBucket = array_filter( $config['dbsByBucket'], 'is_array' ); - $this->dbsByBucket = array_values( $this->dbsByBucket ); // consecutive - - if ( isset( $config['lockExpiry'] ) ) { - $this->lockExpiry = $config['lockExpiry']; - } else { - $met = ini_get( 'max_execution_time' ); - $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0 - } - $this->safeDelay = ( $this->lockExpiry <= 0 ) - ? 60 // pick a safe-ish number to match DB timeout default - : $this->lockExpiry; // cover worst case - - foreach ( $this->dbsByBucket as $bucket ) { - if ( count( $bucket ) > 1 ) { - // Tracks peers that couldn't be queried recently to avoid lengthy - // connection timeouts. This is useless if each bucket has one peer. - $this->statusCache = wfGetMainCache(); - break; - } - } - - $this->session = ''; - for ( $i = 0; $i < 5; $i++ ) { - $this->session .= mt_rand( 0, 2147483647 ); - } - $this->session = wfBaseConvert( sha1( $this->session ), 16, 36, 31 ); - } - - /** - * @see LockManager::doLock() - */ - protected function doLock( array $paths, $type ) { - $status = Status::newGood(); - - $pathsToLock = array(); - // Get locks that need to be acquired (buckets => locks)... - foreach ( $paths as $path ) { - if ( isset( $this->locksHeld[$path][$type] ) ) { - ++$this->locksHeld[$path][$type]; - } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { - $this->locksHeld[$path][$type] = 1; - } else { - $bucket = $this->getBucketFromKey( $path ); - $pathsToLock[$bucket][] = $path; - } - } - - $lockedPaths = array(); // files locked in this attempt - // Attempt to acquire these locks... - foreach ( $pathsToLock as $bucket => $paths ) { - // Try to acquire the locks for this bucket - $res = $this->doLockingQueryAll( $bucket, $paths, $type ); - if ( $res === 'cantacquire' ) { - // Resources already locked by another process. - // Abort and unlock everything we just locked. - foreach ( $paths as $path ) { - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - return $status; - } elseif ( $res !== true ) { - // Couldn't contact any DBs for this bucket. - // Abort and unlock everything we just locked. - $status->fatal( 'lockmanager-fail-db-bucket', $bucket ); - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - return $status; - } - // Record these locks as active - foreach ( $paths as $path ) { - $this->locksHeld[$path][$type] = 1; // locked - } - // Keep track of what locks were made in this attempt - $lockedPaths = array_merge( $lockedPaths, $paths ); - } - - return $status; - } - - /** - * @see LockManager::doUnlock() - */ - protected function doUnlock( array $paths, $type ) { - $status = Status::newGood(); - - foreach ( $paths as $path ) { - if ( !isset( $this->locksHeld[$path] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } elseif ( !isset( $this->locksHeld[$path][$type] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } else { - --$this->locksHeld[$path][$type]; - if ( $this->locksHeld[$path][$type] <= 0 ) { - unset( $this->locksHeld[$path][$type] ); - } - if ( !count( $this->locksHeld[$path] ) ) { - unset( $this->locksHeld[$path] ); // no SH or EX locks left for key - } - } - } - - // Reference count the locks held and COMMIT when zero - if ( !count( $this->locksHeld ) ) { - $status->merge( $this->finishLockTransactions() ); - } - - return $status; - } - - /** - * Get a connection to a lock DB and acquire locks on $paths. - * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118. - * - * @param $lockDb string - * @param $paths Array - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH - * @return bool Resources able to be locked - * @throws DBError - */ - protected function doLockingQuery( $lockDb, array $paths, $type ) { - if ( $type == self::LOCK_EX ) { // writer locks - $db = $this->getConnection( $lockDb ); - if ( !$db ) { - return false; // bad config - } - $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); - # Build up values for INSERT clause - $data = array(); - foreach ( $keys as $key ) { - $data[] = array( 'fle_key' => $key ); - } - # Wait on any existing writers and block new ones if we get in - $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); - } - return true; - } - - /** - * Attempt to acquire locks with the peers for a bucket. - * This should avoid throwing any exceptions. - * - * @param $bucket integer - * @param $paths Array List of resource keys to lock - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH - * @return bool|string One of (true, 'cantacquire', 'dberrors') - */ - protected function doLockingQueryAll( $bucket, array $paths, $type ) { - $yesVotes = 0; // locks made on trustable DBs - $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining DBs - $quorum = floor( $votesLeft/2 + 1 ); // simple majority - // Get votes for each DB, in order, until we have enough... - foreach ( $this->dbsByBucket[$bucket] as $lockDb ) { - // Check that DB is not *known* to be down - if ( $this->cacheCheckFailures( $lockDb ) ) { - try { - // Attempt to acquire the lock on this DB - if ( !$this->doLockingQuery( $lockDb, $paths, $type ) ) { - return 'cantacquire'; // vetoed; resource locked - } - ++$yesVotes; // success for this peer - if ( $yesVotes >= $quorum ) { - return true; // lock obtained - } - } catch ( DBConnectionError $e ) { - $this->cacheRecordFailure( $lockDb ); - } catch ( DBError $e ) { - if ( $this->lastErrorIndicatesLocked( $lockDb ) ) { - return 'cantacquire'; // vetoed; resource locked - } - } - } - --$votesLeft; - $votesNeeded = $quorum - $yesVotes; - if ( $votesNeeded > $votesLeft ) { - // In "trust cache" mode we don't have to meet the quorum - break; // short-circuit - } - } - // At this point, we must not have meet the quorum - return 'dberrors'; // not enough votes to ensure correctness - } - - /** - * Get (or reuse) a connection to a lock DB - * - * @param $lockDb string - * @return Database - * @throws DBError - */ - protected function getConnection( $lockDb ) { - if ( !isset( $this->conns[$lockDb] ) ) { - $db = null; - if ( $lockDb === 'localDBMaster' ) { - $lb = wfGetLBFactory()->newMainLB(); - $db = $lb->getConnection( DB_MASTER ); - } elseif ( isset( $this->dbServers[$lockDb] ) ) { - $config = $this->dbServers[$lockDb]; - $db = DatabaseBase::factory( $config['type'], $config ); - } - if ( !$db ) { - return null; // config error? - } - $this->conns[$lockDb] = $db; - $this->conns[$lockDb]->clearFlag( DBO_TRX ); - # If the connection drops, try to avoid letting the DB rollback - # and release the locks before the file operations are finished. - # This won't handle the case of DB server restarts however. - $options = array(); - if ( $this->lockExpiry > 0 ) { - $options['connTimeout'] = $this->lockExpiry; - } - $this->conns[$lockDb]->setSessionOptions( $options ); - $this->initConnection( $lockDb, $this->conns[$lockDb] ); - } - if ( !$this->conns[$lockDb]->trxLevel() ) { - $this->conns[$lockDb]->begin(); // start transaction - } - return $this->conns[$lockDb]; - } - - /** - * Do additional initialization for new lock DB connection - * - * @param $lockDb string - * @param $db DatabaseBase - * @return void - * @throws DBError - */ - protected function initConnection( $lockDb, DatabaseBase $db ) {} - - /** - * Commit all changes to lock-active databases. - * This should avoid throwing any exceptions. - * - * @return Status - */ - protected function finishLockTransactions() { - $status = Status::newGood(); - foreach ( $this->conns as $lockDb => $db ) { - if ( $db->trxLevel() ) { // in transaction - try { - $db->rollback(); // finish transaction and kill any rows - } catch ( DBError $e ) { - $status->fatal( 'lockmanager-fail-db-release', $lockDb ); - } - } - } - return $status; - } - - /** - * Check if the last DB error for $lockDb indicates - * that a requested resource was locked by another process. - * This should avoid throwing any exceptions. - * - * @param $lockDb string - * @return bool - */ - protected function lastErrorIndicatesLocked( $lockDb ) { - if ( isset( $this->conns[$lockDb] ) ) { // sanity - $db = $this->conns[$lockDb]; - return ( $db->wasDeadlock() || $db->wasLockTimeout() ); - } - return false; - } - - /** - * Checks if the DB has not recently had connection/query errors. - * This just avoids wasting time on doomed connection attempts. - * - * @param $lockDb string - * @return bool - */ - protected function cacheCheckFailures( $lockDb ) { - if ( $this->statusCache && $this->safeDelay > 0 ) { - $path = $this->getMissKey( $lockDb ); - $misses = $this->statusCache->get( $path ); - return !$misses; - } - return true; - } - - /** - * Log a lock request failure to the cache - * - * @param $lockDb string - * @return bool Success - */ - protected function cacheRecordFailure( $lockDb ) { - if ( $this->statusCache && $this->safeDelay > 0 ) { - $path = $this->getMissKey( $lockDb ); - $misses = $this->statusCache->get( $path ); - if ( $misses ) { - return $this->statusCache->incr( $path ); - } else { - return $this->statusCache->add( $path, 1, $this->safeDelay ); - } - } - return true; - } - - /** - * Get a cache key for recent query misses for a DB - * - * @param $lockDb string - * @return string - */ - protected function getMissKey( $lockDb ) { - return 'lockmanager:querymisses:' . str_replace( ' ', '_', $lockDb ); - } - - /** - * Get the bucket for resource path. - * This should avoid throwing any exceptions. - * - * @param $path string - * @return integer - */ - protected function getBucketFromKey( $path ) { - $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) - return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->dbsByBucket ); - } - - /** - * Make sure remaining locks get cleared for sanity - */ - function __destruct() { - foreach ( $this->conns as $lockDb => $db ) { - if ( $db->trxLevel() ) { // in transaction - try { - $db->rollback(); // finish transaction and kill any rows - } catch ( DBError $e ) { - // oh well - } - } - $db->close(); - } - } -} - -/** - * MySQL version of DBLockManager that supports shared locks. - * All locks are non-blocking, which avoids deadlocks. - * - * @ingroup LockManager - */ -class MySqlLockManager extends DBLockManager { - /** @var Array Mapping of lock types to the type actually used */ - protected $lockTypeMap = array( - self::LOCK_SH => self::LOCK_SH, - self::LOCK_UW => self::LOCK_SH, - self::LOCK_EX => self::LOCK_EX - ); - - protected function initConnection( $lockDb, DatabaseBase $db ) { - # Let this transaction see lock rows from other transactions - $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); - } - - protected function doLockingQuery( $lockDb, array $paths, $type ) { - $db = $this->getConnection( $lockDb ); - if ( !$db ) { - return false; - } - $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); - # Build up values for INSERT clause - $data = array(); - foreach ( $keys as $key ) { - $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session ); - } - # Block new writers... - $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) ); - # Actually do the locking queries... - if ( $type == self::LOCK_SH ) { // reader locks - # Bail if there are any existing writers... - $blocked = $db->selectField( 'filelocks_exclusive', '1', - array( 'fle_key' => $keys ), - __METHOD__ - ); - # Prospective writers that haven't yet updated filelocks_exclusive - # will recheck filelocks_shared after doing so and bail due to our entry. - } else { // writer locks - $encSession = $db->addQuotes( $this->session ); - # Bail if there are any existing writers... - # The may detect readers, but the safe check for them is below. - # Note: if two writers come at the same time, both bail :) - $blocked = $db->selectField( 'filelocks_shared', '1', - array( 'fls_key' => $keys, "fls_session != $encSession" ), - __METHOD__ - ); - if ( !$blocked ) { - # Build up values for INSERT clause - $data = array(); - foreach ( $keys as $key ) { - $data[] = array( 'fle_key' => $key ); - } - # Block new readers/writers... - $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); - # Bail if there are any existing readers... - $blocked = $db->selectField( 'filelocks_shared', '1', - array( 'fls_key' => $keys, "fls_session != $encSession" ), - __METHOD__ - ); - } - } - return !$blocked; - } -} diff --git a/includes/filerepo/backend/lockmanager/LSLockManager.php b/includes/filerepo/backend/lockmanager/LSLockManager.php deleted file mode 100644 index b7ac743c..00000000 --- a/includes/filerepo/backend/lockmanager/LSLockManager.php +++ /dev/null @@ -1,295 +0,0 @@ -<?php - -/** - * Manage locks using a lock daemon server. - * - * Version of LockManager based on using lock daemon servers. - * This is meant for multi-wiki systems that may share files. - * All locks are non-blocking, which avoids deadlocks. - * - * All lock requests for a resource, identified by a hash string, will map - * to one bucket. Each bucket maps to one or several peer servers, each - * running LockServerDaemon.php, listening on a designated TCP port. - * A majority of peers must agree for a lock to be acquired. - * - * @ingroup LockManager - * @since 1.19 - */ -class LSLockManager extends LockManager { - /** @var Array Mapping of lock types to the type actually used */ - protected $lockTypeMap = array( - self::LOCK_SH => self::LOCK_SH, - self::LOCK_UW => self::LOCK_SH, - self::LOCK_EX => self::LOCK_EX - ); - - /** @var Array Map of server names to server config */ - protected $lockServers; // (server name => server config array) - /** @var Array Map of bucket indexes to peer server lists */ - protected $srvsByBucket; // (bucket index => (lsrv1, lsrv2, ...)) - - /** @var Array Map Server connections (server name => resource) */ - protected $conns = array(); - - protected $connTimeout; // float number of seconds - protected $session = ''; // random SHA-1 string - - /** - * Construct a new instance from configuration. - * - * $config paramaters include: - * 'lockServers' : Associative array of server names to configuration. - * Configuration is an associative array that includes: - * 'host' - IP address/hostname - * 'port' - TCP port - * 'authKey' - Secret string the lock server uses - * 'srvsByBucket' : Array of 1-16 consecutive integer keys, starting from 0, - * each having an odd-numbered list of server names (peers) as values. - * 'connTimeout' : Lock server connection attempt timeout. [optional] - * - * @param Array $config - */ - public function __construct( array $config ) { - $this->lockServers = $config['lockServers']; - // Sanitize srvsByBucket config to prevent PHP errors - $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' ); - $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive - - if ( isset( $config['connTimeout'] ) ) { - $this->connTimeout = $config['connTimeout']; - } else { - $this->connTimeout = 3; // use some sane amount - } - - $this->session = ''; - for ( $i = 0; $i < 5; $i++ ) { - $this->session .= mt_rand( 0, 2147483647 ); - } - $this->session = wfBaseConvert( sha1( $this->session ), 16, 36, 31 ); - } - - protected function doLock( array $paths, $type ) { - $status = Status::newGood(); - - $pathsToLock = array(); - // Get locks that need to be acquired (buckets => locks)... - foreach ( $paths as $path ) { - if ( isset( $this->locksHeld[$path][$type] ) ) { - ++$this->locksHeld[$path][$type]; - } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { - $this->locksHeld[$path][$type] = 1; - } else { - $bucket = $this->getBucketFromKey( $path ); - $pathsToLock[$bucket][] = $path; - } - } - - $lockedPaths = array(); // files locked in this attempt - // Attempt to acquire these locks... - foreach ( $pathsToLock as $bucket => $paths ) { - // Try to acquire the locks for this bucket - $res = $this->doLockingRequestAll( $bucket, $paths, $type ); - if ( $res === 'cantacquire' ) { - // Resources already locked by another process. - // Abort and unlock everything we just locked. - foreach ( $paths as $path ) { - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - return $status; - } elseif ( $res !== true ) { - // Couldn't contact any servers for this bucket. - // Abort and unlock everything we just locked. - foreach ( $paths as $path ) { - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - return $status; - } - // Record these locks as active - foreach ( $paths as $path ) { - $this->locksHeld[$path][$type] = 1; // locked - } - // Keep track of what locks were made in this attempt - $lockedPaths = array_merge( $lockedPaths, $paths ); - } - - return $status; - } - - protected function doUnlock( array $paths, $type ) { - $status = Status::newGood(); - - foreach ( $paths as $path ) { - if ( !isset( $this->locksHeld[$path] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } elseif ( !isset( $this->locksHeld[$path][$type] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } else { - --$this->locksHeld[$path][$type]; - if ( $this->locksHeld[$path][$type] <= 0 ) { - unset( $this->locksHeld[$path][$type] ); - } - if ( !count( $this->locksHeld[$path] ) ) { - unset( $this->locksHeld[$path] ); // no SH or EX locks left for key - } - } - } - - // Reference count the locks held and release locks when zero - if ( !count( $this->locksHeld ) ) { - $status->merge( $this->releaseLocks() ); - } - - return $status; - } - - /** - * Get a connection to a lock server and acquire locks on $paths - * - * @param $lockSrv string - * @param $paths Array - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH - * @return bool Resources able to be locked - */ - protected function doLockingRequest( $lockSrv, array $paths, $type ) { - if ( $type == self::LOCK_SH ) { // reader locks - $type = 'SH'; - } elseif ( $type == self::LOCK_EX ) { // writer locks - $type = 'EX'; - } else { - return true; // ok... - } - - // Send out the command and get the response... - $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); - $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys ); - - return ( $response === 'ACQUIRED' ); - } - - /** - * Send a command and get back the response - * - * @param $lockSrv string - * @param $action string - * @param $type string - * @param $values Array - * @return string|false - */ - protected function sendCommand( $lockSrv, $action, $type, $values ) { - $conn = $this->getConnection( $lockSrv ); - if ( !$conn ) { - return false; // no connection - } - $authKey = $this->lockServers[$lockSrv]['authKey']; - // Build of the command as a flat string... - $values = implode( '|', $values ); - $key = sha1( $this->session . $action . $type . $values . $authKey ); - // Send out the command... - if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) { - return false; - } - // Get the response... - $response = fgets( $conn ); - if ( $response === false ) { - return false; - } - return trim( $response ); - } - - /** - * Attempt to acquire locks with the peers for a bucket - * - * @param $bucket integer - * @param $paths Array List of resource keys to lock - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH - * @return bool|string One of (true, 'cantacquire', 'srverrors') - */ - protected function doLockingRequestAll( $bucket, array $paths, $type ) { - $yesVotes = 0; // locks made on trustable servers - $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers - $quorum = floor( $votesLeft/2 + 1 ); // simple majority - // Get votes for each peer, in order, until we have enough... - foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { - // Attempt to acquire the lock on this peer - if ( !$this->doLockingRequest( $lockSrv, $paths, $type ) ) { - return 'cantacquire'; // vetoed; resource locked - } - ++$yesVotes; // success for this peer - if ( $yesVotes >= $quorum ) { - return true; // lock obtained - } - --$votesLeft; - $votesNeeded = $quorum - $yesVotes; - if ( $votesNeeded > $votesLeft ) { - // In "trust cache" mode we don't have to meet the quorum - break; // short-circuit - } - } - // At this point, we must not have meet the quorum - return 'srverrors'; // not enough votes to ensure correctness - } - - /** - * Get (or reuse) a connection to a lock server - * - * @param $lockSrv string - * @return resource - */ - protected function getConnection( $lockSrv ) { - if ( !isset( $this->conns[$lockSrv] ) ) { - $cfg = $this->lockServers[$lockSrv]; - wfSuppressWarnings(); - $errno = $errstr = ''; - $conn = fsockopen( $cfg['host'], $cfg['port'], $errno, $errstr, $this->connTimeout ); - wfRestoreWarnings(); - if ( $conn === false ) { - return null; - } - $sec = floor( $this->connTimeout ); - $usec = floor( ( $this->connTimeout - floor( $this->connTimeout ) ) * 1e6 ); - stream_set_timeout( $conn, $sec, $usec ); - $this->conns[$lockSrv] = $conn; - } - return $this->conns[$lockSrv]; - } - - /** - * Release all locks that this session is holding - * - * @return Status - */ - protected function releaseLocks() { - $status = Status::newGood(); - foreach ( $this->conns as $lockSrv => $conn ) { - $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() ); - if ( $response !== 'RELEASED_ALL' ) { - $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); - } - } - return $status; - } - - /** - * Get the bucket for resource path. - * This should avoid throwing any exceptions. - * - * @param $path string - * @return integer - */ - protected function getBucketFromKey( $path ) { - $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) - return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->srvsByBucket ); - } - - /** - * Make sure remaining locks get cleared for sanity - */ - function __destruct() { - $this->releaseLocks(); - foreach ( $this->conns as $conn ) { - fclose( $conn ); - } - } -} diff --git a/includes/filerepo/backend/lockmanager/LockManager.php b/includes/filerepo/backend/lockmanager/LockManager.php deleted file mode 100644 index 23603a4f..00000000 --- a/includes/filerepo/backend/lockmanager/LockManager.php +++ /dev/null @@ -1,182 +0,0 @@ -<?php -/** - * @defgroup LockManager Lock management - * @ingroup FileBackend - */ - -/** - * @file - * @ingroup LockManager - * @author Aaron Schulz - */ - -/** - * Class for handling resource locking. - * - * Locks on resource keys can either be shared or exclusive. - * - * Implementations must keep track of what is locked by this proccess - * in-memory and support nested locking calls (using reference counting). - * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op. - * Locks should either be non-blocking or have low wait timeouts. - * - * Subclasses should avoid throwing exceptions at all costs. - * - * @ingroup LockManager - * @since 1.19 - */ -abstract class LockManager { - /** @var Array Mapping of lock types to the type actually used */ - protected $lockTypeMap = array( - self::LOCK_SH => self::LOCK_SH, - self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH - self::LOCK_EX => self::LOCK_EX - ); - - /** @var Array Map of (resource path => lock type => count) */ - protected $locksHeld = array(); - - /* Lock types; stronger locks have higher values */ - const LOCK_SH = 1; // shared lock (for reads) - const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) - const LOCK_EX = 3; // exclusive lock (for writes) - - /** - * Construct a new instance from configuration - * - * @param $config Array - */ - public function __construct( array $config ) {} - - /** - * Lock the resources at the given abstract paths - * - * @param $paths Array List of resource names - * @param $type integer LockManager::LOCK_* constant - * @return Status - */ - final public function lock( array $paths, $type = self::LOCK_EX ) { - return $this->doLock( array_unique( $paths ), $this->lockTypeMap[$type] ); - } - - /** - * Unlock the resources at the given abstract paths - * - * @param $paths Array List of storage paths - * @param $type integer LockManager::LOCK_* constant - * @return Status - */ - final public function unlock( array $paths, $type = self::LOCK_EX ) { - return $this->doUnlock( array_unique( $paths ), $this->lockTypeMap[$type] ); - } - - /** - * Get the base 36 SHA-1 of a string, padded to 31 digits - * - * @param $path string - * @return string - */ - final protected static function sha1Base36( $path ) { - return wfBaseConvert( sha1( $path ), 16, 36, 31 ); - } - - /** - * Lock resources with the given keys and lock type - * - * @param $paths Array List of storage paths - * @param $type integer LockManager::LOCK_* constant - * @return string - */ - abstract protected function doLock( array $paths, $type ); - - /** - * Unlock resources with the given keys and lock type - * - * @param $paths Array List of storage paths - * @param $type integer LockManager::LOCK_* constant - * @return string - */ - abstract protected function doUnlock( array $paths, $type ); -} - -/** - * Self releasing locks - * - * LockManager helper class to handle scoped locks, which - * release when an object is destroyed or goes out of scope. - * - * @ingroup LockManager - * @since 1.19 - */ -class ScopedLock { - /** @var LockManager */ - protected $manager; - /** @var Status */ - protected $status; - /** @var Array List of resource paths*/ - protected $paths; - - protected $type; // integer lock type - - /** - * @param $manager LockManager - * @param $paths Array List of storage paths - * @param $type integer LockManager::LOCK_* constant - * @param $status Status - */ - protected function __construct( - LockManager $manager, array $paths, $type, Status $status - ) { - $this->manager = $manager; - $this->paths = $paths; - $this->status = $status; - $this->type = $type; - } - - protected function __clone() {} - - /** - * Get a ScopedLock object representing a lock on resource paths. - * Any locks are released once this object goes out of scope. - * The status object is updated with any errors or warnings. - * - * @param $manager LockManager - * @param $paths Array List of storage paths - * @param $type integer LockManager::LOCK_* constant - * @param $status Status - * @return ScopedLock|null Returns null on failure - */ - public static function factory( - LockManager $manager, array $paths, $type, Status $status - ) { - $lockStatus = $manager->lock( $paths, $type ); - $status->merge( $lockStatus ); - if ( $lockStatus->isOK() ) { - return new self( $manager, $paths, $type, $status ); - } - return null; - } - - function __destruct() { - $wasOk = $this->status->isOK(); - $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) ); - if ( $wasOk ) { - // Make sure status is OK, despite any unlockFiles() fatals - $this->status->setResult( true, $this->status->value ); - } - } -} - -/** - * Simple version of LockManager that does nothing - * @since 1.19 - */ -class NullLockManager extends LockManager { - protected function doLock( array $paths, $type ) { - return Status::newGood(); - } - - protected function doUnlock( array $paths, $type ) { - return Status::newGood(); - } -} diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index 3b9bd7f0..c5a0bd1b 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -1,6 +1,21 @@ <?php /** - * Deleted file in the 'filearchive' table + * Deleted file in the 'filearchive' table. + * + * 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 FileAbstraction @@ -93,7 +108,7 @@ class ArchivedFile { /** * Loads a file object from the filearchive table - * @return true on success or null + * @return bool|null True on success or null */ public function load() { if ( $this->dataLoaded ) { @@ -143,7 +158,7 @@ class ArchivedFile { array( 'ORDER BY' => 'fa_timestamp DESC' ) ); if ( $res == false || $dbr->numRows( $res ) == 0 ) { // this revision does not exist? - return; + return null; } $ret = $dbr->resultObject( $res ); $row = $ret->fetchObject(); diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index f74fb678..557609d4 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -9,6 +9,21 @@ /** * Base code for files. * + * 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 FileAbstraction */ @@ -48,6 +63,14 @@ abstract class File { const DELETE_SOURCE = 1; + // Audience options for File::getDescription() + const FOR_PUBLIC = 1; + const FOR_THIS_USER = 2; + const RAW = 3; + + // Options for File::thumbName() + const THUMB_FULL_NAME = 1; + /** * Some member variables can be lazy-initialised using __get(). The * initialisation function for these variables is always a function named @@ -68,19 +91,19 @@ abstract class File { */ /** - * @var FileRepo|false + * @var FileRepo|bool */ var $repo; /** - * @var Title|false + * @var Title */ var $title; var $lastError, $redirected, $redirectedTitle; /** - * @var FSFile|false + * @var FSFile|bool False if undefined */ protected $fsFile; @@ -94,6 +117,8 @@ abstract class File { */ protected $url, $extension, $name, $path, $hashPath, $pageCount, $transformScript; + protected $redirectTitle; + /** * @var bool */ @@ -111,8 +136,8 @@ abstract class File { * may return false or throw exceptions if they are not set. * Most subclasses will want to call assertRepoDefined() here. * - * @param $title Title|string|false - * @param $repo FileRepo|false + * @param $title Title|string|bool + * @param $repo FileRepo|bool */ function __construct( $title, $repo ) { if ( $title !== false ) { // subclasses may not use MW titles @@ -127,7 +152,8 @@ abstract class File { * valid Title object with namespace NS_FILE or null * * @param $title Title|string - * @param $exception string|false Use 'exception' to throw an error on bad titles + * @param $exception string|bool Use 'exception' to throw an error on bad titles + * @throws MWException * @return Title|null */ static function normalizeTitle( $title, $exception = false ) { @@ -223,6 +249,18 @@ abstract class File { } /** + * Callback for usort() to do file sorts by name + * + * @param $a File + * @param $b File + * + * @return Integer: result of name comparison + */ + public static function compare( File $a, File $b ) { + return strcmp( $a->getName(), $b->getName() ); + } + + /** * Return the name of this file * * @return string @@ -252,7 +290,7 @@ abstract class File { /** * Return the associated title object * - * @return Title|false + * @return Title */ public function getTitle() { return $this->title; @@ -319,17 +357,17 @@ abstract class File { } /** - * Return the storage path to the file. Note that this does - * not mean that a file actually exists under that location. - * - * This path depends on whether directory hashing is active or not, - * i.e. whether the files are all found in the same directory, - * or in hashed paths like /images/3/3c. - * - * Most callers don't check the return value, but ForeignAPIFile::getPath - * returns false. + * Return the storage path to the file. Note that this does + * not mean that a file actually exists under that location. + * + * This path depends on whether directory hashing is active or not, + * i.e. whether the files are all found in the same directory, + * or in hashed paths like /images/3/3c. + * + * Most callers don't check the return value, but ForeignAPIFile::getPath + * returns false. * - * @return string|false + * @return string|bool ForeignAPIFile::getPath can return false */ public function getPath() { if ( !isset( $this->path ) ) { @@ -344,7 +382,7 @@ abstract class File { * Returns false on failure. Callers must not alter the file. * Temporary files are cleared automatically. * - * @return string|false + * @return string|bool False on failure */ public function getLocalRefPath() { $this->assertRepoDefined(); @@ -383,7 +421,7 @@ abstract class File { * * @param $page int * - * @return false|number + * @return bool|number False on failure */ public function getHeight( $page = 1 ) { return false; @@ -430,9 +468,43 @@ abstract class File { } /** + * Will the thumbnail be animated if one would expect it to be. + * + * Currently used to add a warning to the image description page + * + * @return bool false if the main image is both animated + * and the thumbnail is not. In all other cases must return + * true. If image is not renderable whatsoever, should + * return true. + */ + public function canAnimateThumbIfAppropriate() { + $handler = $this->getHandler(); + if ( !$handler ) { + // We cannot handle image whatsoever, thus + // one would not expect it to be animated + // so true. + return true; + } else { + if ( $this->allowInlineDisplay() + && $handler->isAnimatedImage( $this ) + && !$handler->canAnimateThumbnail( $this ) + ) { + // Image is animated, but thumbnail isn't. + // This is unexpected to the user. + return false; + } else { + // Image is not animated, so one would + // not expect thumb to be + return true; + } + } + } + + /** * Get handler-specific metadata * Overridden by LocalFile, UnregisteredLocalFile * STUB + * @return bool */ public function getMetadata() { return false; @@ -462,6 +534,7 @@ abstract class File { * Return the bit depth of the file * Overridden by LocalFile * STUB + * @return int */ public function getBitDepth() { return 0; @@ -471,6 +544,7 @@ abstract class File { * Return the size of the image file, in bytes * Overridden by LocalFile, UnregisteredLocalFile * STUB + * @return bool */ public function getSize() { return false; @@ -492,6 +566,7 @@ abstract class File { * Use the value returned by this function with the MEDIATYPE_xxx constants. * Overridden by LocalFile, * STUB + * @return string */ function getMediaType() { return MEDIATYPE_UNKNOWN; @@ -518,6 +593,7 @@ abstract class File { /** * Accessor for __get() + * @return bool */ protected function getCanRender() { return $this->canRender(); @@ -686,15 +762,19 @@ abstract class File { } /** - * Return the file name of a thumbnail with the specified parameters + * Return the file name of a thumbnail with the specified parameters. + * Use File::THUMB_FULL_NAME to always get a name like "<params>-<source>". + * Otherwise, the format may be "<params>-<source>" or "<params>-thumbnail.<ext>". * * @param $params Array: handler-specific parameters - * @private -ish - * + * @param $flags integer Bitfield that supports THUMB_* constants * @return string */ - function thumbName( $params ) { - return $this->generateThumbName( $this->getName(), $params ); + public function thumbName( $params, $flags = 0 ) { + $name = ( $this->repo && !( $flags & self::THUMB_FULL_NAME ) ) + ? $this->repo->nameForThumb( $this->getName() ) + : $this->getName(); + return $this->generateThumbName( $name, $params ); } /** @@ -705,7 +785,7 @@ abstract class File { * * @return string */ - function generateThumbName( $name, $params ) { + public function generateThumbName( $name, $params ) { if ( !$this->getHandler() ) { return null; } @@ -750,7 +830,7 @@ abstract class File { /** * Return either a MediaTransformError or placeholder thumbnail (if $wgIgnoreImageErrors) - * + * * @param $thumbPath string Thumbnail storage path * @param $thumbUrl string Thumbnail URL * @param $params Array @@ -764,7 +844,7 @@ abstract class File { return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); } else { return new MediaTransformError( 'thumbnail_error', - $params['width'], 0, wfMsg( 'thumbnail-dest-create' ) ); + $params['width'], 0, wfMessage( 'thumbnail-dest-create' )->text() ); } } @@ -774,7 +854,7 @@ abstract class File { * @param $params Array: an associative array of handler-specific parameters. * Typical keys are width, height and page. * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering - * @return MediaTransformOutput|false + * @return MediaTransformOutput|bool False on failure */ function transform( $params, $flags = 0 ) { global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch; @@ -837,6 +917,13 @@ abstract class File { } } + // If the backend is ready-only, don't keep generating thumbnails + // only to return transformation errors, just return the error now. + if ( $this->repo->getReadOnlyReason() !== false ) { + $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ); + break; + } + // Create a temp FS file with the same extension and the thumbnail $thumbExt = FileBackend::extensionFromPath( $thumbPath ); $tmpFile = TempFSFile::factory( 'transform_', $thumbExt ); @@ -847,7 +934,9 @@ abstract class File { $tmpThumbPath = $tmpFile->getPath(); // path of 0-byte temp file // Actually render the thumbnail... + wfProfileIn( __METHOD__ . '-doTransform' ); $thumb = $this->handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $params ); + wfProfileOut( __METHOD__ . '-doTransform' ); $tmpFile->bind( $thumb ); // keep alive with $thumb if ( !$thumb ) { // bad params? @@ -859,19 +948,16 @@ abstract class File { $thumb = $this->handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $params ); } } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) { - $backend = $this->repo->getBackend(); - // Copy the thumbnail from the file system into storage. This avoids using - // FileRepo::store(); getThumbPath() uses a different zone in some subclasses. - $backend->prepare( array( 'dir' => dirname( $thumbPath ) ) ); - $status = $backend->store( - array( 'src' => $tmpThumbPath, 'dst' => $thumbPath, 'overwrite' => 1 ), - array( 'force' => 1, 'nonLocking' => 1, 'allowStale' => 1 ) - ); + // Copy the thumbnail from the file system into storage... + $disposition = $this->getThumbDisposition( $thumbName ); + $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition ); if ( $status->isOK() ) { $thumb->setStoragePath( $thumbPath ); } else { $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ); } + // Give extensions a chance to do something with this thumbnail... + wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) ); } // Purge. Useful in the event of Core -> Squid connection failure or squid @@ -889,6 +975,19 @@ abstract class File { } /** + * @param $thumbName string Thumbnail name + * @return string Content-Disposition header value + */ + function getThumbDisposition( $thumbName ) { + $fileName = $this->name; // file name to suggest + $thumbExt = FileBackend::extensionFromPath( $thumbName ); + if ( $thumbExt != '' && $thumbExt !== $this->getExtension() ) { + $fileName .= ".$thumbExt"; + } + return FileBackend::makeContentDisposition( 'inline', $fileName ); + } + + /** * Hook into transform() to allow migration of thumbnail files * STUB * Overridden by LocalFile @@ -920,7 +1019,8 @@ abstract class File { $path = '/common/images/icons/' . $icon; $filepath = $wgStyleDirectory . $path; if ( file_exists( $filepath ) ) { // always FS - return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 ); + $params = array( 'width' => 120, 'height' => 120 ); + return new ThumbnailImage( $this, $wgStylePath . $path, false, $params ); } } return null; @@ -938,6 +1038,7 @@ abstract class File { * Get all thumbnail names previously generated for this file * STUB * Overridden by LocalFile + * @return array */ function getThumbnails() { return array(); @@ -987,13 +1088,13 @@ abstract class File { * * STUB * @param $limit integer Limit of rows to return - * @param $start timestamp Only revisions older than $start will be returned - * @param $end timestamp Only revisions newer than $end will be returned + * @param $start string timestamp Only revisions older than $start will be returned + * @param $end string timestamp Only revisions newer than $end will be returned * @param $inc bool Include the endpoints of the time range * * @return array */ - function getHistory($limit = null, $start = null, $end = null, $inc=true) { + function getHistory( $limit = null, $start = null, $end = null, $inc=true ) { return array(); } @@ -1004,6 +1105,7 @@ abstract class File { * * STUB * Overridden in LocalFile + * @return bool */ public function nextHistoryLine() { return false; @@ -1185,7 +1287,7 @@ abstract class File { * * @param $suffix bool|string if not false, the name of a thumbnail file * - * @return path + * @return string path */ function getThumbUrl( $suffix = false ) { $this->assertRepoDefined(); @@ -1251,7 +1353,7 @@ abstract class File { */ function isHashed() { $this->assertRepoDefined(); - return $this->repo->isHashed(); + return (bool)$this->repo->getHashLevels(); } /** @@ -1329,7 +1431,7 @@ abstract class File { /** * Returns the repository * - * @return FileRepo|false + * @return FileRepo|bool */ function getRepo() { return $this->repo; @@ -1360,6 +1462,7 @@ abstract class File { /** * Return the deletion bitfield * STUB + * @return int */ function getVisibility() { return 0; @@ -1401,7 +1504,7 @@ abstract class File { * * @param $reason String * @param $suppress Boolean: hide content from sysops? - * @return true on success, false on some kind of failure + * @return bool on success, false on some kind of failure * STUB * Overridden by LocalFile */ @@ -1418,7 +1521,7 @@ abstract class File { * @param $versions array set of record ids of deleted items to restore, * or empty to restore all revisions. * @param $unsuppress bool remove restrictions on content upon restoration? - * @return int|false the number of file revisions restored if successful, + * @return int|bool the number of file revisions restored if successful, * or false on failure * STUB * Overridden by LocalFile @@ -1442,7 +1545,7 @@ abstract class File { * Returns the number of pages of a multipage document, or false for * documents which aren't multipage documents * - * @return false|int + * @return bool|int */ function pageCount() { if ( !isset( $this->pageCount ) ) { @@ -1536,19 +1639,25 @@ abstract class File { } /** - * Get discription of file revision + * Get description of file revision * STUB * + * @param $audience Integer: one of: + * File::FOR_PUBLIC to be displayed to all users + * File::FOR_THIS_USER to be displayed to the given user + * File::RAW get the description regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter * @return string */ - function getDescription() { + function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) { return null; } /** * Get the 14-character timestamp of the file upload * - * @return string|false TS_MW timestamp or false on failure + * @return string|bool TS_MW timestamp or false on failure */ function getTimestamp() { $this->assertRepoDefined(); @@ -1566,7 +1675,7 @@ abstract class File { } /** - * Get the deletion archive key, <sha1>.<ext> + * Get the deletion archive key, "<sha1>.<ext>" * * @return string */ @@ -1618,7 +1727,7 @@ abstract class File { * * @param $path string * - * @return false|string False on failure + * @return bool|string False on failure */ static function sha1Base36( $path ) { wfDeprecated( __METHOD__, '1.19' ); @@ -1698,6 +1807,14 @@ abstract class File { } /** + * Check if this file object is small and can be cached + * @return boolean + */ + public function isCacheable() { + return true; + } + + /** * Assert that $this->repo is set to a valid FileRepo instance * @throws MWException */ diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php index 681544fd..56482611 100644 --- a/includes/filerepo/file/ForeignAPIFile.php +++ b/includes/filerepo/file/ForeignAPIFile.php @@ -2,6 +2,21 @@ /** * Foreign file accessible through api.php requests. * + * 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 FileAbstraction */ @@ -39,9 +54,9 @@ class ForeignAPIFile extends File { */ static function newFromTitle( Title $title, $repo ) { $data = $repo->fetchImageQuery( array( - 'titles' => 'File:' . $title->getDBKey(), - 'iiprop' => self::getProps(), - 'prop' => 'imageinfo', + 'titles' => 'File:' . $title->getDBKey(), + 'iiprop' => self::getProps(), + 'prop' => 'imageinfo', 'iimetadataversion' => MediaHandler::getMetadataVersion() ) ); @@ -68,20 +83,33 @@ class ForeignAPIFile extends File { /** * Get the property string for iiprop and aiprop + * @return string */ static function getProps() { return 'timestamp|user|comment|url|size|sha1|metadata|mime'; } // Dummy functions... + + /** + * @return bool + */ public function exists() { return $this->mExists; } + /** + * @return bool + */ public function getPath() { return false; } + /** + * @param Array $params + * @param int $flags + * @return bool|MediaTransformOutput + */ function transform( $params, $flags = 0 ) { if( !$this->canRender() ) { // show icon @@ -101,6 +129,11 @@ class ForeignAPIFile extends File { } // Info we can get from API... + + /** + * @param $page int + * @return int|number + */ public function getWidth( $page = 1 ) { return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0; } @@ -113,6 +146,9 @@ class ForeignAPIFile extends File { return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0; } + /** + * @return bool|null|string + */ public function getMetadata() { if ( isset( $this->mInfo['metadata'] ) ) { return serialize( self::parseMetadata( $this->mInfo['metadata'] ) ); @@ -120,6 +156,10 @@ class ForeignAPIFile extends File { return null; } + /** + * @param $metadata array + * @return array + */ public static function parseMetadata( $metadata ) { if( !is_array( $metadata ) ) { return $metadata; @@ -131,28 +171,47 @@ class ForeignAPIFile extends File { return $ret; } + /** + * @return bool|int|null + */ public function getSize() { return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null; } + /** + * @return null|string + */ public function getUrl() { return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null; } + /** + * @param string $method + * @return int|null|string + */ public function getUser( $method='text' ) { return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null; } - public function getDescription() { + /** + * @return null|string + */ + public function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) { return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null; } + /** + * @return null|String + */ function getSha1() { return isset( $this->mInfo['sha1'] ) ? wfBaseConvert( strval( $this->mInfo['sha1'] ), 16, 36, 31 ) : null; } + /** + * @return bool|Mixed|string + */ function getTimestamp() { return wfTimestamp( TS_MW, isset( $this->mInfo['timestamp'] ) @@ -161,6 +220,9 @@ class ForeignAPIFile extends File { ); } + /** + * @return string + */ function getMimeType() { if( !isset( $this->mInfo['mime'] ) ) { $magic = MimeMagic::singleton(); @@ -169,12 +231,18 @@ class ForeignAPIFile extends File { return $this->mInfo['mime']; } - /// @todo FIXME: May guess wrong on file types that can be eg audio or video + /** + * @todo FIXME: May guess wrong on file types that can be eg audio or video + * @return int|string + */ function getMediaType() { $magic = MimeMagic::singleton(); return $magic->getMediaType( null, $this->getMimeType() ); } + /** + * @return bool|string + */ function getDescriptionUrl() { return isset( $this->mInfo['descriptionurl'] ) ? $this->mInfo['descriptionurl'] @@ -183,6 +251,8 @@ class ForeignAPIFile extends File { /** * Only useful if we're locally caching thumbs anyway... + * @param $suffix string + * @return null|string */ function getThumbPath( $suffix = '' ) { if ( $this->repo->canCacheThumbs() ) { @@ -196,6 +266,9 @@ class ForeignAPIFile extends File { } } + /** + * @return array + */ function getThumbnails() { $dir = $this->getThumbPath( $this->getName() ); $iter = $this->repo->getBackend()->getFileList( array( 'dir' => $dir ) ); @@ -225,6 +298,9 @@ class ForeignAPIFile extends File { $wgMemc->delete( $key ); } + /** + * @param $options array + */ function purgeThumbnails( $options = array() ) { global $wgMemc; @@ -245,8 +321,8 @@ class ForeignAPIFile extends File { } # Delete the thumbnails - $this->repo->cleanupBatch( $purgeList, FileRepo::SKIP_LOCKING ); + $this->repo->quickPurgeBatch( $purgeList ); # Clear out the thumbnail directory if empty - $this->repo->getBackend()->clean( array( 'dir' => $dir ) ); + $this->repo->quickCleanDir( $dir ); } } diff --git a/includes/filerepo/file/ForeignDBFile.php b/includes/filerepo/file/ForeignDBFile.php index 191a712d..91f6cb62 100644 --- a/includes/filerepo/file/ForeignDBFile.php +++ b/includes/filerepo/file/ForeignDBFile.php @@ -1,6 +1,21 @@ <?php /** - * Foreign file with an accessible MediaWiki database + * Foreign file with an accessible MediaWiki database. + * + * 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 FileAbstraction @@ -39,23 +54,52 @@ class ForeignDBFile extends LocalFile { return $file; } + /** + * @param $srcPath String + * @param $flags int + * @throws MWException + */ function publish( $srcPath, $flags = 0 ) { $this->readOnlyError(); } + /** + * @param $oldver + * @param $desc string + * @param $license string + * @param $copyStatus string + * @param $source string + * @param $watch bool + * @param $timestamp bool|string + * @throws MWException + */ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false, $timestamp = false ) { $this->readOnlyError(); } + /** + * @param $versions array + * @param $unsuppress bool + * @throws MWException + */ function restore( $versions = array(), $unsuppress = false ) { $this->readOnlyError(); } + /** + * @param $reason string + * @param $suppress bool + * @throws MWException + */ function delete( $reason, $suppress = false ) { $this->readOnlyError(); } + /** + * @param $target Title + * @throws MWException + */ function move( $target ) { $this->readOnlyError(); } diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 0f8b4754..695c4e9e 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1,6 +1,21 @@ <?php /** - * Local file in the wiki's own database + * Local file in the wiki's own database. + * + * 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 FileAbstraction @@ -29,6 +44,8 @@ define( 'MW_FILE_VERSION', 8 ); * @ingroup FileAbstraction */ class LocalFile extends File { + const CACHE_FIELD_MAX_LEN = 1000; + /**#@+ * @private */ @@ -58,6 +75,11 @@ class LocalFile extends File { /**#@-*/ + /** + * @var LocalRepo + */ + var $repo; + protected $repoClass = 'LocalRepo'; /** @@ -121,6 +143,7 @@ class LocalFile extends File { /** * Fields in the image table + * @return array */ static function selectFields() { return array( @@ -160,6 +183,7 @@ class LocalFile extends File { /** * Get the memcached key for the main data for this file, or false if * there is no access to the shared cache. + * @return bool */ function getCacheKey() { $hashedName = md5( $this->getName() ); @@ -169,6 +193,7 @@ class LocalFile extends File { /** * Try to load file metadata from memcached. Returns true on success. + * @return bool */ function loadFromCache() { global $wgMemc; @@ -238,6 +263,10 @@ class LocalFile extends File { $this->setProps( $props ); } + /** + * @param $prefix string + * @return array + */ function getCacheFields( $prefix = 'img_' ) { static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' ); @@ -286,6 +315,10 @@ class LocalFile extends File { /** * Decode a row from the database (either object or array) to an array * with timestamps and MIME types decoded, and the field prefix removed. + * @param $row + * @param $prefix string + * @throws MWException + * @return array */ function decodeRow( $row, $prefix = 'img_' ) { $array = (array)$row; @@ -407,6 +440,7 @@ class LocalFile extends File { $dbw->update( 'image', array( + 'img_size' => $this->size, // sanity 'img_width' => $this->width, 'img_height' => $this->height, 'img_bits' => $this->bits, @@ -462,9 +496,12 @@ class LocalFile extends File { /** getPath inherited */ /** isVisible inhereted */ + /** + * @return bool + */ function isMissing() { if ( $this->missing === null ) { - list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl(), FileRepo::FILES_ONLY ); + list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() ); $this->missing = !$fileExists; } return $this->missing; @@ -473,7 +510,8 @@ class LocalFile extends File { /** * Return the width of the image * - * Returns false on error + * @param $page int + * @return bool|int Returns false on error */ public function getWidth( $page = 1 ) { $this->load(); @@ -493,7 +531,8 @@ class LocalFile extends File { /** * Return the height of the image * - * Returns false on error + * @param $page int + * @return bool|int Returns false on error */ public function getHeight( $page = 1 ) { $this->load(); @@ -514,6 +553,7 @@ class LocalFile extends File { * Returns ID or name of user who uploaded the file * * @param $type string 'text' or 'id' + * @return int|string */ function getUser( $type = 'text' ) { $this->load(); @@ -527,12 +567,16 @@ class LocalFile extends File { /** * Get handler-specific metadata + * @return string */ function getMetadata() { $this->load(); return $this->metadata; } + /** + * @return int + */ function getBitDepth() { $this->load(); return $this->bits; @@ -540,6 +584,7 @@ class LocalFile extends File { /** * Return the size of the image file, in bytes + * @return int */ public function getSize() { $this->load(); @@ -548,6 +593,7 @@ class LocalFile extends File { /** * Returns the mime type of the file. + * @return string */ function getMimeType() { $this->load(); @@ -557,6 +603,7 @@ class LocalFile extends File { /** * Return the type of the media in the file. * Use the value returned by this function with the MEDIATYPE_xxx constants. + * @return string */ function getMediaType() { $this->load(); @@ -586,6 +633,9 @@ class LocalFile extends File { /** * Fix thumbnail files from 1.4 or before, with extreme prejudice + * @todo : do we still care about this? Perhaps a maintenance script + * can be made instead. Enabling this code results in a serious + * RTT regression for wikis without 404 handling. */ function migrateThumbFile( $thumbName ) { $thumbDir = $this->getThumbPath(); @@ -608,10 +658,12 @@ class LocalFile extends File { } */ - if ( $this->repo->fileExists( $thumbDir, FileRepo::FILES_ONLY ) ) { + /* + if ( $this->repo->fileExists( $thumbDir ) ) { // Delete file where directory should be $this->repo->cleanupBatch( array( $thumbDir ) ); } + */ } /** getHandler inherited */ @@ -620,12 +672,10 @@ class LocalFile extends File { /** * Get all thumbnail names previously generated for this file - * @param $archiveName string|false Name of an archive file + * @param $archiveName string|bool Name of an archive file, default false * @return array first element is the base dir, then files in that base dir. */ function getThumbnails( $archiveName = false ) { - $this->load(); - if ( $archiveName ) { $dir = $this->getArchiveThumbPath( $archiveName ); } else { @@ -690,6 +740,8 @@ class LocalFile extends File { */ function purgeOldThumbnails( $archiveName ) { global $wgUseSquid; + wfProfileIn( __METHOD__ ); + // Get a list of old thumbnails and URLs $files = $this->getThumbnails( $archiveName ); $dir = array_shift( $files ); @@ -706,6 +758,8 @@ class LocalFile extends File { } SquidUpdate::purge( $urls ); } + + wfProfileOut( __METHOD__ ); } /** @@ -713,6 +767,7 @@ class LocalFile extends File { */ function purgeThumbnails( $options = array() ) { global $wgUseSquid; + wfProfileIn( __METHOD__ ); // Delete thumbnails $files = $this->getThumbnails(); @@ -739,6 +794,8 @@ class LocalFile extends File { } SquidUpdate::purge( $urls ); } + + wfProfileOut( __METHOD__ ); } /** @@ -763,14 +820,21 @@ class LocalFile extends File { } # Delete the thumbnails - $this->repo->cleanupBatch( $purgeList, FileRepo::SKIP_LOCKING ); + $this->repo->quickPurgeBatch( $purgeList ); # Clear out the thumbnail directory if empty - $this->repo->getBackend()->clean( array( 'dir' => $dir ) ); + $this->repo->quickCleanDir( $dir ); } /** purgeDescription inherited */ /** purgeEverything inherited */ + /** + * @param $limit null + * @param $start null + * @param $end null + * @param $inc bool + * @return array + */ function getHistory( $limit = null, $start = null, $end = null, $inc = true ) { $dbr = $this->repo->getSlaveDB(); $tables = array( 'oldimage' ); @@ -824,6 +888,7 @@ class LocalFile extends File { * 0 return line for current version * 1 query for old versions, return first one * 2, ... return next old version from above query + * @return bool */ public function nextHistoryLine() { # Polymorphic function name to distinguish foreign and local fetches @@ -888,21 +953,25 @@ class LocalFile extends File { * @param $comment String: upload description * @param $pageText String: text to use for the new description page, * if a new description page is created - * @param $flags Integer: flags for publish() - * @param $props Array: File properties, if known. This can be used to reduce the + * @param $flags Integer|bool: flags for publish() + * @param $props Array|bool: File properties, if known. This can be used to reduce the * upload time when uploading virtual URLs for which the file info * is already known - * @param $timestamp String: timestamp for img_timestamp, or false to use the current time - * @param $user Mixed: User object or null to use $wgUser + * @param $timestamp String|bool: timestamp for img_timestamp, or false to use the current time + * @param $user User|null: User object or null to use $wgUser * * @return FileRepoStatus object. On success, the value member contains the * archive name, or an empty string if it was a new file. */ function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) { global $wgContLang; + + if ( $this->getRepo()->getReadOnlyReason() !== false ) { + return $this->readOnlyFatalStatus(); + } + // truncate nicely or the DB will do it for us - // non-nicely (dangling multi-byte chars, non-truncated - // version in cache). + // non-nicely (dangling multi-byte chars, non-truncated version in cache). $comment = $wgContLang->truncate( $comment, 255 ); $this->lock(); // begin $status = $this->publish( $srcPath, $flags ); @@ -923,6 +992,14 @@ class LocalFile extends File { /** * Record a file upload in the upload log and the image table + * @param $oldver + * @param $desc string + * @param $license string + * @param $copyStatus string + * @param $source string + * @param $watch bool + * @param $timestamp string|bool + * @return bool */ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false, $timestamp = false ) @@ -942,20 +1019,31 @@ class LocalFile extends File { /** * Record a file upload in the upload log and the image table + * @param $oldver + * @param $comment string + * @param $pageText string + * @param $props bool|array + * @param $timestamp bool|string + * @param $user null|User + * @return bool */ function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null ) { + wfProfileIn( __METHOD__ ); + if ( is_null( $user ) ) { global $wgUser; $user = $wgUser; } $dbw = $this->repo->getMasterDB(); - $dbw->begin(); + $dbw->begin( __METHOD__ ); if ( !$props ) { + wfProfileIn( __METHOD__ . '-getProps' ); $props = $this->repo->getFileProps( $this->getVirtualUrl() ); + wfProfileOut( __METHOD__ . '-getProps' ); } if ( $timestamp === false ) { @@ -968,15 +1056,10 @@ class LocalFile extends File { $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW $this->setProps( $props ); - # Delete thumbnails - $this->purgeThumbnails(); - - # The file is already on its final location, remove it from the squid cache - SquidUpdate::purge( array( $this->getURL() ) ); - # Fail now if the file isn't there if ( !$this->fileExists ) { wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" ); + wfProfileOut( __METHOD__ ); return false; } @@ -1005,15 +1088,12 @@ class LocalFile extends File { __METHOD__, 'IGNORE' ); - if ( $dbw->affectedRows() == 0 ) { - if ( $oldver == '' ) { // XXX - # (bug 34993) publish() can displace the current file and yet fail to save - # a new one. The next publish attempt will treat the file as a brand new file - # and pass an empty $oldver. Allow this bogus value so we can displace the - # `image` row to `oldimage`, leaving room for the new current file `image` row. - #throw new MWException( "Empty oi_archive_name. Database and storage out of sync?" ); - } + # (bug 34993) Note: $oldver can be empty here, if the previous + # version of the file was broken. Allow registration of the new + # version to continue anyway, because that's better than having + # an image that's not fixable by user operations. + $reupload = true; # Collision, this is an update of a file # Insert previous contents into oldimage @@ -1060,16 +1140,8 @@ class LocalFile extends File { __METHOD__ ); } else { - # This is a new file - # Update the image count - $dbw->begin( __METHOD__ ); - $dbw->update( - 'site_stats', - array( 'ss_images = ss_images+1' ), - '*', - __METHOD__ - ); - $dbw->commit( __METHOD__ ); + # This is a new file, so update the image count + DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) ); } $descTitle = $this->getTitle(); @@ -1079,14 +1151,17 @@ class LocalFile extends File { # Add the log entry $log = new LogPage( 'upload' ); $action = $reupload ? 'overwrite' : 'upload'; - $log->addEntry( $action, $descTitle, $comment, array(), $user ); + $logId = $log->addEntry( $action, $descTitle, $comment, array(), $user ); + + wfProfileIn( __METHOD__ . '-edit' ); + $exists = $descTitle->exists(); - if ( $descTitle->exists() ) { + if ( $exists ) { # Create a null revision $latest = $descTitle->getLatestRevID(); $nullRevision = Revision::newNullRevision( $dbw, - $descTitle->getArticleId(), + $descTitle->getArticleID(), $log->getRcComment(), false ); @@ -1096,6 +1171,15 @@ class LocalFile extends File { wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) ); $wikiPage->updateRevisionOn( $dbw, $nullRevision ); } + } + + # Commit the transaction now, in case something goes wrong later + # The most important thing is that files don't get lost, especially archives + # NOTE: once we have support for nested transactions, the commit may be moved + # to after $wikiPage->doEdit has been called. + $dbw->commit( __METHOD__ ); + + if ( $exists ) { # Invalidate the cache for the description page $descTitle->invalidateCache(); $descTitle->purgeSquid(); @@ -1103,12 +1187,19 @@ class LocalFile extends File { # New file; create the description page. # There's already a log entry, so don't make a second RC entry # Squid and file cache for the description page are purged by doEdit. - $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user ); + $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user ); + + if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction + $dbw->begin(); + $dbw->update( 'logging', + array( 'log_page' => $status->value['revision']->getPage() ), + array( 'log_id' => $logId ), + __METHOD__ + ); + $dbw->commit(); // commit before anything bad can happen + } } - - # Commit the transaction now, in case something goes wrong later - # The most important thing is that files don't get lost, especially archives - $dbw->commit(); + wfProfileOut( __METHOD__ . '-edit' ); # Save to cache and purge the squid # We shall not saveToCache before the commit since otherwise @@ -1116,8 +1207,20 @@ class LocalFile extends File { # which in fact doesn't really exist (bug 24978) $this->saveToCache(); + if ( $reupload ) { + # Delete old thumbnails + wfProfileIn( __METHOD__ . '-purge' ); + $this->purgeThumbnails(); + wfProfileOut( __METHOD__ . '-purge' ); + + # Remove the old file from the squid cache + SquidUpdate::purge( array( $this->getURL() ) ); + } + # Hooks, hooks, the magic of hooks... + wfProfileIn( __METHOD__ . '-hooks' ); wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) ); + wfProfileOut( __METHOD__ . '-hooks' ); # Invalidate cache for all pages using this file $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); @@ -1131,6 +1234,7 @@ class LocalFile extends File { $update->doUpdate(); } + wfProfileOut( __METHOD__ ); return true; } @@ -1167,6 +1271,10 @@ class LocalFile extends File { * archive name, or an empty string if it was a new file. */ function publishTo( $srcPath, $dstRel, $flags = 0 ) { + if ( $this->getRepo()->getReadOnlyReason() !== false ) { + return $this->readOnlyFatalStatus(); + } + $this->lock(); // begin $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName(); @@ -1203,20 +1311,26 @@ class LocalFile extends File { * @return FileRepoStatus object. */ function move( $target ) { - wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() ); - $this->lock(); // begin + if ( $this->getRepo()->getReadOnlyReason() !== false ) { + return $this->readOnlyFatalStatus(); + } + wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() ); $batch = new LocalFileMoveBatch( $this, $target ); - $batch->addCurrent(); - $batch->addOlds(); + $this->lock(); // begin + $batch->addCurrent(); + $archiveNames = $batch->addOlds(); $status = $batch->execute(); + $this->unlock(); // done + wfDebugLog( 'imagemove', "Finished moving {$this->name}" ); $this->purgeEverything(); - $this->unlock(); // done - - if ( $status->isOk() ) { + foreach ( $archiveNames as $archiveName ) { + $this->purgeOldThumbnails( $archiveName ); + } + if ( $status->isOK() ) { // Now switch the object $this->title = $target; // Force regeneration of the name and hashpath @@ -1242,30 +1356,27 @@ class LocalFile extends File { * @return FileRepoStatus object. */ function delete( $reason, $suppress = false ) { - $this->lock(); // begin + if ( $this->getRepo()->getReadOnlyReason() !== false ) { + return $this->readOnlyFatalStatus(); + } $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); - $batch->addCurrent(); + $this->lock(); // begin + $batch->addCurrent(); # Get old version relative paths - $dbw = $this->repo->getMasterDB(); - $result = $dbw->select( 'oldimage', - array( 'oi_archive_name' ), - array( 'oi_name' => $this->getName() ) ); - foreach ( $result as $row ) { - $batch->addOld( $row->oi_archive_name ); - $this->purgeOldThumbnails( $row->oi_archive_name ); - } + $archiveNames = $batch->addOlds(); $status = $batch->execute(); + $this->unlock(); // done - if ( $status->ok ) { - // Update site_stats - $site_stats = $dbw->tableName( 'site_stats' ); - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ ); - $this->purgeEverything(); + if ( $status->isOK() ) { + DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) ); } - $this->unlock(); // done + $this->purgeEverything(); + foreach ( $archiveNames as $archiveName ) { + $this->purgeOldThumbnails( $archiveName ); + } return $status; } @@ -1285,16 +1396,19 @@ class LocalFile extends File { * @return FileRepoStatus object. */ function deleteOld( $archiveName, $reason, $suppress = false ) { - $this->lock(); // begin + if ( $this->getRepo()->getReadOnlyReason() !== false ) { + return $this->readOnlyFatalStatus(); + } $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); + + $this->lock(); // begin $batch->addOld( $archiveName ); - $this->purgeOldThumbnails( $archiveName ); $status = $batch->execute(); - $this->unlock(); // done - if ( $status->ok ) { + $this->purgeOldThumbnails( $archiveName ); + if ( $status->isOK() ) { $this->purgeDescription(); $this->purgeHistory(); } @@ -1308,30 +1422,32 @@ class LocalFile extends File { * * May throw database exceptions on error. * - * @param $versions set of record ids of deleted items to restore, + * @param $versions array set of record ids of deleted items to restore, * or empty to restore all revisions. * @param $unsuppress Boolean * @return FileRepoStatus */ function restore( $versions = array(), $unsuppress = false ) { + if ( $this->getRepo()->getReadOnlyReason() !== false ) { + return $this->readOnlyFatalStatus(); + } + $batch = new LocalFileRestoreBatch( $this, $unsuppress ); + $this->lock(); // begin if ( !$versions ) { $batch->addAll(); } else { $batch->addIds( $versions ); } - $status = $batch->execute(); - - if ( !$status->isGood() ) { - return $status; + if ( $status->isGood() ) { + $cleanupStatus = $batch->cleanup(); + $cleanupStatus->successCount = 0; + $cleanupStatus->failCount = 0; + $status->merge( $cleanupStatus ); } - - $cleanupStatus = $batch->cleanup(); - $cleanupStatus->successCount = 0; - $cleanupStatus->failCount = 0; - $status->merge( $cleanupStatus ); + $this->unlock(); // done return $status; } @@ -1343,6 +1459,7 @@ class LocalFile extends File { /** * Get the URL of the file description page. + * @return String */ function getDescriptionUrl() { return $this->title->getLocalUrl(); @@ -1352,10 +1469,11 @@ class LocalFile extends File { * Get the HTML text of the description page * This is not used by ImagePage for local files, since (among other things) * it skips the parser cache. + * @return bool|mixed */ function getDescriptionText() { global $wgParser; - $revision = Revision::newFromTitle( $this->title ); + $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL ); if ( !$revision ) return false; $text = $revision->getText(); if ( !$text ) return false; @@ -1363,16 +1481,33 @@ class LocalFile extends File { return $pout->getText(); } - function getDescription() { + /** + * @return string + */ + function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) { $this->load(); - return $this->description; + if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { + return ''; + } elseif ( $audience == self::FOR_THIS_USER + && !$this->userCan( self::DELETED_COMMENT, $user ) ) + { + return ''; + } else { + return $this->description; + } } + /** + * @return bool|string + */ function getTimestamp() { $this->load(); return $this->timestamp; } + /** + * @return string + */ function getSha1() { $this->load(); // Initialise now if necessary @@ -1396,6 +1531,14 @@ class LocalFile extends File { } /** + * @return bool + */ + function isCacheable() { + $this->load(); + return strlen( $this->metadata ) <= self::CACHE_FIELD_MAX_LEN; // avoid OOMs + } + + /** * Start a transaction and lock the image for update * Increments a reference counter if the lock is already held * @return boolean True if the image exists, false otherwise @@ -1404,11 +1547,12 @@ class LocalFile extends File { $dbw = $this->repo->getMasterDB(); if ( !$this->locked ) { - $dbw->begin(); + $dbw->begin( __METHOD__ ); $this->locked++; } - return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ ); + return $dbw->selectField( 'image', '1', + array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) ); } /** @@ -1420,7 +1564,7 @@ class LocalFile extends File { --$this->locked; if ( !$this->locked ) { $dbw = $this->repo->getMasterDB(); - $dbw->commit(); + $dbw->commit( __METHOD__ ); } } } @@ -1431,7 +1575,15 @@ class LocalFile extends File { function unlockAndRollback() { $this->locked = false; $dbw = $this->repo->getMasterDB(); - $dbw->rollback(); + $dbw->rollback( __METHOD__ ); + } + + /** + * @return Status + */ + protected function readOnlyFatalStatus() { + return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(), + $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() ); } } // LocalFile class @@ -1451,6 +1603,11 @@ class LocalFileDeleteBatch { var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress; var $status; + /** + * @param $file File + * @param $reason string + * @param $suppress bool + */ function __construct( File $file, $reason = '', $suppress = false ) { $this->file = $file; $this->reason = $reason; @@ -1462,11 +1619,39 @@ class LocalFileDeleteBatch { $this->srcRels['.'] = $this->file->getRel(); } + /** + * @param $oldName string + */ function addOld( $oldName ) { $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName ); $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName ); } + /** + * Add the old versions of the image to the batch + * @return Array List of archive names from old versions + */ + function addOlds() { + $archiveNames = array(); + + $dbw = $this->file->repo->getMasterDB(); + $result = $dbw->select( 'oldimage', + array( 'oi_archive_name' ), + array( 'oi_name' => $this->file->getName() ), + __METHOD__ + ); + + foreach ( $result as $row ) { + $this->addOld( $row->oi_archive_name ); + $archiveNames[] = $row->oi_archive_name; + } + + return $archiveNames; + } + + /** + * @return array + */ function getOldRels() { if ( !isset( $this->srcRels['.'] ) ) { $oldRels =& $this->srcRels; @@ -1480,6 +1665,9 @@ class LocalFileDeleteBatch { return array( $oldRels, $deleteCurrent ); } + /** + * @return array + */ protected function getHashes() { $hashes = array(); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); @@ -1601,7 +1789,7 @@ class LocalFileDeleteBatch { 'fa_deleted_user' => $encUserId, 'fa_deleted_timestamp' => $encTimestamp, 'fa_deleted_reason' => $encReason, - 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted', + 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted', 'fa_name' => 'oi_name', 'fa_archive_name' => 'oi_archive_name', @@ -1617,7 +1805,6 @@ class LocalFileDeleteBatch { 'fa_user' => 'oi_user', 'fa_user_text' => 'oi_user_text', 'fa_timestamp' => 'oi_timestamp', - 'fa_deleted' => $bitfield ), $where, __METHOD__ ); } } @@ -1641,9 +1828,9 @@ class LocalFileDeleteBatch { /** * Run the transaction + * @return FileRepoStatus */ function execute() { - global $wgUseSquid; wfProfileIn( __METHOD__ ); $this->file->lock(); @@ -1699,7 +1886,7 @@ class LocalFileDeleteBatch { $this->status->merge( $status ); } - if ( !$this->status->ok ) { + if ( !$this->status->isOK() ) { // Critical file deletion error // Roll back inserts, release lock and abort // TODO: delete the defunct filearchive rows if we are using a non-transactional DB @@ -1708,17 +1895,6 @@ class LocalFileDeleteBatch { return $this->status; } - // Purge squid - if ( $wgUseSquid ) { - $urls = array(); - - foreach ( $this->srcRels as $srcRel ) { - $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) ); - $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel; - } - SquidUpdate::purge( $urls ); - } - // Delete image/oldimage rows $this->doDBDeletes(); @@ -1731,6 +1907,8 @@ class LocalFileDeleteBatch { /** * Removes non-existent files from a deletion batch. + * @param $batch array + * @return array */ function removeNonexistentFiles( $batch ) { $files = $newBatch = array(); @@ -1740,7 +1918,7 @@ class LocalFileDeleteBatch { $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src ); } - $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY ); + $result = $this->file->repo->fileExistsBatch( $files ); foreach ( $batch as $batchItem ) { if ( $result[$batchItem[0]] ) { @@ -1766,6 +1944,10 @@ class LocalFileRestoreBatch { var $cleanupBatch, $ids, $all, $unsuppress = false; + /** + * @param $file File + * @param $unsuppress bool + */ function __construct( File $file, $unsuppress = false ) { $this->file = $file; $this->cleanupBatch = $this->ids = array(); @@ -1800,6 +1982,7 @@ class LocalFileRestoreBatch { * rows and there's no need to keep the image row locked while it's acquiring those locks * The caller may have its own transaction open. * So we save the batch and let the caller call cleanup() + * @return FileRepoStatus */ function execute() { global $wgLang; @@ -2003,9 +2186,7 @@ class LocalFileRestoreBatch { if ( !$exists ) { wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" ); - // Update site_stats - $site_stats = $dbw->tableName( 'site_stats' ); - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); + DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) ); $this->file->purgeEverything(); } else { @@ -2022,13 +2203,16 @@ class LocalFileRestoreBatch { /** * Removes non-existent files from a store batch. + * @param $triplets array + * @return array */ function removeNonexistentFiles( $triplets ) { $files = $filteredTriplets = array(); - foreach ( $triplets as $file ) + foreach ( $triplets as $file ) { $files[$file[0]] = $file[0]; + } - $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY ); + $result = $this->file->repo->fileExistsBatch( $files ); foreach ( $triplets as $file ) { if ( $result[$file[0]] ) { @@ -2041,6 +2225,8 @@ class LocalFileRestoreBatch { /** * Removes non-existent files from a cleanup batch. + * @param $batch array + * @return array */ function removeNonexistentFromCleanup( $batch ) { $files = $newBatch = array(); @@ -2051,7 +2237,7 @@ class LocalFileRestoreBatch { rawurlencode( $repo->getDeletedHashPath( $file ) . $file ); } - $result = $repo->fileExistsBatch( $files, FileRepo::FILES_ONLY ); + $result = $repo->fileExistsBatch( $files ); foreach ( $batch as $file ) { if ( $result[$file] ) { @@ -2065,6 +2251,7 @@ class LocalFileRestoreBatch { /** * Delete unused files in the deleted zone. * This should be called from outside the transaction in which execute() was called. + * @return FileRepoStatus|void */ function cleanup() { if ( !$this->cleanupBatch ) { @@ -2109,7 +2296,7 @@ class LocalFileRestoreBatch { class LocalFileMoveBatch { /** - * @var File + * @var LocalFile */ var $file; @@ -2118,8 +2305,17 @@ class LocalFileMoveBatch { */ var $target; - var $cur, $olds, $oldCount, $archive, $db; + var $cur, $olds, $oldCount, $archive; + /** + * @var DatabaseBase + */ + var $db; + + /** + * @param File $file + * @param Title $target + */ function __construct( File $file, Title $target ) { $this->file = $file; $this->target = $target; @@ -2129,7 +2325,7 @@ class LocalFileMoveBatch { $this->newName = $this->file->repo->getNameFromTitle( $this->target ); $this->oldRel = $this->oldHash . $this->oldName; $this->newRel = $this->newHash . $this->newName; - $this->db = $file->repo->getMasterDb(); + $this->db = $file->getRepo()->getMasterDb(); } /** @@ -2141,11 +2337,13 @@ class LocalFileMoveBatch { /** * Add the old versions of the image to the batch + * @return Array List of archive names from old versions */ function addOlds() { $archiveBase = 'archive'; $this->olds = array(); $this->oldCount = 0; + $archiveNames = array(); $result = $this->db->select( 'oldimage', array( 'oi_archive_name', 'oi_deleted' ), @@ -2154,6 +2352,7 @@ class LocalFileMoveBatch { ); foreach ( $result as $row ) { + $archiveNames[] = $row->oi_archive_name; $oldName = $row->oi_archive_name; $bits = explode( '!', $oldName, 2 ); @@ -2181,39 +2380,49 @@ class LocalFileMoveBatch { "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}" ); } + + return $archiveNames; } /** * Perform the move. + * @return FileRepoStatus */ function execute() { $repo = $this->file->repo; $status = $repo->newGood(); - $triplets = $this->getMoveTriplets(); + $triplets = $this->getMoveTriplets(); $triplets = $this->removeNonexistentFiles( $triplets ); - // Copy the files into their new location - $statusMove = $repo->storeBatch( $triplets ); + $this->file->lock(); // begin + // Rename the file versions metadata in the DB. + // This implicitly locks the destination file, which avoids race conditions. + // If we moved the files from A -> C before DB updates, another process could + // move files from B -> C at this point, causing storeBatch() to fail and thus + // cleanupTarget() to trigger. It would delete the C files and cause data loss. + $statusDb = $this->doDBUpdates(); + if ( !$statusDb->isGood() ) { + $this->file->unlockAndRollback(); + $statusDb->ok = false; + return $statusDb; + } + wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" ); + + // Copy the files into their new location. + // If a prior process fataled copying or cleaning up files we tolerate any + // of the existing files if they are identical to the ones being stored. + $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME ); wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" ); if ( !$statusMove->isGood() ) { - wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); + // Delete any files copied over (while the destination is still locked) $this->cleanupTarget( $triplets ); + $this->file->unlockAndRollback(); // unlocks the destination + wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); $statusMove->ok = false; return $statusMove; } - - $this->db->begin(); - $statusDb = $this->doDBUpdates(); - wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" ); - if ( !$statusDb->isGood() ) { - $this->db->rollback(); - // Something went wrong with the DB updates, so remove the target files - $this->cleanupTarget( $triplets ); - $statusDb->ok = false; - return $statusDb; - } - $this->db->commit(); + $this->file->unlock(); // done // Everything went ok, remove the source files $this->cleanupSource( $triplets ); @@ -2256,7 +2465,8 @@ class LocalFileMoveBatch { 'oldimage', array( 'oi_name' => $this->newName, - 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ), + 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', + $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ), ), array( 'oi_name' => $this->oldName ), __METHOD__ @@ -2265,7 +2475,10 @@ class LocalFileMoveBatch { $affected = $dbw->affectedRows(); $total = $this->oldCount; $status->successCount += $affected; - $status->failCount += $total - $affected; + // Bug 34934: $total is based on files that actually exist. + // There may be more DB rows than such files, in which case $affected + // can be greater than $total. We use max() to avoid negatives here. + $status->failCount += max( 0, $total - $affected ); if ( $status->failCount ) { $status->error( 'imageinvalidfilename' ); } @@ -2275,6 +2488,7 @@ class LocalFileMoveBatch { /** * Generate triplets for FileRepo::storeBatch(). + * @return array */ function getMoveTriplets() { $moves = array_merge( array( $this->cur ), $this->olds ); @@ -2292,6 +2506,8 @@ class LocalFileMoveBatch { /** * Removes non-existent files from move batch. + * @param $triplets array + * @return array */ function removeNonexistentFiles( $triplets ) { $files = array(); @@ -2300,7 +2516,7 @@ class LocalFileMoveBatch { $files[$file[0]] = $file[0]; } - $result = $this->file->repo->fileExistsBatch( $files, FileRepo::FILES_ONLY ); + $result = $this->file->repo->fileExistsBatch( $files ); $filteredTriplets = array(); foreach ( $triplets as $file ) { @@ -2322,6 +2538,7 @@ class LocalFileMoveBatch { // Create dest pairs from the triplets $pairs = array(); foreach ( $triplets as $triplet ) { + // $triplet: (old source virtual URL, dst zone, dest rel) $pairs[] = array( $triplet[1], $triplet[2] ); } diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php index ebd83c4d..40d7dca7 100644 --- a/includes/filerepo/file/OldLocalFile.php +++ b/includes/filerepo/file/OldLocalFile.php @@ -1,6 +1,21 @@ <?php /** - * Old file in the oldimage table + * Old file in the oldimage table. + * + * 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 FileAbstraction @@ -17,6 +32,13 @@ class OldLocalFile extends LocalFile { const CACHE_VERSION = 1; const MAX_CACHE_ROWS = 20; + /** + * @param $title Title + * @param $repo FileRepo + * @param $time null + * @return OldLocalFile + * @throws MWException + */ static function newFromTitle( $title, $repo, $time = null ) { # The null default value is only here to avoid an E_STRICT if ( $time === null ) { @@ -25,10 +47,21 @@ class OldLocalFile extends LocalFile { return new self( $title, $repo, $time, null ); } + /** + * @param $title Title + * @param $repo FileRepo + * @param $archiveName + * @return OldLocalFile + */ static function newFromArchiveName( $title, $repo, $archiveName ) { return new self( $title, $repo, null, $archiveName ); } + /** + * @param $row + * @param $repo FileRepo + * @return OldLocalFile + */ static function newFromRow( $row, $repo ) { $title = Title::makeTitle( NS_FILE, $row->oi_name ); $file = new self( $title, $repo, null, $row->oi_archive_name ); @@ -61,9 +94,10 @@ class OldLocalFile extends LocalFile { return false; } } - + /** * Fields in the oldimage table + * @return array */ static function selectFields() { return array( @@ -91,6 +125,7 @@ class OldLocalFile extends LocalFile { * @param $repo FileRepo * @param $time String: timestamp or null to load by archive name * @param $archiveName String: archive name or null to load by timestamp + * @throws MWException */ function __construct( $title, $repo, $time, $archiveName ) { parent::__construct( $title, $repo ); @@ -101,10 +136,16 @@ class OldLocalFile extends LocalFile { } } + /** + * @return bool + */ function getCacheKey() { return false; } + /** + * @return String + */ function getArchiveName() { if ( !isset( $this->archive_name ) ) { $this->load(); @@ -112,10 +153,16 @@ class OldLocalFile extends LocalFile { return $this->archive_name; } + /** + * @return bool + */ function isOld() { return true; } + /** + * @return bool + */ function isVisible() { return $this->exists() && !$this->isDeleted(File::DELETED_FILE); } @@ -140,6 +187,10 @@ class OldLocalFile extends LocalFile { wfProfileOut( __METHOD__ ); } + /** + * @param $prefix string + * @return array + */ function getCacheFields( $prefix = 'img_' ) { $fields = parent::getCacheFields( $prefix ); $fields[] = $prefix . 'archive_name'; @@ -147,10 +198,16 @@ class OldLocalFile extends LocalFile { return $fields; } + /** + * @return string + */ function getRel() { return 'archive/' . $this->getHashPath() . $this->getArchiveName(); } + /** + * @return string + */ function getUrlRel() { return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() ); } @@ -172,14 +229,15 @@ class OldLocalFile extends LocalFile { wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n"); $dbw->update( 'oldimage', array( - 'oi_width' => $this->width, - 'oi_height' => $this->height, - 'oi_bits' => $this->bits, + 'oi_size' => $this->size, // sanity + 'oi_width' => $this->width, + 'oi_height' => $this->height, + 'oi_bits' => $this->bits, 'oi_media_type' => $this->media_type, 'oi_major_mime' => $major, 'oi_minor_mime' => $minor, - 'oi_metadata' => $this->metadata, - 'oi_sha1' => $this->sha1, + 'oi_metadata' => $this->metadata, + 'oi_sha1' => $this->sha1, ), array( 'oi_name' => $this->getName(), 'oi_archive_name' => $this->archive_name ), @@ -219,47 +277,52 @@ class OldLocalFile extends LocalFile { $this->load(); return Revision::userCanBitfield( $this->deleted, $field, $user ); } - + /** * Upload a file directly into archive. Generally for Special:Import. - * + * * @param $srcPath string File system path of the source file - * @param $archiveName string Full archive name of the file, in the form - * $timestamp!$filename, where $filename must match $this->getName() + * @param $archiveName string Full archive name of the file, in the form + * $timestamp!$filename, where $filename must match $this->getName() * + * @param $timestamp string + * @param $comment string + * @param $user + * @param $flags int * @return FileRepoStatus */ function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) { $this->lock(); - + $dstRel = 'archive/' . $this->getHashPath() . $archiveName; $status = $this->publishTo( $srcPath, $dstRel, $flags & File::DELETE_SOURCE ? FileRepo::DELETE_SOURCE : 0 ); - + if ( $status->isGood() ) { if ( !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) ) { $status->fatal( 'filenotfound', $srcPath ); } } - + $this->unlock(); - + return $status; } - + /** * Record a file upload in the oldimage table, without adding log entries. - * + * * @param $srcPath string File system path to the source file * @param $archiveName string The archive name of the file + * @param $timestamp string * @param $comment string Upload comment * @param $user User User who did this upload * @return bool */ function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) { $dbw = $this->repo->getMasterDB(); - $dbw->begin(); + $dbw->begin( __METHOD__ ); $dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel(); $props = $this->repo->getFileProps( $dstPath ); @@ -287,9 +350,9 @@ class OldLocalFile extends LocalFile { ), __METHOD__ ); - $dbw->commit(); + $dbw->commit( __METHOD__ ); return true; } - + } diff --git a/includes/filerepo/file/UnregisteredLocalFile.php b/includes/filerepo/file/UnregisteredLocalFile.php index cd9d3d02..8d4a3f88 100644 --- a/includes/filerepo/file/UnregisteredLocalFile.php +++ b/includes/filerepo/file/UnregisteredLocalFile.php @@ -1,6 +1,21 @@ <?php /** - * File without associated database record + * File without associated database record. + * + * 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 FileAbstraction @@ -19,7 +34,7 @@ * @ingroup FileAbstraction */ class UnregisteredLocalFile extends File { - var $title, $path, $mime, $dims; + var $title, $path, $mime, $dims, $metadata; /** * @var MediaHandler @@ -47,12 +62,12 @@ class UnregisteredLocalFile extends File { /** * Create an UnregisteredLocalFile based on a path or a (title,repo) pair. * A FileRepo object is not required here, unlike most other File classes. - * + * * @throws MWException - * @param $title Title|false - * @param $repo FileRepo - * @param $path string - * @param $mime string + * @param $title Title|bool + * @param $repo FileRepo|bool + * @param $path string|bool + * @param $mime string|bool */ function __construct( $title = false, $repo = false, $path = false, $mime = false ) { if ( !( $title && $repo ) && !$path ) { @@ -79,6 +94,10 @@ class UnregisteredLocalFile extends File { $this->dims = array(); } + /** + * @param $page int + * @return bool + */ private function cachePageDimensions( $page = 1 ) { if ( !isset( $this->dims[$page] ) ) { if ( !$this->getHandler() ) { @@ -89,16 +108,27 @@ class UnregisteredLocalFile extends File { return $this->dims[$page]; } + /** + * @param $page int + * @return number + */ function getWidth( $page = 1 ) { $dim = $this->cachePageDimensions( $page ); return $dim['width']; } + /** + * @param $page int + * @return number + */ function getHeight( $page = 1 ) { $dim = $this->cachePageDimensions( $page ); return $dim['height']; } + /** + * @return bool|string + */ function getMimeType() { if ( !isset( $this->mime ) ) { $magic = MimeMagic::singleton(); @@ -107,6 +137,10 @@ class UnregisteredLocalFile extends File { return $this->mime; } + /** + * @param $filename String + * @return Array|bool + */ function getImageSize( $filename ) { if ( !$this->getHandler() ) { return false; @@ -114,6 +148,9 @@ class UnregisteredLocalFile extends File { return $this->handler->getImageSize( $this, $this->getLocalRefPath() ); } + /** + * @return bool + */ function getMetadata() { if ( !isset( $this->metadata ) ) { if ( !$this->getHandler() ) { @@ -125,6 +162,9 @@ class UnregisteredLocalFile extends File { return $this->metadata; } + /** + * @return bool|string + */ function getURL() { if ( $this->repo ) { return $this->repo->getZoneUrl( 'public' ) . '/' . @@ -134,6 +174,9 @@ class UnregisteredLocalFile extends File { } } + /** + * @return bool|int + */ function getSize() { $this->assertRepoDefined(); $props = $this->repo->getFileProps( $this->path ); diff --git a/includes/installer/CliInstaller.php b/includes/installer/CliInstaller.php index f9afbb20..38b4a824 100644 --- a/includes/installer/CliInstaller.php +++ b/includes/installer/CliInstaller.php @@ -2,6 +2,21 @@ /** * Core installer command line interface. * + * 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 Deployment */ @@ -117,7 +132,7 @@ class CliInstaller extends Installer { * @param $path String Full path to write LocalSettings.php to */ public function writeConfigurationFile( $path ) { - $ls = new LocalSettingsGenerator( $this ); + $ls = InstallerOverrides::getLocalSettingsGenerator( $this ); $ls->writeFile( "$path/LocalSettings.php" ); } @@ -148,7 +163,7 @@ class CliInstaller extends Installer { protected function getMessageText( $params ) { $msg = array_shift( $params ); - $text = wfMsgExt( $msg, array( 'parseinline' ), $params ); + $text = wfMessage( $msg, $params )->parse(); $text = preg_replace( '/<a href="(.*?)".*?>(.*?)<\/a>/', '$2 <$1>', $text ); return html_entity_decode( strip_tags( $text ), ENT_QUOTES ); @@ -172,7 +187,7 @@ class CliInstaller extends Installer { if ( !$status->isOk() ) { echo "\n"; - exit; + exit( 1 ); } } diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php index ab77e2d3..de59b2d6 100644 --- a/includes/installer/DatabaseInstaller.php +++ b/includes/installer/DatabaseInstaller.php @@ -2,6 +2,21 @@ /** * DBMS-specific installation helper. * + * 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 Deployment */ @@ -50,7 +65,7 @@ abstract class DatabaseInstaller { public abstract function getName(); /** - * @return true if the client library is compiled in. + * @return bool Returns true if the client library is compiled in. */ public abstract function isCompiled(); @@ -88,6 +103,7 @@ abstract class DatabaseInstaller { * $this->parent can be assumed to be a WebInstaller. * If the DB type has no settings beyond those already configured with * getConnectForm(), this should return false. + * @return bool */ public function getSettingsForm() { return false; @@ -140,7 +156,7 @@ abstract class DatabaseInstaller { $this->db = $status->value; // Enable autocommit $this->db->clearFlag( DBO_TRX ); - $this->db->commit(); + $this->db->commit( __METHOD__ ); } return $status; } @@ -207,6 +223,7 @@ abstract class DatabaseInstaller { /** * Override this to provide DBMS-specific schema variables, to be * substituted into tables.sql and other schema files. + * @return array */ public function getSchemaVars() { return array(); @@ -256,7 +273,7 @@ abstract class DatabaseInstaller { $up = DatabaseUpdater::newForDB( $this->db ); $up->doUpdates(); } catch ( MWException $e ) { - echo "\nAn error occured:\n"; + echo "\nAn error occurred:\n"; echo $e->getText(); $ret = false; } @@ -282,6 +299,7 @@ abstract class DatabaseInstaller { /** * Get an array of MW configuration globals that will be configured by this class. + * @return array */ public function getGlobalNames() { return $this->globalNames; @@ -313,14 +331,16 @@ abstract class DatabaseInstaller { /** * Get the internationalised name for this DBMS. + * @return String */ public function getReadableName() { - return wfMsg( 'config-type-' . $this->getName() ); + return wfMessage( 'config-type-' . $this->getName() )->text(); } /** * Get a name=>value map of MW configuration globals that overrides. * DefaultSettings.php + * @return array */ public function getGlobalDefaults() { return array(); @@ -328,6 +348,7 @@ abstract class DatabaseInstaller { /** * Get a name=>value map of internal variables used during installation. + * @return array */ public function getInternalDefaults() { return $this->internalDefaults; @@ -439,6 +460,7 @@ abstract class DatabaseInstaller { * values: List of allowed values (required) * itemAttribs Array of attribute arrays, outer key is the value name (optional) * + * @return string */ public function getRadioSet( $params ) { $params['controlName'] = $this->getName() . '_' . $params['var']; @@ -451,6 +473,7 @@ abstract class DatabaseInstaller { * Assumes that variables containing "password" in the name are (potentially * fake) passwords. * @param $varNames Array + * @return array */ public function setVarsFromRequest( $varNames ) { return $this->parent->setVarsFromRequest( $varNames, $this->getName() . '_' ); @@ -486,7 +509,7 @@ abstract class DatabaseInstaller { public function getInstallUserBox() { return Html::openElement( 'fieldset' ) . - Html::element( 'legend', array(), wfMsg( 'config-db-install-account' ) ) . + Html::element( 'legend', array(), wfMessage( 'config-db-install-account' )->text() ) . $this->getTextBox( '_InstallUser', 'config-db-username', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-install-username' ) ) . $this->getPasswordBox( '_InstallPassword', 'config-db-password', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-install-password' ) ) . Html::closeElement( 'fieldset' ); @@ -494,6 +517,7 @@ abstract class DatabaseInstaller { /** * Submit a standard install user fieldset. + * @return Status */ public function submitInstallUserBox() { $this->setVarsFromRequest( array( '_InstallUser', '_InstallPassword' ) ); @@ -510,7 +534,7 @@ abstract class DatabaseInstaller { public function getWebUserBox( $noCreateMsg = false ) { $wrapperStyle = $this->getVar( '_SameAccount' ) ? 'display: none' : ''; $s = Html::openElement( 'fieldset' ) . - Html::element( 'legend', array(), wfMsg( 'config-db-web-account' ) ) . + Html::element( 'legend', array(), wfMessage( 'config-db-web-account' )->text() ) . $this->getCheckBox( '_SameAccount', 'config-db-web-account-same', array( 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' ) @@ -520,7 +544,7 @@ abstract class DatabaseInstaller { $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) . $this->parent->getHelpBox( 'config-db-web-help' ); if ( $noCreateMsg ) { - $s .= $this->parent->getWarningBox( wfMsgNoTrans( $noCreateMsg ) ); + $s .= $this->parent->getWarningBox( wfMessage( $noCreateMsg )->plain() ); } else { $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' ); } diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php index f2e36aec..ff0a99e9 100644 --- a/includes/installer/DatabaseUpdater.php +++ b/includes/installer/DatabaseUpdater.php @@ -2,11 +2,26 @@ /** * DBMS-specific updater helper. * + * 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 Deployment */ -require_once( dirname(__FILE__) . '/../../maintenance/Maintenance.php' ); +require_once( __DIR__ . '/../../maintenance/Maintenance.php' ); /** * Class for handling database updates. Roughly based off of updaters.inc, with @@ -203,10 +218,44 @@ abstract class DatabaseUpdater { } /** + * + * @since 1.20 + * + * @param $tableName string + * @param $columnName string + * @param $sqlPath string + */ + public function dropExtensionField( $tableName, $columnName, $sqlPath ) { + $this->extensionUpdates[] = array( 'dropField', $tableName, $columnName, $sqlPath, true ); + } + + /** + * + * @since 1.20 + * + * @param $tableName string + * @param $sqlPath string + */ + public function dropExtensionTable( $tableName, $sqlPath ) { + $this->extensionUpdates[] = array( 'dropTable', $tableName, $sqlPath, true ); + } + + /** + * + * @since 1.20 + * + * @param $tableName string + * @return bool + */ + public function tableExists( $tableName ) { + return ( $this->db->tableExists( $tableName, __METHOD__ ) ); + } + + /** * Add a maintenance script to be run after the database updates are complete. - * + * * @since 1.19 - * + * * @param $class string Name of a Maintenance subclass */ public function addPostDatabaseUpdateMaintenance( $class ) { @@ -224,7 +273,7 @@ abstract class DatabaseUpdater { /** * @since 1.17 - * + * * @return array */ public function getPostDatabaseUpdateMaintenance() { @@ -239,6 +288,7 @@ abstract class DatabaseUpdater { public function doUpdates( $what = array( 'core', 'extensions', 'purge', 'stats' ) ) { global $wgLocalisationCacheConf, $wgVersion; + $this->db->begin( __METHOD__ ); $what = array_flip( $what ); if ( isset( $what['core'] ) ) { $this->runUpdates( $this->getCoreUpdateList(), false ); @@ -261,6 +311,7 @@ abstract class DatabaseUpdater { $this->rebuildLocalisationCache(); } } + $this->db->commit( __METHOD__ ); } /** @@ -412,13 +463,20 @@ abstract class DatabaseUpdater { * Applies a SQL patch * @param $path String Path to the patch file * @param $isFullPath Boolean Whether to treat $path as a relative or not + * @param $msg String Description of the patch */ - protected function applyPatch( $path, $isFullPath = false ) { - if ( $isFullPath ) { - $this->db->sourceFile( $path ); - } else { - $this->db->sourceFile( $this->db->patchPath( $path ) ); + protected function applyPatch( $path, $isFullPath = false, $msg = null ) { + if ( $msg === null ) { + $msg = "Applying $path patch"; } + + if ( !$isFullPath ) { + $path = $this->db->patchPath( $path ); + } + + $this->output( "$msg ..." ); + $this->db->sourceFile( $path ); + $this->output( "done.\n" ); } /** @@ -431,9 +489,7 @@ abstract class DatabaseUpdater { if ( $this->db->tableExists( $name, __METHOD__ ) ) { $this->output( "...$name table already exists.\n" ); } else { - $this->output( "Creating $name table..." ); - $this->applyPatch( $patch, $fullpath ); - $this->output( "done.\n" ); + $this->applyPatch( $patch, $fullpath, "Creating $name table" ); } } @@ -450,9 +506,7 @@ abstract class DatabaseUpdater { } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { $this->output( "...have $field field in $table table.\n" ); } else { - $this->output( "Adding $field field to table $table..." ); - $this->applyPatch( $patch, $fullpath ); - $this->output( "done.\n" ); + $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" ); } } @@ -467,9 +521,7 @@ abstract class DatabaseUpdater { if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { $this->output( "...index $index already set on $table table.\n" ); } else { - $this->output( "Adding index $index to table $table... " ); - $this->applyPatch( $patch, $fullpath ); - $this->output( "done.\n" ); + $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" ); } } @@ -483,9 +535,7 @@ abstract class DatabaseUpdater { */ protected function dropField( $table, $field, $patch, $fullpath = false ) { if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { - $this->output( "Table $table contains $field field. Dropping... " ); - $this->applyPatch( $patch, $fullpath ); - $this->output( "done.\n" ); + $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" ); } else { $this->output( "...$table table does not contain $field field.\n" ); } @@ -501,24 +551,35 @@ abstract class DatabaseUpdater { */ protected function dropIndex( $table, $index, $patch, $fullpath = false ) { if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { - $this->output( "Dropping $index index from table $table... " ); - $this->applyPatch( $patch, $fullpath ); - $this->output( "done.\n" ); + $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" ); } else { $this->output( "...$index key doesn't exist.\n" ); } } /** + * If the specified table exists, drop it, or execute the + * patch if one is provided. + * + * Public @since 1.20 + * * @param $table string - * @param $patch string + * @param $patch string|false * @param $fullpath bool */ - protected function dropTable( $table, $patch, $fullpath = false ) { + public function dropTable( $table, $patch = false, $fullpath = false ) { if ( $this->db->tableExists( $table, __METHOD__ ) ) { - $this->output( "Dropping table $table... " ); - $this->applyPatch( $patch, $fullpath ); - $this->output( "done.\n" ); + $msg = "Dropping table $table"; + + if ( $patch === false ) { + $this->output( "$msg ..." ); + $this->db->dropTable( $table, __METHOD__ ); + $this->output( "done.\n" ); + } + else { + $this->applyPatch( $patch, $fullpath, $msg ); + } + } else { $this->output( "...$table doesn't exist.\n" ); } @@ -541,10 +602,8 @@ abstract class DatabaseUpdater { } elseif( $this->updateRowExists( $updateKey ) ) { $this->output( "...$field in table $table already modified by patch $patch.\n" ); } else { - $this->output( "Modifying $field field of table $table..." ); - $this->applyPatch( $patch, $fullpath ); + $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" ); $this->insertUpdateRow( $updateKey ); - $this->output( "done.\n" ); } } @@ -637,9 +696,7 @@ abstract class DatabaseUpdater { return; } - $this->output( "Converting tc_time from UNIX epoch to MediaWiki timestamp... " ); - $this->applyPatch( 'patch-tc-timestamp.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-tc-timestamp.sql', false, "Converting tc_time from UNIX epoch to MediaWiki timestamp" ); } /** diff --git a/includes/installer/Ibm_db2Installer.php b/includes/installer/Ibm_db2Installer.php index a6c4fd65..ca9bdf4b 100644 --- a/includes/installer/Ibm_db2Installer.php +++ b/includes/installer/Ibm_db2Installer.php @@ -2,6 +2,21 @@ /** * IBM_DB2-specific installer. * + * 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 Deployment */ @@ -53,7 +68,7 @@ class Ibm_db2Installer extends DatabaseInstaller { $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) . $this->getTextBox( 'wgDBport', 'config-db-port', array(), $this->parent->getHelpBox( 'config-db-port' ) ) . Html::openElement( 'fieldset' ) . - Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) . + Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) . $this->getTextBox( 'wgDBname', 'config-db-name', array(), $this->parent->getHelpBox( 'config-db-name-help' ) ) . $this->getTextBox( 'wgDBmwschema', 'config-db-schema', array(), $this->parent->getHelpBox( 'config-db-schema-help' ) ) . Html::closeElement( 'fieldset' ) . diff --git a/includes/installer/Ibm_db2Updater.php b/includes/installer/Ibm_db2Updater.php index 03540bb0..9daba9c2 100644 --- a/includes/installer/Ibm_db2Updater.php +++ b/includes/installer/Ibm_db2Updater.php @@ -2,6 +2,21 @@ /** * IBM_DB2-specific updater. * + * 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 Deployment */ @@ -55,20 +70,22 @@ class Ibm_db2Updater extends DatabaseUpdater { array( 'addField', 'categorylinks', 'cl_sortkey_prefix', 'patch-cl_sortkey_prefix-field.sql' ), array( 'addField', 'categorylinks', 'cl_collation', 'patch-cl_collation-field.sql' ), array( 'addField', 'categorylinks', 'cl_type', 'patch-cl_type-field.sql' ), - + //1.18 array( 'doUserNewTalkTimestampNotNull' ), array( 'addIndex', 'user', 'user_email', 'patch-user_email_index.sql' ), array( 'modifyField', 'user_properties', 'up_property', 'patch-up_property.sql' ), array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ), array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql'), - array( 'doRebuildLocalisationCache' ), - + array( 'doRebuildLocalisationCache' ), + // 1.19 array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql'), array( 'dropField', 'user', 'user_options', 'patch-drop-user_options.sql' ), array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1.sql' ), - array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ) + array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ), + + // 1.20 ); } } diff --git a/includes/installer/InstallDocFormatter.php b/includes/installer/InstallDocFormatter.php index 5801f26d..9a389dd8 100644 --- a/includes/installer/InstallDocFormatter.php +++ b/includes/installer/InstallDocFormatter.php @@ -1,4 +1,24 @@ <?php +/** + * Installer-specific wikitext formating. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ class InstallDocFormatter { static function format( $text ) { diff --git a/includes/installer/Installer.i18n.php b/includes/installer/Installer.i18n.php index fb68a2b5..4f1c4d0c 100644 --- a/includes/installer/Installer.i18n.php +++ b/includes/installer/Installer.i18n.php @@ -550,6 +550,7 @@ $3 When that has been done, you can '''[$2 enter your wiki]'''.", 'config-download-localsettings' => 'Download LocalSettings.php', 'config-help' => 'help', + 'config-nofile' => 'File "$1" could not be found. Has it been deleted?', 'mainpagetext' => "'''MediaWiki has been successfully installed.'''", 'mainpagedocfooter' => "Consult the [//meta.wikimedia.org/wiki/Help:Contents User's Guide] for information on using the wiki software. @@ -4484,7 +4485,7 @@ Esta no es la contraseña para la cuenta de MediaWiki; esta es la contraseña pa 'config-db-wiki-help' => 'Introduce el nombre de usuario y la contraseña que serán usados para acceder a la base de datos durante la operación normal del wiki. Si esta cuenta no existe y la cuenta de instalación tiene suficientes privilegios, se creará esta cuenta de usuario con los privilegios mínimos necesarios para la operación normal del wiki.', 'config-db-prefix' => 'Prefijo para las tablas de la base de datos:', - 'config-db-prefix-help' => 'Si necesita compartir una base de datos entre múltiples wikis, o entre MediaWiki y otra aplicación web, puede optar por agregar un prefijo a todos los nombres de tabla para evitar conflictos. + 'config-db-prefix-help' => 'Si necesita compartir una base de datos entre múltiples wikis, o entre MediaWiki y otra aplicación web, puede optar por agregar un prefijo a todos los nombres de tabla para evitar conflictos. No utilice espacios. Normalmente se deja este campo vacío.', @@ -14228,7 +14229,7 @@ Jeśli korzystasz ze współdzielonego hostingu, dostawca usługi hostingowej mo Możesz utworzyć konto użytkownika bazy danych podczas instalacji MediaWiki. Wówczas należy podać nazwę i hasło użytkownika z rolą SYSDBA w celu użycia go przez instalator do utworzenia nowe konta użytkownika, z którego korzystać będzie MediaWiki. -Możesz również skorzystać z konta użytkownika bazy danych utworzonego bezpośrednio w Oracle i wówczas wystarczy podać tylko nazwę i hasło tego użytkownika. Konto z rolą SYSDBA nie będzie potrzebne, jednak konto użytkownika powinno mieć uprawnienia do utworzenia obiektów w schemacie bazy danych. Możesz też podać dwa konta - konto dla instalatora, z pomocą którego zostaną obiekty w schemacie bazy danych i drugie konto, z którego będzie MediaWiki korzystać będzie do pracy. +Możesz również skorzystać z konta użytkownika bazy danych utworzonego bezpośrednio w Oracle i wówczas wystarczy podać tylko nazwę i hasło tego użytkownika. Konto z rolą SYSDBA nie będzie potrzebne, jednak konto użytkownika powinno mieć uprawnienia do utworzenia obiektów w schemacie bazy danych. Możesz też podać dwa konta - konto dla instalatora, z pomocą którego zostaną obiekty w schemacie bazy danych i drugie konto, z którego będzie MediaWiki korzystać będzie do pracy. W podkatalogu "maintenance/oracle" znajduje się skrypt do tworzenia konta użytkownika. Korzystanie z konta użytkownika z ograniczonymi uprawnieniami spowoduje wyłączenie funkcji związanych z aktualizacją oprogramowania MediaWiki.', 'config-db-install-account' => 'Konto użytkownika dla instalatora', @@ -17765,7 +17766,7 @@ Ang mas masasalimuot na mga kaayusan ng mga karapatan ng tagagamit ay makukuha p 'config-license-gfdl' => 'Lisensiyang 1.3 ng Malayang Dokumentasyon ng GNU o mas lalong huli', 'config-license-pd' => 'Nasasakupan ng Madla', 'config-license-cc-choose' => 'Pumili ng isang pasadyang Lisensiya ng Malikhaing mga Pangkaraniwan', - 'config-license-help' => "Maraming mga pangmadlang wiki ang naglalagay ng lahat ng mga ambag sa ilalim ng [http://freedomdefined.org/Definition lisensiyang malaya]. + 'config-license-help' => "Maraming mga pangmadlang wiki ang naglalagay ng lahat ng mga ambag sa ilalim ng [http://freedomdefined.org/Definition lisensiyang malaya]. Nakakatulong ito sa paglikha ng isang diwa ng pagmamay-ari ng pamayanan at nakapanghihikayat ng ambag na pangmahabang panahon. Sa pangkalahatan, hindi kailangan ang isang wiking pribado o pangsamahan. diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index 990d4449..ac5dbd74 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -2,6 +2,21 @@ /** * Base code for MediaWiki installer. * + * 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 Deployment */ @@ -24,7 +39,7 @@ abstract class Installer { // This is the absolute minimum PHP version we can support - const MINIMUM_PHP_VERSION = '5.2.3'; + const MINIMUM_PHP_VERSION = '5.3.2'; /** * @var array @@ -293,7 +308,7 @@ abstract class Installer { /** * UI interface for displaying a short message - * The parameters are like parameters to wfMsg(). + * The parameters are like parameters to wfMessage(). * The messages will be in wikitext format, which will be converted to an * output format such as HTML or text before being sent to the user. * @param $msg @@ -324,7 +339,7 @@ abstract class Installer { // Load the installer's i18n file. $wgExtensionMessagesFiles['MediawikiInstaller'] = - dirname( __FILE__ ) . '/Installer.i18n.php'; + __DIR__ . '/Installer.i18n.php'; // Having a user with id = 0 safeguards us from DB access via User::loadOptions(). $wgUser = User::newFromId( 0 ); @@ -543,7 +558,7 @@ abstract class Installer { * write your messages. This appears to work well enough. Basic formatting and * external links work just fine. * - * But in case a translator decides to throw in a #ifexist or internal link or + * But in case a translator decides to throw in a "#ifexist" or internal link or * whatever, this function is guarded to catch the attempted DB access and to present * some fallback text. * @@ -630,7 +645,7 @@ abstract class Installer { $allNames = array(); foreach ( self::getDBTypes() as $name ) { - $allNames[] = wfMsg( "config-type-$name" ); + $allNames[] = wfMessage( "config-type-$name" )->text(); } // cache initially available databases to make sure that everything will be displayed correctly @@ -659,6 +674,7 @@ abstract class Installer { return false; } $this->setVar( '_CompiledDBs', $databases ); + return true; } /** @@ -672,6 +688,7 @@ abstract class Installer { /** * Some versions of libxml+PHP break < and > encoding horribly + * @return bool */ protected function envCheckBrokenXML() { $test = new PhpXmlBugTester(); @@ -679,11 +696,13 @@ abstract class Installer { $this->showError( 'config-brokenlibxml' ); return false; } + return true; } /** * Test PHP (probably 5.3.1, but it could regress again) to make sure that * reference parameters to __call() are not converted to null + * @return bool */ protected function envCheckPHP531() { $test = new PhpRefCallBugTester; @@ -692,66 +711,79 @@ abstract class Installer { $this->showError( 'config-using531', phpversion() ); return false; } + return true; } /** * Environment check for magic_quotes_runtime. + * @return bool */ protected function envCheckMagicQuotes() { if( wfIniGetBool( "magic_quotes_runtime" ) ) { $this->showError( 'config-magic-quotes-runtime' ); return false; } + return true; } /** * Environment check for magic_quotes_sybase. + * @return bool */ protected function envCheckMagicSybase() { if ( wfIniGetBool( 'magic_quotes_sybase' ) ) { $this->showError( 'config-magic-quotes-sybase' ); return false; } + return true; } /** * Environment check for mbstring.func_overload. + * @return bool */ protected function envCheckMbstring() { if ( wfIniGetBool( 'mbstring.func_overload' ) ) { $this->showError( 'config-mbstring' ); return false; } + return true; } /** * Environment check for zend.ze1_compatibility_mode. + * @return bool */ protected function envCheckZE1() { if ( wfIniGetBool( 'zend.ze1_compatibility_mode' ) ) { $this->showError( 'config-ze1' ); return false; } + return true; } /** * Environment check for safe_mode. + * @return bool */ protected function envCheckSafeMode() { if ( wfIniGetBool( 'safe_mode' ) ) { $this->setVar( '_SafeMode', true ); $this->showMessage( 'config-safe-mode' ); } + return true; } /** * Environment check for the XML module. + * @return bool */ protected function envCheckXML() { if ( !function_exists( "utf8_encode" ) ) { $this->showError( 'config-xml-bad' ); return false; } + return true; } /** @@ -779,10 +811,12 @@ abstract class Installer { $this->showError( 'config-pcre-no-utf8' ); return false; } + return true; } /** * Environment check for available memory. + * @return bool */ protected function envCheckMemory() { $limit = ini_get( 'memory_limit' ); @@ -802,9 +836,8 @@ abstract class Installer { $this->showMessage( 'config-memory-raised', $limit, $newLimit ); $this->setVar( '_RaiseMemory', true ); } - } else { - return true; } + return true; } /** @@ -830,15 +863,18 @@ abstract class Installer { /** * Scare user to death if they have mod_security + * @return bool */ protected function envCheckModSecurity() { if ( self::apacheModulePresent( 'mod_security' ) ) { $this->showMessage( 'config-mod-security' ); } + return true; } /** * Search for GNU diff3. + * @return bool */ protected function envCheckDiff3() { $names = array( "gdiff3", "diff3", "diff3.exe" ); @@ -852,10 +888,12 @@ abstract class Installer { $this->setVar( 'wgDiff3', false ); $this->showMessage( 'config-diff3-bad' ); } + return true; } /** * Environment check for ImageMagick and GD. + * @return bool */ protected function envCheckGraphics() { $names = array( wfIsWindows() ? 'convert.exe' : 'convert' ); @@ -868,10 +906,11 @@ abstract class Installer { return true; } elseif ( function_exists( 'imagejpeg' ) ) { $this->showMessage( 'config-gd' ); - return true; + } else { $this->showMessage( 'config-no-scaling' ); } + return true; } /** @@ -881,6 +920,7 @@ abstract class Installer { $server = $this->envGetDefaultServer(); $this->showMessage( 'config-using-server', $server ); $this->setVar( 'wgServer', $server ); + return true; } /** @@ -895,7 +935,7 @@ abstract class Installer { */ protected function envCheckPath() { global $IP; - $IP = dirname( dirname( dirname( __FILE__ ) ) ); + $IP = dirname( dirname( __DIR__ ) ); $this->setVar( 'IP', $IP ); $this->showMessage( 'config-using-uri', $this->getVar( 'wgServer' ), $this->getVar( 'wgScriptPath' ) ); @@ -913,6 +953,7 @@ abstract class Installer { $ext = 'php'; } $this->setVar( 'wgScriptExtension', ".$ext" ); + return true; } /** @@ -991,6 +1032,7 @@ abstract class Installer { /** * TODO: document + * @return bool */ protected function envCheckUploadsDirectory() { global $IP; @@ -999,17 +1041,17 @@ abstract class Installer { $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/'; $safe = !$this->dirIsExecutable( $dir, $url ); - if ( $safe ) { - return true; - } else { + if ( !$safe ) { $this->showMessage( 'config-uploads-not-safe', $dir ); } + return true; } /** * Checks if suhosin.get.max_value_length is set, and if so, sets * $wgResourceLoaderMaxQueryLength to that value in the generated * LocalSettings file + * @return bool */ protected function envCheckSuhosinMaxValueLength() { $maxValueLength = ini_get( 'suhosin.get.max_value_length' ); @@ -1022,6 +1064,7 @@ abstract class Installer { $maxValueLength = -1; } $this->setVar( 'wgResourceLoaderMaxQueryLength', $maxValueLength ); + return true; } /** @@ -1075,12 +1118,16 @@ abstract class Installer { if( $utf8 ) { $useNormalizer = 'utf8'; $utf8 = utf8_normalize( $not_normal_c, UtfNormal::UNORM_NFC ); - if ( $utf8 !== $normal_c ) $needsUpdate = true; + if ( $utf8 !== $normal_c ) { + $needsUpdate = true; + } } if( $intl ) { $useNormalizer = 'intl'; $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C ); - if ( $intl !== $normal_c ) $needsUpdate = true; + if ( $intl !== $normal_c ) { + $needsUpdate = true; + } } // Uses messages 'config-unicode-using-php', 'config-unicode-using-utf8', 'config-unicode-using-intl' @@ -1094,11 +1141,15 @@ abstract class Installer { } } + /** + * @return bool + */ protected function envCheckCtype() { if ( !function_exists( 'ctype_digit' ) ) { $this->showError( 'config-ctype' ); return false; } + return true; } /** @@ -1131,6 +1182,7 @@ abstract class Installer { * * If $versionInfo is not false, only executables with a version * matching $versionInfo[1] will be returned. + * @return bool|string */ public static function locateExecutable( $path, $names, $versionInfo = false ) { if ( !is_array( $names ) ) { @@ -1179,6 +1231,9 @@ abstract class Installer { * Checks if scripts located in the given directory can be executed via the given URL. * * Used only by environment checks. + * @param $dir string + * @param $url string + * @return bool|int|string */ public function dirIsExecutable( $dir, $url ) { $scriptTypes = array( @@ -1539,12 +1594,13 @@ abstract class Installer { $status = Status::newGood(); try { $page = WikiPage::factory( Title::newMainPage() ); - $page->doEdit( wfMsgForContent( 'mainpagetext' ) . "\n\n" . - wfMsgForContent( 'mainpagedocfooter' ), - '', - EDIT_NEW, - false, - User::newFromName( 'MediaWiki default' ) ); + $page->doEdit( wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" . + wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text(), + '', + EDIT_NEW, + false, + User::newFromName( 'MediaWiki default' ) + ); } catch (MWException $e) { //using raw, because $wgShowExceptionDetails can not be set yet $status->fatal( 'config-install-mainpage-failed', $e->getMessage() ); @@ -1561,6 +1617,8 @@ abstract class Installer { // Don't access the database $GLOBALS['wgUseDatabaseMessages'] = false; + // Don't cache langconv tables + $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE; // Debug-friendly $GLOBALS['wgShowExceptionDetails'] = true; // Don't break forms diff --git a/includes/installer/LocalSettingsGenerator.php b/includes/installer/LocalSettingsGenerator.php index 89154e58..bbc6b64e 100644 --- a/includes/installer/LocalSettingsGenerator.php +++ b/includes/installer/LocalSettingsGenerator.php @@ -2,6 +2,21 @@ /** * Generator for LocalSettings.php file. * + * 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 Deployment */ @@ -14,16 +29,16 @@ */ class LocalSettingsGenerator { - private $extensions = array(); - private $values = array(); - private $groupPermissions = array(); - private $dbSettings = ''; - private $safeMode = false; + protected $extensions = array(); + protected $values = array(); + protected $groupPermissions = array(); + protected $dbSettings = ''; + protected $safeMode = false; /** * @var Installer */ - private $installer; + protected $installer; /** * Constructor. @@ -151,7 +166,7 @@ class LocalSettingsGenerator { /** * @return String */ - private function buildMemcachedServerList() { + protected function buildMemcachedServerList() { $servers = $this->values['_MemCachedServers']; if( !$servers ) { @@ -172,7 +187,7 @@ class LocalSettingsGenerator { /** * @return String */ - private function getDefaultText() { + protected function getDefaultText() { if( !$this->values['wgImageMagickConvertCommand'] ) { $this->values['wgImageMagickConvertCommand'] = '/usr/bin/convert'; $magic = '#'; @@ -244,7 +259,8 @@ if ( !defined( 'MEDIAWIKI' ) ) { {$metaNamespace} ## The URL base path to the directory containing the wiki; ## defaults for all runtime URL paths are based off of this. -## For more information on customizing the URLs please see: +## For more information on customizing the URLs +## (like /w/index.php/Page_title to /wiki/Page_title) please see: ## http://www.mediawiki.org/wiki/Manual:Short_URL \$wgScriptPath = \"{$this->values['wgScriptPath']}\"; \$wgScriptExtension = \"{$this->values['wgScriptExtension']}\"; diff --git a/includes/installer/MysqlInstaller.php b/includes/installer/MysqlInstaller.php index 7585fe7a..f66f15f2 100644 --- a/includes/installer/MysqlInstaller.php +++ b/includes/installer/MysqlInstaller.php @@ -2,6 +2,21 @@ /** * MySQL-specific installer. * + * 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 Deployment */ @@ -74,7 +89,7 @@ class MysqlInstaller extends DatabaseInstaller { return $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) . Html::openElement( 'fieldset' ) . - Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) . + Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) . $this->getTextBox( 'wgDBname', 'config-db-name', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-name-help' ) ) . $this->getTextBox( 'wgDBprefix', 'config-db-prefix', array( 'dir' => 'ltr' ), $this->parent->getHelpBox( 'config-db-prefix-help' ) ) . Html::closeElement( 'fieldset' ) . @@ -336,7 +351,7 @@ class MysqlInstaller extends DatabaseInstaller { $s .= Xml::openElement( 'div', array( 'id' => 'dbMyisamWarning' )); - $s .= $this->parent->getWarningBox( wfMsg( 'config-mysql-myisam-dep' ) ); + $s .= $this->parent->getWarningBox( wfMessage( 'config-mysql-myisam-dep' )->text() ); $s .= Xml::closeElement( 'div' ); if( $this->getVar( '_MysqlEngine' ) != 'MyISAM' ) { @@ -514,21 +529,21 @@ class MysqlInstaller extends DatabaseInstaller { $fullName = $this->buildFullUserName( $dbUser, $host ); if( !$this->userDefinitelyExists( $dbUser, $host ) ) { try{ - $this->db->begin(); + $this->db->begin( __METHOD__ ); $this->db->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ ); - $this->db->commit(); + $this->db->commit( __METHOD__ ); $grantableNames[] = $fullName; } catch( DBQueryError $dqe ) { if( $this->db->lastErrno() == 1396 /* ER_CANNOT_USER */ ) { // User (probably) already exists - $this->db->rollback(); + $this->db->rollback( __METHOD__ ); $status->warning( 'config-install-user-alreadyexists', $dbUser ); $grantableNames[] = $fullName; break; } else { // If we couldn't create for some bizzare reason and the // user probably doesn't exist, skip the grant - $this->db->rollback(); + $this->db->rollback( __METHOD__ ); $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getText() ); } } @@ -544,11 +559,11 @@ class MysqlInstaller extends DatabaseInstaller { $dbAllTables = $this->db->addIdentifierQuotes( $dbName ) . '.*'; foreach( $grantableNames as $name ) { try { - $this->db->begin(); + $this->db->begin( __METHOD__ ); $this->db->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ ); - $this->db->commit(); + $this->db->commit( __METHOD__ ); } catch( DBQueryError $dqe ) { - $this->db->rollback(); + $this->db->rollback( __METHOD__ ); $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getText() ); } } diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index 9e7869ec..49dff805 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -2,6 +2,21 @@ /** * MySQL-specific updater. * + * 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 Deployment */ @@ -192,6 +207,12 @@ class MysqlUpdater extends DatabaseUpdater { array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ), array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ), array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ), + + // 1.20 + array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ), + array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ), + array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ), + array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ), ); } @@ -211,9 +232,7 @@ class MysqlUpdater extends DatabaseUpdater { if ( in_array( 'binary', $flags ) ) { $this->output( "...$table table has correct $field encoding.\n" ); } else { - $this->output( "Fixing $field encoding on $table table... " ); - $this->applyPatch( $patchFile ); - $this->output( "done.\n" ); + $this->applyPatch( $patchFile, false, "Fixing $field encoding on $table table" ); } } @@ -250,12 +269,8 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( 'Creating interwiki table...' ); - $this->applyPatch( 'patch-interwiki.sql' ); - $this->output( "done.\n" ); - $this->output( 'Adding default interwiki definitions...' ); - $this->applyPatch( "$IP/maintenance/interwiki.sql", true ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-interwiki.sql', false, 'Creating interwiki table' ); + $this->applyPatch( "$IP/maintenance/interwiki.sql", true, 'Adding default interwiki definitions' ); } /** @@ -271,9 +286,7 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Updating indexes to 20031107..." ); - $this->applyPatch( 'patch-indexes.sql', true ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-indexes.sql', true, "Updating indexes to 20031107" ); } protected function doOldLinksUpdate() { @@ -288,10 +301,9 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Fixing ancient broken imagelinks table.\n" ); - $this->output( "NOTE: you will have to run maintenance/refreshLinks.php after this.\n" ); - $this->applyPatch( 'patch-fix-il_from.sql' ); - $this->output( "done.\n" ); + if( $this->applyPatch( 'patch-fix-il_from.sql', false, "Fixing ancient broken imagelinks table." ) ) { + $this->output("NOTE: you will have to run maintenance/refreshLinks.php after this." ); + } } /** @@ -513,9 +525,7 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Converting links and brokenlinks tables to pagelinks... " ); - $this->applyPatch( 'patch-pagelinks.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-pagelinks.sql', false, "Converting links and brokenlinks tables to pagelinks" ); global $wgContLang; foreach ( MWNamespace::getCanonicalNamespaces() as $ns => $name ) { @@ -551,9 +561,7 @@ class MysqlUpdater extends DatabaseUpdater { if ( !$duper->clearDupes() ) { $this->output( "WARNING: This next step will probably fail due to unfixed duplicates...\n" ); } - $this->output( "Adding unique index on user_name... " ); - $this->applyPatch( 'patch-user_nameindex.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-user_nameindex.sql', false, "Adding unique index on user_name" ); } protected function doUserGroupsUpdate() { @@ -566,9 +574,7 @@ class MysqlUpdater extends DatabaseUpdater { $this->db->query( "ALTER TABLE $oldug RENAME TO $newug", __METHOD__ ); $this->output( "done.\n" ); - $this->output( "Re-adding fresh user_groups table... " ); - $this->applyPatch( 'patch-user_groups.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-user_groups.sql', false, "Re-adding fresh user_groups table" ); $this->output( "***\n" ); $this->output( "*** WARNING: You will need to manually fix up user permissions in the user_groups\n" ); @@ -580,15 +586,11 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Adding user_groups table... " ); - $this->applyPatch( 'patch-user_groups.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-user_groups.sql', false, "Adding user_groups table" ); if ( !$this->db->tableExists( 'user_rights', __METHOD__ ) ) { if ( $this->db->fieldExists( 'user', 'user_rights', __METHOD__ ) ) { - $this->output( "Upgrading from a 1.3 or older database? Breaking out user_rights for conversion..." ); - $this->db->applyPatch( 'patch-user_rights.sql' ); - $this->output( "done.\n" ); + $this->db->applyPatch( 'patch-user_rights.sql', false, "Upgrading from a 1.3 or older database? Breaking out user_rights for conversion" ); } else { $this->output( "*** WARNING: couldn't locate user_rights table or field for upgrade.\n" ); $this->output( "*** You may need to manually configure some sysops by manipulating\n" ); @@ -630,9 +632,7 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Making wl_notificationtimestamp nullable... " ); - $this->applyPatch( 'patch-watchlist-null.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-watchlist-null.sql', false, "Making wl_notificationtimestamp nullable" ); } /** @@ -658,8 +658,8 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Creating templatelinks table...\n" ); - $this->applyPatch( 'patch-templatelinks.sql' ); + $this->applyPatch( 'patch-templatelinks.sql', false, "Creating templatelinks table" ); + $this->output( "Populating...\n" ); if ( wfGetLB()->getServerCount() > 1 ) { // Slow, replication-friendly update @@ -700,8 +700,7 @@ class MysqlUpdater extends DatabaseUpdater { !$this->indexHasField( 'templatelinks', 'tl_namespace', 'tl_from' ) || !$this->indexHasField( 'imagelinks', 'il_to', 'il_from' ) ) { - $this->applyPatch( 'patch-backlinkindexes.sql' ); - $this->output( "...backlinking indices updated\n" ); + $this->applyPatch( 'patch-backlinkindexes.sql', false, "Updating backlinking indices" ); } } @@ -716,9 +715,8 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Creating page_restrictions table..." ); - $this->applyPatch( 'patch-page_restrictions.sql' ); - $this->applyPatch( 'patch-page_restrictions_sortkey.sql' ); + $this->applyPatch( 'patch-page_restrictions.sql', false, "Creating page_restrictions table (1/2)" ); + $this->applyPatch( 'patch-page_restrictions_sortkey.sql', false, "Creating page_restrictions table (2/2)" ); $this->output( "done.\n" ); $this->output( "Migrating old restrictions to new table...\n" ); @@ -728,8 +726,7 @@ class MysqlUpdater extends DatabaseUpdater { protected function doCategorylinksIndicesUpdate() { if ( !$this->indexHasField( 'categorylinks', 'cl_sortkey', 'cl_from' ) ) { - $this->applyPatch( 'patch-categorylinksindex.sql' ); - $this->output( "...categorylinks indices updated\n" ); + $this->applyPatch( 'patch-categorylinksindex.sql', false, "Updating categorylinks Indices" ); } } @@ -768,18 +765,14 @@ class MysqlUpdater extends DatabaseUpdater { } elseif ( $this->db->fieldExists( 'profiling', 'pf_memory', __METHOD__ ) ) { $this->output( "...profiling table has pf_memory field.\n" ); } else { - $this->output( "Adding pf_memory field to table profiling..." ); - $this->applyPatch( 'patch-profiling-memory.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-profiling-memory.sql', false, "Adding pf_memory field to table profiling" ); } } protected function doFilearchiveIndicesUpdate() { $info = $this->db->indexInfo( 'filearchive', 'fa_user_timestamp', __METHOD__ ); if ( !$info ) { - $this->output( "Updating filearchive indices..." ); - $this->applyPatch( 'patch-filearchive-user-index.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-filearchive-user-index.sql', false, "Updating filearchive indices" ); } } @@ -790,9 +783,7 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Making pl_namespace, tl_namespace and il_to indices UNIQUE... " ); - $this->applyPatch( 'patch-pl-tl-il-unique.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-pl-tl-il-unique.sql', false, "Making pl_namespace, tl_namespace and il_to indices UNIQUE" ); } protected function renameEuWikiId() { @@ -801,9 +792,7 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Renaming eu_wiki_id -> eu_local_id... " ); - $this->applyPatch( 'patch-eu_local_id.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-eu_local_id.sql', false, "Renaming eu_wiki_id -> eu_local_id" ); } protected function doUpdateMimeMinorField() { @@ -812,9 +801,7 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Altering all *_mime_minor fields to 100 bytes in size ... " ); - $this->applyPatch( 'patch-mime_minor_length.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-mime_minor_length.sql', false, "Altering all *_mime_minor fields to 100 bytes in size" ); } protected function doClFieldsUpdate() { @@ -823,9 +810,7 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( 'Updating categorylinks (again)...' ); - $this->applyPatch( 'patch-categorylinks-better-collation2.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-categorylinks-better-collation2.sql', false, 'Updating categorylinks (again)' ); } protected function doLangLinksLengthUpdate() { @@ -834,9 +819,7 @@ class MysqlUpdater extends DatabaseUpdater { $row = $this->db->fetchObject( $res ); if ( $row && $row->Type == "varbinary(10)" ) { - $this->output( 'Updating length of ll_lang in langlinks...' ); - $this->applyPatch( 'patch-langlinks-ll_lang-20.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-langlinks-ll_lang-20.sql', false, 'Updating length of ll_lang in langlinks' ); } else { $this->output( "...ll_lang is up-to-date.\n" ); } @@ -849,8 +832,6 @@ class MysqlUpdater extends DatabaseUpdater { return; } - $this->output( "Making user_last_timestamp nullable... " ); - $this->applyPatch( 'patch-user-newtalk-timestamp-null.sql' ); - $this->output( "done.\n" ); + $this->applyPatch( 'patch-user-newtalk-timestamp-null.sql', false, "Making user_last_timestamp nullable" ); } } diff --git a/includes/installer/OracleInstaller.php b/includes/installer/OracleInstaller.php index 51e6d4a2..72ec800d 100644 --- a/includes/installer/OracleInstaller.php +++ b/includes/installer/OracleInstaller.php @@ -2,6 +2,21 @@ /** * Oracle-specific installer. * + * 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 Deployment */ @@ -47,12 +62,12 @@ class OracleInstaller extends DatabaseInstaller { return $this->getTextBox( 'wgDBserver', 'config-db-host-oracle', array(), $this->parent->getHelpBox( 'config-db-host-oracle-help' ) ) . Html::openElement( 'fieldset' ) . - Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) . + Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) . $this->getTextBox( 'wgDBprefix', 'config-db-prefix' ) . $this->getTextBox( '_OracleDefTS', 'config-oracle-def-ts' ) . $this->getTextBox( '_OracleTempTS', 'config-oracle-temp-ts', array(), $this->parent->getHelpBox( 'config-db-oracle-help' ) ) . Html::closeElement( 'fieldset' ) . - $this->parent->getWarningBox( wfMsg( 'config-db-account-oracle-warn' ) ). + $this->parent->getWarningBox( wfMessage( 'config-db-account-oracle-warn' )->text() ). $this->getInstallUserBox(). $this->getWebUserBox(); } @@ -243,6 +258,7 @@ class OracleInstaller extends DatabaseInstaller { /** * Overload: after this action field info table has to be rebuilt + * @return Status */ public function createTables() { $this->setupSchemaVars(); diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php index 93c2726b..e71c26fe 100644 --- a/includes/installer/OracleUpdater.php +++ b/includes/installer/OracleUpdater.php @@ -2,6 +2,21 @@ /** * Oracle-specific updater. * + * 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 Deployment */ @@ -51,6 +66,10 @@ class OracleUpdater extends DatabaseUpdater { array( 'doPageRestrictionsPKUKFix' ), array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ), + //1.20 + array( 'addIndex', 'ipblocks', 'i05', 'patch-ipblocks_i05_index.sql' ), + array( 'addIndex', 'revision', 'i05', 'patch-revision_i05_index.sql' ), + // KEEP THIS AT THE BOTTOM!! array( 'doRebuildDuplicateFunction' ), @@ -63,40 +82,32 @@ class OracleUpdater extends DatabaseUpdater { * Oracle inserts NULL, so namespace fields should have a default value */ protected function doNamespaceDefaults() { - $this->output( "Altering namespace fields with default value ... " ); $meta = $this->db->fieldInfo( 'page', 'page_namespace' ); if ( $meta->defaultValue() != null ) { - $this->output( "defaults seem to present on namespace fields\n" ); return; } - $this->applyPatch( 'patch_namespace_defaults.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_namespace_defaults.sql', false, "Altering namespace fields with default value" ); } /** * Uniform FK names + deferrable state */ protected function doFKRenameDeferr() { - $this->output( "Altering foreign keys ... " ); $meta = $this->db->query( 'SELECT COUNT(*) cnt FROM user_constraints WHERE constraint_type = \'R\' AND deferrable = \'DEFERRABLE\'' ); $row = $meta->fetchRow(); if ( $row && $row['cnt'] > 0 ) { - $this->output( "at least one FK is deferrable, considering up to date\n" ); return; } - $this->applyPatch( 'patch_fk_rename_deferred.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_fk_rename_deferred.sql', false, "Altering foreign keys ... " ); } /** * Recreate functions to 17 schema layout */ protected function doFunctions17() { - $this->output( "Recreating functions ... " ); - $this->applyPatch( 'patch_create_17_functions.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_create_17_functions.sql', false, "Recreating functions" ); } /** @@ -104,14 +115,11 @@ class OracleUpdater extends DatabaseUpdater { * there are no incremental patches prior to this */ protected function doSchemaUpgrade17() { - $this->output( "Updating schema to 17 ... " ); // check if iwlinks table exists which was added in 1.17 if ( $this->db->tableExists( 'iwlinks' ) ) { - $this->output( "schema seem to be up to date.\n" ); return; } - $this->applyPatch( 'patch_16_17_schema_changes.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_16_17_schema_changes.sql', false, "Updating schema to 17" ); } /** @@ -140,24 +148,19 @@ class OracleUpdater extends DatabaseUpdater { * converted to NULL in Oracle */ protected function doRemoveNotNullEmptyDefaults() { - $this->output( "Removing not null empty constraints ... " ); $meta = $this->db->fieldInfo( 'categorylinks' , 'cl_sortkey_prefix' ); if ( $meta->isNullable() ) { - $this->output( "constraints seem to be removed\n" ); return; } - $this->applyPatch( 'patch_remove_not_null_empty_defs.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_remove_not_null_empty_defs.sql', false, "Removing not null empty constraints" ); } + protected function doRemoveNotNullEmptyDefaults2() { - $this->output( "Removing not null empty constraints ... " ); $meta = $this->db->fieldInfo( 'ipblocks' , 'ipb_by_text' ); if ( $meta->isNullable() ) { - $this->output( "constraints seem to be removed\n" ); return; } - $this->applyPatch( 'patch_remove_not_null_empty_defs2.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_remove_not_null_empty_defs2.sql', false, "Removing not null empty constraints" ); } /** @@ -165,17 +168,13 @@ class OracleUpdater extends DatabaseUpdater { * cascading taken in account in the deleting function */ protected function doRecentchangesFK2Cascade() { - $this->output( "Altering RECENTCHANGES_FK2 ... " ); - $meta = $this->db->query( 'SELECT 1 FROM all_constraints WHERE owner = \''.strtoupper($this->db->getDBname()).'\' AND constraint_name = \''.$this->db->tablePrefix().'RECENTCHANGES_FK2\' AND delete_rule = \'CASCADE\'' ); $row = $meta->fetchRow(); if ( $row ) { - $this->output( "FK up to date\n" ); return; } - $this->applyPatch( 'patch_recentchanges_fk2_cascade.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_recentchanges_fk2_cascade.sql', false, "Altering RECENTCHANGES_FK2" ); } /** @@ -199,9 +198,7 @@ class OracleUpdater extends DatabaseUpdater { * rebuilding of the function that duplicates tables for tests */ protected function doRebuildDuplicateFunction() { - $this->output( "Rebuilding duplicate function ... " ); - $this->applyPatch( 'patch_rebuild_dupfunc.sql', false ); - $this->output( "ok\n" ); + $this->applyPatch( 'patch_rebuild_dupfunc.sql', false, "Rebuilding duplicate function" ); } /** diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php index fea012e2..3ac2b3a8 100644 --- a/includes/installer/PostgresInstaller.php +++ b/includes/installer/PostgresInstaller.php @@ -2,6 +2,21 @@ /** * PostgreSQL-specific installer. * + * 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 Deployment */ @@ -45,7 +60,7 @@ class PostgresInstaller extends DatabaseInstaller { $this->getTextBox( 'wgDBserver', 'config-db-host', array(), $this->parent->getHelpBox( 'config-db-host-help' ) ) . $this->getTextBox( 'wgDBport', 'config-db-port' ) . Html::openElement( 'fieldset' ) . - Html::element( 'legend', array(), wfMsg( 'config-db-wiki-settings' ) ) . + Html::element( 'legend', array(), wfMessage( 'config-db-wiki-settings' )->text() ) . $this->getTextBox( 'wgDBname', 'config-db-name', array(), $this->parent->getHelpBox( 'config-db-name-help' ) ) . $this->getTextBox( 'wgDBmwschema', 'config-db-schema', array(), $this->parent->getHelpBox( 'config-db-schema-help' ) ) . Html::closeElement( 'fieldset' ) . @@ -110,9 +125,9 @@ class PostgresInstaller extends DatabaseInstaller { /** * Open a PG connection with given parameters - * @param $user User name - * @param $password Password - * @param $dbName Database name + * @param $user string User name + * @param $password string Password + * @param $dbName string Database name * @return Status */ protected function openConnectionWithParams( $user, $password, $dbName ) { @@ -147,7 +162,7 @@ class PostgresInstaller extends DatabaseInstaller { */ $conn = $status->value; $conn->clearFlag( DBO_TRX ); - $conn->commit(); + $conn->commit( __METHOD__ ); $this->pgConns[$type] = $conn; } return $status; @@ -168,14 +183,14 @@ class PostgresInstaller extends DatabaseInstaller { * separate connection for this allows us to avoid accidental cross-module * dependencies. * - * @param $type The type of connection to get: + * @param $type string The type of connection to get: * - create-db: A connection for creating DBs, suitable for pre- * installation. * - create-schema: A connection to the new DB, for creating schemas and * other similar objects in the new DB. * - create-tables: A connection with a role suitable for creating tables. * - * @return A Status object. On success, a connection object will be in the + * @return Status object. On success, a connection object will be in the * value member. */ protected function openPgConnection( $type ) { @@ -344,6 +359,7 @@ class PostgresInstaller extends DatabaseInstaller { /** * Returns true if the install user is able to create objects owned * by the web user, false otherwise. + * @return bool */ protected function canCreateObjectsForWebUser() { if ( $this->isSuperUser() ) { @@ -365,10 +381,11 @@ class PostgresInstaller extends DatabaseInstaller { /** * Recursive helper for canCreateObjectsForWebUser(). - * @param $conn Database object - * @param $targetMember Role ID of the member to look for - * @param $group Role ID of the group to look for - * @param $maxDepth Maximum recursive search depth + * @param $conn DatabaseBase object + * @param $targetMember int Role ID of the member to look for + * @param $group int Role ID of the group to look for + * @param $maxDepth int Maximum recursive search depth + * @return bool */ protected function isRoleMember( $conn, $targetMember, $group, $maxDepth ) { if ( $targetMember === $group ) { @@ -429,10 +446,6 @@ class PostgresInstaller extends DatabaseInstaller { $conn = $status->value; $dbName = $this->getVar( 'wgDBname' ); - //$schema = $this->getVar( 'wgDBmwschema' ); - //$user = $this->getVar( 'wgDBuser' ); - //$safeschema = $conn->addIdentifierQuotes( $schema ); - //$safeuser = $conn->addIdentifierQuotes( $user ); $exists = $conn->selectField( '"pg_catalog"."pg_database"', '1', array( 'datname' => $dbName ), __METHOD__ ); @@ -464,19 +477,13 @@ class PostgresInstaller extends DatabaseInstaller { } } - // If we created a user, alter it now to search the new schema by default - if ( $this->getVar( '_CreateDBAccount' ) ) { - $conn->query( "ALTER ROLE $safeuser SET search_path = $safeschema, public", - __METHOD__ ); - } - // Select the new schema in the current connection - $conn->query( "SET search_path = $safeschema" ); + $conn->determineCoreSchema( $schema ); return Status::newGood(); } function commitChanges() { - $this->db->commit(); + $this->db->commit( __METHOD__ ); return Status::newGood(); } @@ -491,10 +498,8 @@ class PostgresInstaller extends DatabaseInstaller { } $conn = $status->value; - //$schema = $this->getVar( 'wgDBmwschema' ); $safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) ); $safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) ); - //$safeschema = $conn->addIdentifierQuotes( $schema ); // Check if the user already exists $userExists = $conn->roleExists( $this->getVar( 'wgDBuser' ) ); @@ -551,7 +556,7 @@ class PostgresInstaller extends DatabaseInstaller { */ $conn = $status->value; - if( $conn->tableExists( 'user' ) ) { + if( $conn->tableExists( 'archive' ) ) { $status->warning( 'config-install-tables-exist' ); $this->enableLB(); return $status; diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php index 023cb300..6cffe84a 100644 --- a/includes/installer/PostgresUpdater.php +++ b/includes/installer/PostgresUpdater.php @@ -2,6 +2,21 @@ /** * PostgreSQL-specific updater. * + * 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 Deployment */ @@ -23,28 +38,35 @@ class PostgresUpdater extends DatabaseUpdater { /** * @todo FIXME: Postgres should use sequential updates like Mysql, Sqlite * and everybody else. It never got refactored like it should've. + * @return array */ protected function getCoreUpdateList() { return array( # rename tables 1.7.3 # r15791 Change reserved word table names "user" and "text" - array( 'renameTable', 'user', 'mwuser'), - array( 'renameTable', 'text', 'pagecontent'), - - # new sequences - array( 'addSequence', 'logging_log_id_seq' ), - array( 'addSequence', 'page_restrictions_pr_id_seq' ), + array( 'renameTable', 'user', 'mwuser' ), + array( 'renameTable', 'text', 'pagecontent' ), + array( 'renameIndex', 'mwuser', 'user_pkey', 'mwuser_pkey'), + array( 'renameIndex', 'mwuser', 'user_user_name_key', 'mwuser_user_name_key' ), + array( 'renameIndex', 'pagecontent','text_pkey', 'pagecontent_pkey' ), # renamed sequences array( 'renameSequence', 'ipblocks_ipb_id_val', 'ipblocks_ipb_id_seq' ), array( 'renameSequence', 'rev_rev_id_val', 'revision_rev_id_seq' ), array( 'renameSequence', 'text_old_id_val', 'text_old_id_seq' ), - array( 'renameSequence', 'category_id_seq', 'category_cat_id_seq' ), array( 'renameSequence', 'rc_rc_id_seq', 'recentchanges_rc_id_seq' ), array( 'renameSequence', 'log_log_id_seq', 'logging_log_id_seq' ), array( 'renameSequence', 'pr_id_val', 'page_restrictions_pr_id_seq' ), array( 'renameSequence', 'us_id_seq', 'uploadstash_us_id_seq' ), + # since r58263 + array( 'renameSequence', 'category_id_seq', 'category_cat_id_seq'), + + # new sequences if not renamed above + array( 'addSequence', 'logging', false, 'logging_log_id_seq' ), + array( 'addSequence', 'page_restrictions', false, 'page_restrictions_pr_id_seq' ), + array( 'addSequence', 'filearchive', 'fa_id', 'filearchive_fa_id_seq' ), + # new tables array( 'addTable', 'category', 'patch-category.sql' ), array( 'addTable', 'page', 'patch-page.sql' ), @@ -67,11 +89,13 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addTable', 'module_deps', 'patch-module_deps.sql' ), array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ), array( 'addTable', 'user_former_groups','patch-user_former_groups.sql' ), + array( 'addTable', 'external_user', 'patch-external_user.sql' ), # Needed before new field array( 'convertArchive2' ), # new fields + array( 'addPgField', 'updatelog', 'ul_value', 'TEXT' ), array( 'addPgField', 'archive', 'ar_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), array( 'addPgField', 'archive', 'ar_len', 'INTEGER' ), array( 'addPgField', 'archive', 'ar_page_id', 'INTEGER' ), @@ -87,6 +111,7 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addPgField', 'ipblocks', 'ipb_create_account', 'SMALLINT NOT NULL DEFAULT 1' ), array( 'addPgField', 'ipblocks', 'ipb_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), array( 'addPgField', 'ipblocks', 'ipb_enable_autoblock', 'SMALLINT NOT NULL DEFAULT 1' ), + array( 'addPgField', 'ipblocks', 'ipb_parent_block_id', 'INTEGER DEFAULT NULL REFERENCES ipblocks(ipb_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED' ), array( 'addPgField', 'filearchive', 'fa_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), array( 'addPgField', 'logging', 'log_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), array( 'addPgField', 'logging', 'log_id', "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('logging_log_id_seq')" ), @@ -138,7 +163,7 @@ class PostgresUpdater extends DatabaseUpdater { array( 'changeField', 'image', 'img_size', 'integer', '' ), array( 'changeField', 'image', 'img_width', 'integer', '' ), array( 'changeField', 'image', 'img_height', 'integer', '' ), - array( 'changeField', 'interwiki', 'iw_local', 'smallint', 'iw_local::smallint DEFAULT 0' ), + array( 'changeField', 'interwiki', 'iw_local', 'smallint', 'iw_local::smallint' ), array( 'changeField', 'interwiki', 'iw_trans', 'smallint', 'iw_trans::smallint DEFAULT 0' ), array( 'changeField', 'ipblocks', 'ipb_auto', 'smallint', 'ipb_auto::smallint DEFAULT 0' ), array( 'changeField', 'ipblocks', 'ipb_anon_only', 'smallint', "CASE WHEN ipb_anon_only=' ' THEN 0 ELSE ipb_anon_only::smallint END DEFAULT 0" ), @@ -168,18 +193,23 @@ class PostgresUpdater extends DatabaseUpdater { array( 'changeField', 'revision', 'rev_minor_edit', 'smallint', 'rev_minor_edit::smallint DEFAULT 0' ), array( 'changeField', 'templatelinks', 'tl_namespace', 'smallint', 'tl_namespace::smallint' ), array( 'changeField', 'user_newtalk', 'user_ip', 'text', 'host(user_ip)' ), + array( 'changeField', 'uploadstash', 'us_image_bits', 'smallint', '' ), # null changes array( 'changeNullableField', 'oldimage', 'oi_bits', 'NULL' ), array( 'changeNullableField', 'oldimage', 'oi_timestamp', 'NULL' ), array( 'changeNullableField', 'oldimage', 'oi_major_mime', 'NULL' ), array( 'changeNullableField', 'oldimage', 'oi_minor_mime', 'NULL' ), + array( 'changeNullableField', 'image', 'img_metadata', 'NOT NULL'), + array( 'changeNullableField', 'filearchive', 'fa_metadata', 'NOT NULL'), + array( 'changeNullableField', 'recentchanges', 'rc_cur_id', 'NULL' ), array( 'checkOiDeleted' ), # New indexes array( 'addPgIndex', 'archive', 'archive_user_text', '(ar_user_text)' ), array( 'addPgIndex', 'image', 'img_sha1', '(img_sha1)' ), + array( 'addPgIndex', 'ipblocks', 'ipb_parent_block_id', '(ipb_parent_block_id)' ), array( 'addPgIndex', 'oldimage', 'oi_sha1', '(oi_sha1)' ), array( 'addPgIndex', 'page', 'page_mediawiki_title', '(page_title) WHERE page_namespace = 8' ), array( 'addPgIndex', 'pagelinks', 'pagelinks_title', '(pl_title)' ), @@ -192,12 +222,78 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addPgIndex', 'iwlinks', 'iwl_prefix_title_from', '(iwl_prefix, iwl_title, iwl_from)' ), array( 'addPgIndex', 'job', 'job_timestamp_idx', '(job_timestamp)' ), + array( 'checkIndex', 'pagelink_unique', array( + array('pl_from', 'int4_ops', 'btree', 0), + array('pl_namespace', 'int2_ops', 'btree', 0), + array('pl_title', 'text_ops', 'btree', 0), + ), + 'CREATE UNIQUE INDEX pagelink_unique ON pagelinks (pl_from,pl_namespace,pl_title)' ), + array( 'checkIndex', 'cl_sortkey', array( + array('cl_to', 'text_ops', 'btree', 0), + array('cl_sortkey', 'text_ops', 'btree', 0), + array('cl_from', 'int4_ops', 'btree', 0), + ), + 'CREATE INDEX cl_sortkey ON "categorylinks" USING "btree" ("cl_to", "cl_sortkey", "cl_from")' ), + array( 'checkIndex', 'logging_times', array( + array('log_timestamp', 'timestamptz_ops', 'btree', 0), + ), + 'CREATE INDEX "logging_times" ON "logging" USING "btree" ("log_timestamp")' ), + array( 'dropIndex', 'oldimage', 'oi_name' ), + array( 'checkIndex', 'oi_name_archive_name', array( + array('oi_name', 'text_ops', 'btree', 0), + array('oi_archive_name', 'text_ops', 'btree', 0), + ), + 'CREATE INDEX "oi_name_archive_name" ON "oldimage" USING "btree" ("oi_name", "oi_archive_name")' ), + array( 'checkIndex', 'oi_name_timestamp', array( + array('oi_name', 'text_ops', 'btree', 0), + array('oi_timestamp', 'timestamptz_ops', 'btree', 0), + ), + 'CREATE INDEX "oi_name_timestamp" ON "oldimage" USING "btree" ("oi_name", "oi_timestamp")' ), + array( 'checkIndex', 'page_main_title', array( + array('page_title', 'text_pattern_ops', 'btree', 0), + ), + 'CREATE INDEX "page_main_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 0)' ), + array( 'checkIndex', 'page_mediawiki_title', array( + array('page_title', 'text_pattern_ops', 'btree', 0), + ), + 'CREATE INDEX "page_mediawiki_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 8)' ), + array( 'checkIndex', 'page_project_title', array( + array('page_title', 'text_pattern_ops', 'btree', 0), + ), + 'CREATE INDEX "page_project_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 4)' ), + array( 'checkIndex', 'page_talk_title', array( + array('page_title', 'text_pattern_ops', 'btree', 0), + ), + 'CREATE INDEX "page_talk_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 1)' ), + array( 'checkIndex', 'page_user_title', array( + array('page_title', 'text_pattern_ops', 'btree', 0), + ), + 'CREATE INDEX "page_user_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 2)' ), + array( 'checkIndex', 'page_utalk_title', array( + array('page_title', 'text_pattern_ops', 'btree', 0), + ), + 'CREATE INDEX "page_utalk_title" ON "page" USING "btree" ("page_title" "text_pattern_ops") WHERE ("page_namespace" = 3)' ), + array( 'checkIndex', 'ts2_page_text', array( + array('textvector', 'tsvector_ops', 'gist', 0), + ), + 'CREATE INDEX "ts2_page_text" ON "pagecontent" USING "gist" ("textvector")' ), + array( 'checkIndex', 'ts2_page_title', array( + array('titlevector', 'tsvector_ops', 'gist', 0), + ), + 'CREATE INDEX "ts2_page_title" ON "page" USING "gist" ("titlevector")' ), + array( 'checkOiNameConstraint' ), array( 'checkPageDeletedTrigger' ), - array( 'checkRcCurIdNullable' ), - array( 'checkPagelinkUniqueIndex' ), array( 'checkRevUserFkey' ), - array( 'checkIpbAdress' ), + array( 'dropIndex', 'ipblocks', 'ipb_address'), + array( 'checkIndex', 'ipb_address_unique', array( + array('ipb_address', 'text_ops', 'btree', 0), + array('ipb_user', 'int4_ops', 'btree', 0), + array('ipb_auto', 'int2_ops', 'btree', 0), + array('ipb_anon_only', 'int2_ops', 'btree', 0), + ), + 'CREATE UNIQUE INDEX ipb_address_unique ON ipblocks (ipb_address,ipb_user,ipb_auto,ipb_anon_only)' ), + array( 'checkIwlPrefix' ), # All FK columns should be deferred @@ -210,6 +306,7 @@ class PostgresUpdater extends DatabaseUpdater { array( 'changeFkeyDeferrable', 'imagelinks', 'il_from', 'page(page_id) ON DELETE CASCADE' ), array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_by', 'mwuser(user_id) ON DELETE CASCADE' ), array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_user', 'mwuser(user_id) ON DELETE SET NULL' ), + array( 'changeFkeyDeferrable', 'ipblocks', 'ipb_parent_block_id', 'ipblocks(ipb_id) ON DELETE SET NULL' ), array( 'changeFkeyDeferrable', 'langlinks', 'll_from', 'page(page_id) ON DELETE CASCADE' ), array( 'changeFkeyDeferrable', 'logging', 'log_user', 'mwuser(user_id) ON DELETE SET NULL' ), array( 'changeFkeyDeferrable', 'oldimage', 'oi_name', 'image(img_name) ON DELETE CASCADE ON UPDATE CASCADE' ), @@ -229,6 +326,8 @@ class PostgresUpdater extends DatabaseUpdater { array( 'changeFkeyDeferrable', 'user_properties', 'up_user', 'mwuser(user_id) ON DELETE CASCADE' ), array( 'changeFkeyDeferrable', 'watchlist', 'wl_user', 'mwuser(user_id) ON DELETE CASCADE' ), + # r81574 + array( 'addInterwikiType' ), # end array( 'tsearchFixes' ), ); @@ -274,7 +373,6 @@ class PostgresUpdater extends DatabaseUpdater { } protected function describeTable( $table ) { - global $wgDBmwschema; $q = <<<END SELECT attname, attnum FROM pg_namespace, pg_class, pg_attribute WHERE pg_class.relnamespace = pg_namespace.oid @@ -283,7 +381,7 @@ SELECT attname, attnum FROM pg_namespace, pg_class, pg_attribute END; $res = $this->db->query( sprintf( $q, $this->db->addQuotes( $table ), - $this->db->addQuotes( $wgDBmwschema ) ) ); + $this->db->addQuotes( $this->db->getCoreSchema() ) ) ); if ( !$res ) { return null; } @@ -299,8 +397,6 @@ END; } function describeIndex( $idx ) { - global $wgDBmwschema; - // first fetch the key (which is a list of columns ords) and // the table the index applies to (an oid) $q = <<<END @@ -313,7 +409,7 @@ END; $res = $this->db->query( sprintf( $q, - $this->db->addQuotes( $wgDBmwschema ), + $this->db->addQuotes( $this->db->getCoreSchema() ), $this->db->addQuotes( $idx ) ) ); @@ -350,7 +446,6 @@ END; } function fkeyDeltype( $fkey ) { - global $wgDBmwschema; $q = <<<END SELECT confdeltype FROM pg_constraint, pg_namespace WHERE connamespace=pg_namespace.oid @@ -360,7 +455,7 @@ END; $r = $this->db->query( sprintf( $q, - $this->db->addQuotes( $wgDBmwschema ), + $this->db->addQuotes( $this->db->getCoreSchema() ), $this->db->addQuotes( $fkey ) ) ); @@ -371,7 +466,6 @@ END; } function ruleDef( $table, $rule ) { - global $wgDBmwschema; $q = <<<END SELECT definition FROM pg_rules WHERE schemaname = %s @@ -381,7 +475,7 @@ END; $r = $this->db->query( sprintf( $q, - $this->db->addQuotes( $wgDBmwschema ), + $this->db->addQuotes( $this->db->getCoreSchema() ), $this->db->addQuotes( $table ), $this->db->addQuotes( $rule ) ) @@ -394,10 +488,13 @@ END; return $d; } - protected function addSequence( $ns ) { + protected function addSequence( $table, $pkey, $ns ) { if ( !$this->db->sequenceExists( $ns ) ) { $this->output( "Creating sequence $ns\n" ); $this->db->query( "CREATE SEQUENCE $ns" ); + if( $pkey !== false ) { + $this->setDefault( $table, $pkey, '"nextval"(\'"' . $ns . '"\'::"regclass")' ); + } } } @@ -412,12 +509,22 @@ END; } } - protected function renameTable( $old, $new ) { + protected function renameTable( $old, $new, $patch = false ) { if ( $this->db->tableExists( $old ) ) { $this->output( "Renaming table $old to $new\n" ); $old = $this->db->realTableName( $old, "quoted" ); $new = $this->db->realTableName( $new, "quoted" ); $this->db->query( "ALTER TABLE $old RENAME TO $new" ); + if( $patch !== false ) { + $this->applyPatch( $patch ); + } + } + } + + protected function renameIndex( $table, $old, $new ) { + if ( $this->db->indexExists( $table, $old ) ) { + $this->output( "Renaming index $old to $new\n" ); + $this->db->query( "ALTER INDEX $old RENAME TO $new" ); } } @@ -453,13 +560,20 @@ END; } $sql .= " USING $default"; } - $this->db->begin( __METHOD__ ); $this->db->query( $sql ); - $this->db->commit( __METHOD__ ); } } - protected function changeNullableField( $table, $field, $null ) { + protected function setDefault( $table, $field, $default ) { + + $info = $this->db->fieldInfo( $table, $field ); + if ( $info->defaultValue() !== $default ) { + $this->output( "Changing '$table.$field' default value\n" ); + $this->db->query( "ALTER TABLE $table ALTER $field SET DEFAULT " . $default ); + } + } + + protected function changeNullableField( $table, $field, $null) { $fi = $this->db->fieldInfo( $table, $field ); if ( is_null( $fi ) ) { $this->output( "...ERROR: expected column $table.$field to exist\n" ); @@ -498,11 +612,11 @@ END; if ( $this->db->indexExists( $table, $index ) ) { $this->output( "...index '$index' on table '$table' already exists\n" ); } else { - $this->output( "Creating index '$index' on table '$table'\n" ); if ( preg_match( '/^\(/', $type ) ) { + $this->output( "Creating index '$index' on table '$table'\n" ); $this->db->query( "CREATE INDEX $index ON $table $type" ); } else { - $this->applyPatch( $type, true ); + $this->applyPatch( $type, true, "Creating index '$index' on table '$table'" ); } } } @@ -518,15 +632,20 @@ END; } $this->output( "Altering column '$table.$field' to be DEFERRABLE INITIALLY DEFERRED\n" ); $conname = $fi->conname(); - $command = "ALTER TABLE $table DROP CONSTRAINT $conname"; - $this->db->query( $command ); - $command = "ALTER TABLE $table ADD CONSTRAINT $conname FOREIGN KEY ($field) REFERENCES $clause DEFERRABLE INITIALLY DEFERRED"; + if ( $fi->conname() ) { + $conclause = "CONSTRAINT \"$conname\""; + $command = "ALTER TABLE $table DROP CONSTRAINT $conname"; + $this->db->query( $command ); + } else { + $this->output( "Column '$table.$field' does not have a foreign key constraint, will be added\n" ); + $conclause = ""; + } + $command = "ALTER TABLE $table ADD $conclause FOREIGN KEY ($field) REFERENCES $clause DEFERRABLE INITIALLY DEFERRED"; $this->db->query( $command ); } protected function convertArchive2() { if ( $this->db->tableExists( "archive2" ) ) { - $this->output( "Converting 'archive2' back to normal archive table\n" ); if ( $this->db->ruleExists( 'archive', 'archive_insert' ) ) { $this->output( "Dropping rule 'archive_insert'\n" ); $this->db->query( 'DROP RULE archive_insert ON archive' ); @@ -535,7 +654,7 @@ END; $this->output( "Dropping rule 'archive_delete'\n" ); $this->db->query( 'DROP RULE archive_delete ON archive' ); } - $this->applyPatch( 'patch-remove-archive2.sql' ); + $this->applyPatch( 'patch-remove-archive2.sql', false, "Converting 'archive2' back to normal archive table" ); } else { $this->output( "...obsolete table 'archive2' does not exist\n" ); } @@ -570,38 +689,34 @@ END; protected function checkPageDeletedTrigger() { if ( !$this->db->triggerExists( 'page', 'page_deleted' ) ) { - $this->output( "Adding function and trigger 'page_deleted' to table 'page'\n" ); - $this->applyPatch( 'patch-page_deleted.sql' ); + $this->applyPatch( 'patch-page_deleted.sql', false, "Adding function and trigger 'page_deleted' to table 'page'" ); } else { $this->output( "...table 'page' has 'page_deleted' trigger\n" ); } } - protected function checkRcCurIdNullable(){ - $fi = $this->db->fieldInfo( 'recentchanges', 'rc_cur_id' ); - if ( !$fi->isNullable() ) { - $this->output( "Removing NOT NULL constraint from 'recentchanges.rc_cur_id'\n" ); - $this->applyPatch( 'patch-rc_cur_id-not-null.sql' ); - } else { - $this->output( "...column 'recentchanges.rc_cur_id' has a NOT NULL constraint\n" ); + protected function dropIndex( $table, $index, $patch = '', $fullpath = false ) { + if ( $this->db->indexExists( $table, $index ) ) { + $this->output( "Dropping obsolete index '$index'\n" ); + $this->db->query( "DROP INDEX \"". $index ."\"" ); } } - protected function checkPagelinkUniqueIndex() { - $pu = $this->describeIndex( 'pagelink_unique' ); - if ( !is_null( $pu ) && ( $pu[0] != 'pl_from' || $pu[1] != 'pl_namespace' || $pu[2] != 'pl_title' ) ) { - $this->output( "Dropping obsolete version of index 'pagelink_unique index'\n" ); - $this->db->query( 'DROP INDEX pagelink_unique' ); - $pu = null; + protected function checkIndex( $index, $should_be, $good_def ) { + $pu = $this->db->indexAttributes( $index ); + if ( !empty( $pu ) && $pu != $should_be ) { + $this->output( "Dropping obsolete version of index '$index'\n" ); + $this->db->query( "DROP INDEX \"". $index ."\"" ); + $pu = array(); } else { - $this->output( "...obsolete version of index 'pagelink_unique index' does not exist\n" ); + $this->output( "...no need to drop index '$index'\n" ); } - if ( is_null( $pu ) ) { - $this->output( "Creating index 'pagelink_unique index'\n" ); - $this->db->query( 'CREATE UNIQUE INDEX pagelink_unique ON pagelinks (pl_from,pl_namespace,pl_title)' ); + if ( empty( $pu ) ) { + $this->output( "Creating index '$index'\n" ); + $this->db->query( $good_def ); } else { - $this->output( "...index 'pagelink_unique_index' already exists\n" ); + $this->output( "...index '$index' exists\n" ); } } @@ -609,43 +724,30 @@ END; if ( $this->fkeyDeltype( 'revision_rev_user_fkey' ) == 'r' ) { $this->output( "...constraint 'revision_rev_user_fkey' is ON DELETE RESTRICT\n" ); } else { - $this->output( "Changing constraint 'revision_rev_user_fkey' to ON DELETE RESTRICT\n" ); - $this->applyPatch( 'patch-revision_rev_user_fkey.sql' ); - } - } - - protected function checkIpbAdress() { - if ( $this->db->indexExists( 'ipblocks', 'ipb_address' ) ) { - $this->output( "Removing deprecated index 'ipb_address'...\n" ); - $this->db->query( 'DROP INDEX ipb_address' ); - } - if ( $this->db->indexExists( 'ipblocks', 'ipb_address_unique' ) ) { - $this->output( "...have ipb_address_unique\n" ); - } else { - $this->output( "Adding ipb_address_unique index\n" ); - $this->applyPatch( 'patch-ipb_address_unique.sql' ); + $this->applyPatch( 'patch-revision_rev_user_fkey.sql', false, "Changing constraint 'revision_rev_user_fkey' to ON DELETE RESTRICT" ); } } protected function checkIwlPrefix() { if ( $this->db->indexExists( 'iwlinks', 'iwl_prefix' ) ) { - $this->output( "Replacing index 'iwl_prefix' with 'iwl_prefix_from_title'...\n" ); - $this->applyPatch( 'patch-rename-iwl_prefix.sql' ); + $this->applyPatch( 'patch-rename-iwl_prefix.sql', false, "Replacing index 'iwl_prefix' with 'iwl_prefix_from_title'" ); } } + protected function addInterwikiType() { + $this->applyPatch( 'patch-add_interwiki.sql', false, "Refreshing add_interwiki()" ); + } + protected function tsearchFixes() { # Tweak the page_title tsearch2 trigger to filter out slashes # This is create or replace, so harmless to call if not needed - $this->output( "Refreshing ts2_page_title()...\n" ); - $this->applyPatch( 'patch-ts2pagetitle.sql' ); + $this->applyPatch( 'patch-ts2pagetitle.sql', false, "Refreshing ts2_page_title()" ); # If the server is 8.3 or higher, rewrite the tsearch2 triggers # in case they have the old 'default' versions # Gather version numbers in case we need them if ( $this->db->getServerVersion() >= 8.3 ) { - $this->output( "Rewriting tsearch2 triggers...\n" ); - $this->applyPatch( 'patch-tsearch2funcs.sql' ); + $this->applyPatch( 'patch-tsearch2funcs.sql', false, "Rewriting tsearch2 triggers" ); } } } diff --git a/includes/installer/SqliteInstaller.php b/includes/installer/SqliteInstaller.php index 658a3b16..6e1a74f6 100644 --- a/includes/installer/SqliteInstaller.php +++ b/includes/installer/SqliteInstaller.php @@ -2,6 +2,21 @@ /** * Sqlite-specific installer. * + * 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 Deployment */ @@ -207,7 +222,7 @@ class SqliteInstaller extends DatabaseInstaller { } /** - * @return Staus + * @return Status */ public function createTables() { $status = parent::createTables(); diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index e1bc2926..12a310af 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -2,6 +2,21 @@ /** * Sqlite-specific updater. * + * 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 Deployment */ @@ -71,6 +86,12 @@ class SqliteUpdater extends DatabaseUpdater { array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ), array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ), array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ), + + // 1.20 + array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ), + array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ), + array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ), + array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ), ); } @@ -80,22 +101,16 @@ class SqliteUpdater extends DatabaseUpdater { $this->output( "...have initial indexes\n" ); return; } - $this->output( "Adding initial indexes..." ); - $this->applyPatch( 'initial-indexes.sql' ); - $this->output( "done\n" ); + $this->applyPatch( 'initial-indexes.sql', false, "Adding initial indexes" ); } protected function sqliteSetupSearchindex() { $module = DatabaseSqlite::getFulltextSearchModule(); $fts3tTable = $this->updateRowExists( 'fts3' ); if ( $fts3tTable && !$module ) { - $this->output( '...PHP is missing FTS3 support, downgrading tables...' ); - $this->applyPatch( 'searchindex-no-fts.sql' ); - $this->output( "done\n" ); + $this->applyPatch( 'searchindex-no-fts.sql', false, 'PHP is missing FTS3 support, downgrading tables' ); } elseif ( !$fts3tTable && $module == 'FTS3' ) { - $this->output( '...adding FTS3 search capabilities...' ); - $this->applyPatch( 'searchindex-fts3.sql' ); - $this->output( "done\n" ); + $this->applyPatch( 'searchindex-fts3.sql', false, "Adding FTS3 search capabilities" ); } else { $this->output( "...fulltext search table appears to be in order.\n" ); } diff --git a/includes/installer/WebInstaller.php b/includes/installer/WebInstaller.php index 1ff77db7..2f46ff0b 100644 --- a/includes/installer/WebInstaller.php +++ b/includes/installer/WebInstaller.php @@ -2,6 +2,21 @@ /** * Core installer web interface. * + * 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 Deployment */ @@ -146,7 +161,7 @@ class WebInstaller extends Installer { 'Content-Disposition: attachment; filename="LocalSettings.php"' ); - $ls = new LocalSettingsGenerator( $this ); + $ls = InstallerOverrides::getLocalSettingsGenerator( $this ); $rightsProfile = $this->rightsProfiles[$this->getVar( '_RightsProfile' )]; foreach( $rightsProfile as $group => $rightsArr ) { $ls->setGroupRights( $group, $rightsArr ); @@ -347,21 +362,21 @@ class WebInstaller extends Installer { $url = $m[1]; } return md5( serialize( array( - 'local path' => dirname( dirname( __FILE__ ) ), + 'local path' => dirname( __DIR__ ), 'url' => $url, 'version' => $GLOBALS['wgVersion'] ) ) ); } /** - * Show an error message in a box. Parameters are like wfMsg(). + * Show an error message in a box. Parameters are like wfMessage(). * @param $msg */ public function showError( $msg /*...*/ ) { $args = func_get_args(); array_shift( $args ); $args = array_map( 'htmlspecialchars', $args ); - $msg = wfMsgReal( $msg, $args, false, false, false ); + $msg = wfMessage( $msg, $args )->useDatabase( false )->plain(); $this->output->addHTML( $this->getErrorBox( $msg ) ); } @@ -433,6 +448,7 @@ class WebInstaller extends Installer { * * @param $name String * @param $default + * @return null */ public function getSession( $name, $default = null ) { if ( !isset( $this->session[$name] ) ) { @@ -484,7 +500,7 @@ class WebInstaller extends Installer { public function getAcceptLanguage() { global $wgLanguageCode, $wgRequest; - $mwLanguages = Language::getLanguageNames(); + $mwLanguages = Language::fetchLanguageNames(); $headerLanguages = array_keys( $wgRequest->getAcceptLang() ); foreach ( $headerLanguages as $lang ) { @@ -524,7 +540,7 @@ class WebInstaller extends Installer { $s .= $this->getPageListItem( 'Restart', true, $currentPageName ); $s .= "</ul></div>\n"; // end list pane $s .= Html::element( 'h2', array(), - wfMsg( 'config-page-' . strtolower( $currentPageName ) ) ); + wfMessage( 'config-page-' . strtolower( $currentPageName ) )->text() ); $this->output->addHTMLNoFlush( $s ); } @@ -540,7 +556,7 @@ class WebInstaller extends Installer { */ private function getPageListItem( $pageName, $enabled, $currentPageName ) { $s = "<li class=\"config-page-list-item\">"; - $name = wfMsg( 'config-page-' . strtolower( $pageName ) ); + $name = wfMessage( 'config-page-' . strtolower( $pageName ) )->text(); if ( $enabled ) { $query = array( 'page' => $pageName ); @@ -593,7 +609,7 @@ class WebInstaller extends Installer { /** * Get HTML for an error box with an icon. * - * @param $text String: wikitext, get this with wfMsgNoTrans() + * @param $text String: wikitext, get this with wfMessage()->plain() * * @return string */ @@ -604,7 +620,7 @@ class WebInstaller extends Installer { /** * Get HTML for a warning box with an icon. * - * @param $text String: wikitext, get this with wfMsgNoTrans() + * @param $text String: wikitext, get this with wfMessage()->plain() * * @return string */ @@ -615,7 +631,7 @@ class WebInstaller extends Installer { /** * Get HTML for an info box with an icon. * - * @param $text String: wikitext, get this with wfMsgNoTrans() + * @param $text String: wikitext, get this with wfMessage()->plain() * @param $icon String: icon name, file in skins/common/images * @param $class String: additional class name to add to the wrapper div * @@ -624,13 +640,13 @@ class WebInstaller extends Installer { public function getInfoBox( $text, $icon = false, $class = false ) { $text = $this->parse( $text, true ); $icon = ( $icon == false ) ? '../skins/common/images/info-32.png' : '../skins/common/images/'.$icon; - $alt = wfMsg( 'config-information' ); + $alt = wfMessage( 'config-information' )->text(); return Html::infoBox( $text, $icon, $alt, $class, false ); } /** * Get small text indented help for a preceding form field. - * Parameters like wfMsg(). + * Parameters like wfMessage(). * * @param $msg * @return string @@ -639,18 +655,19 @@ class WebInstaller extends Installer { $args = func_get_args(); array_shift( $args ); $args = array_map( 'htmlspecialchars', $args ); - $text = wfMsgReal( $msg, $args, false, false, false ); + $text = wfMessage( $msg, $args )->useDatabase( false )->plain(); $html = $this->parse( $text, true ); return "<div class=\"mw-help-field-container\">\n" . - "<span class=\"mw-help-field-hint\">" . wfMsgHtml( 'config-help' ) . "</span>\n" . + "<span class=\"mw-help-field-hint\">" . wfMessage( 'config-help' )->escaped() . + "</span>\n" . "<span class=\"mw-help-field-data\">" . $html . "</span>\n" . "</div>\n"; } /** * Output a help box. - * @param $msg String key for wfMsg() + * @param $msg String key for wfMessage() */ public function showHelpBox( $msg /*, ... */ ) { $args = func_get_args(); @@ -668,7 +685,7 @@ class WebInstaller extends Installer { $args = func_get_args(); array_shift( $args ); $html = '<div class="config-message">' . - $this->parse( wfMsgReal( $msg, $args, false, false, false ) ) . + $this->parse( wfMessage( $msg, $args )->useDatabase( false )->plain() ) . "</div>\n"; $this->output->addHTML( $html ); } @@ -697,7 +714,7 @@ class WebInstaller extends Installer { if ( strval( $msg ) == '' ) { $labelText = ' '; } else { - $labelText = wfMsgHtml( $msg ); + $labelText = wfMessage( $msg )->escaped(); } $attributes = array( 'class' => 'config-label' ); @@ -877,7 +894,7 @@ class WebInstaller extends Installer { if( isset( $params['rawtext'] ) ) { $labelText = $params['rawtext']; } else { - $labelText = $this->parse( wfMsg( $params['label'] ) ); + $labelText = $this->parse( wfMessage( $params['label'] )->text() ); } return @@ -953,7 +970,7 @@ class WebInstaller extends Installer { Xml::radio( $params['controlName'], $value, $checked, $itemAttribs ) . ' ' . Xml::tags( 'label', array( 'for' => $id ), $this->parse( - wfMsgNoTrans( $params['itemLabelPrefix'] . strtolower( $value ) ) + wfMessage( $params['itemLabelPrefix'] . strtolower( $value ) )->plain() ) ) . "</li>\n"; } @@ -1061,7 +1078,7 @@ class WebInstaller extends Installer { ) ); $anchor = Html::rawElement( 'a', array( 'href' => $this->getURL( array( 'localsettings' => 1 ) ) ), - $img . ' ' . wfMsgHtml( 'config-download-localsettings' ) ); + $img . ' ' . wfMessage( 'config-download-localsettings' )->escaped() ); return Html::rawElement( 'div', array( 'class' => 'config-download-link' ), $anchor ); } diff --git a/includes/installer/WebInstallerOutput.php b/includes/installer/WebInstallerOutput.php index c6c8a4c2..f3166c25 100644 --- a/includes/installer/WebInstallerOutput.php +++ b/includes/installer/WebInstallerOutput.php @@ -2,6 +2,21 @@ /** * Output handler for the web installer. * + * 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 Deployment */ @@ -93,7 +108,7 @@ class WebInstallerOutput { * @return String */ public function getCSS( $dir ) { - $skinDir = dirname( dirname( dirname( __FILE__ ) ) ) . '/skins'; + $skinDir = dirname( dirname( __DIR__ ) ) . '/skins'; // All these files will be concatenated in sequence and loaded // as one file. @@ -103,6 +118,7 @@ class WebInstallerOutput { $cssFileNames = array( // Basically the "skins.vector" ResourceLoader module styles + 'common/shared.css', 'common/commonElements.css', 'common/commonContent.css', 'common/commonInterface.css', @@ -133,11 +149,12 @@ class WebInstallerOutput { if( $dir == 'rtl' ) { $css = CSSJanus::transform( $css, true ); } + return $css; } /** - * <link> to index.php?css=foobar for the <head> + * "<link>" to index.php?css=foobar for the "<head>" * @return String */ private function getCssUrl( ) { @@ -221,7 +238,6 @@ class WebInstallerOutput { <meta name="robots" content="noindex, nofollow" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <title><?php $this->outputTitle(); ?></title> - <?php echo Html::linkedStyle( '../skins/common/shared.css' ) . "\n"; ?> <?php echo $this->getCssUrl() . "\n"; ?> <?php echo Html::inlineScript( "var dbTypes = " . Xml::encodeJsVar( $dbTypes ) ) . "\n"; ?> <?php echo $this->getJQuery() . "\n"; ?> @@ -258,7 +274,7 @@ class WebInstallerOutput { </div> <div class="portal"><div class="body"> <?php - echo $this->parent->parse( wfMsgNoTrans( 'config-sidebar' ), true ); + echo $this->parent->parse( wfMessage( 'config-sidebar' )->plain(), true ); ?> </div></div> </div> @@ -286,7 +302,7 @@ class WebInstallerOutput { public function outputTitle() { global $wgVersion; - echo htmlspecialchars( wfMsg( 'config-title', $wgVersion ) ); + echo wfMessage( 'config-title', $wgVersion )->escaped(); } public function getJQuery() { diff --git a/includes/installer/WebInstallerPage.php b/includes/installer/WebInstallerPage.php index ff8185a1..a193afb7 100644 --- a/includes/installer/WebInstallerPage.php +++ b/includes/installer/WebInstallerPage.php @@ -2,6 +2,21 @@ /** * Base code for web installer pages. * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Deployment */ @@ -36,6 +51,7 @@ abstract class WebInstallerPage { * Is this a slow-running page in the installer? If so, WebInstaller will * set_time_limit(0) before calling execute(). Right now this only applies * to Install and Upgrade pages + * @return bool */ public function isSlow() { return false; @@ -68,13 +84,13 @@ abstract class WebInstallerPage { if ( $continue ) { // Fake submit button for enter keypress (bug 26267) - $s .= Xml::submitButton( wfMsg( "config-$continue" ), + $s .= Xml::submitButton( wfMessage( "config-$continue" )->text(), array( 'name' => "enter-$continue", 'style' => 'visibility:hidden;overflow:hidden;width:1px;margin:0' ) ) . "\n"; } if ( $back ) { - $s .= Xml::submitButton( wfMsg( "config-$back" ), + $s .= Xml::submitButton( wfMessage( "config-$back" )->text(), array( 'name' => "submit-$back", 'tabindex' => $this->parent->nextTabIndex() @@ -82,7 +98,7 @@ abstract class WebInstallerPage { } if ( $continue ) { - $s .= Xml::submitButton( wfMsg( "config-$continue" ), + $s .= Xml::submitButton( wfMessage( "config-$continue" )->text(), array( 'name' => "submit-$continue", 'tabindex' => $this->parent->nextTabIndex(), @@ -117,7 +133,7 @@ abstract class WebInstallerPage { * @return string */ protected function getFieldsetStart( $legend ) { - return "\n<fieldset><legend>" . wfMsgHtml( $legend ) . "</legend>\n"; + return "\n<fieldset><legend>" . wfMessage( $legend )->escaped() . "</legend>\n"; } /** @@ -161,7 +177,7 @@ class WebInstaller_Language extends WebInstallerPage { $userLang = $r->getVal( 'uselang' ); $contLang = $r->getVal( 'ContLang' ); - $languages = Language::getLanguageNames(); + $languages = Language::fetchLanguageNames(); $lifetime = intval( ini_get( 'session.gc_maxlifetime' ) ); if ( !$lifetime ) { $lifetime = 1440; // PHP default @@ -216,7 +232,7 @@ class WebInstaller_Language extends WebInstallerPage { } /** - * Get a <select> for selecting languages. + * Get a "<select>" for selecting languages. * * @param $name * @param $label @@ -232,7 +248,7 @@ class WebInstaller_Language extends WebInstallerPage { $s .= Html::openElement( 'select', array( 'id' => $name, 'name' => $name, 'tabindex' => $this->parent->nextTabIndex() ) ) . "\n"; - $languages = Language::getLanguageNames(); + $languages = Language::fetchLanguageNames(); ksort( $languages ); foreach ( $languages as $code => $lang ) { if ( isset( $wgDummyLanguageCodes[$code] ) ) continue; @@ -279,8 +295,8 @@ class WebInstaller_ExistingWiki extends WebInstallerPage { } $this->startForm(); $this->addHTML( $this->parent->getInfoBox( - wfMsgNoTrans( 'config-upgrade-key-missing', - "<pre dir=\"ltr\">\$wgUpgradeKey = '" . $this->getVar( 'wgUpgradeKey' ) . "';</pre>" ) + wfMessage( 'config-upgrade-key-missing', "<pre dir=\"ltr\">\$wgUpgradeKey = '" . + $this->getVar( 'wgUpgradeKey' ) . "';</pre>" )->plain() ) ); $this->endForm( 'continue' ); return 'output'; @@ -317,7 +333,7 @@ class WebInstaller_ExistingWiki extends WebInstallerPage { protected function showKeyForm() { $this->startForm(); $this->addHTML( - $this->parent->getInfoBox( wfMsgNoTrans( 'config-localsettings-upgrade' ) ). + $this->parent->getInfoBox( wfMessage( 'config-localsettings-upgrade' )->plain() ). '<br />' . $this->parent->getTextBox( array( 'var' => 'wgUpgradeKey', @@ -341,7 +357,7 @@ class WebInstaller_ExistingWiki extends WebInstallerPage { /** * Initiate an upgrade of the existing database - * @param $vars Variables from LocalSettings.php and AdminSettings.php + * @param $vars array Variables from LocalSettings.php and AdminSettings.php * @return Status */ protected function handleExistingUpgrade( $vars ) { @@ -394,13 +410,13 @@ class WebInstaller_Welcome extends WebInstallerPage { return 'continue'; } } - $this->parent->output->addWikiText( wfMsgNoTrans( 'config-welcome' ) ); + $this->parent->output->addWikiText( wfMessage( 'config-welcome' )->plain() ); $status = $this->parent->doEnvironmentChecks(); if ( $status->isGood() ) { $this->parent->output->addHTML( '<span class="success-message">' . - wfMsgHtml( 'config-env-good' ) . '</span>' ); - $this->parent->output->addWikiText( wfMsgNoTrans( 'config-copyright', - SpecialVersion::getCopyrightAndAuthorList() ) ); + wfMessage( 'config-env-good' )->escaped() . '</span>' ); + $this->parent->output->addWikiText( wfMessage( 'config-copyright', + SpecialVersion::getCopyrightAndAuthorList() )->plain() ); $this->startForm(); $this->endForm(); } else { @@ -438,10 +454,10 @@ class WebInstaller_DBConnect extends WebInstallerPage { $dbSupport = ''; foreach( $this->parent->getDBTypes() as $type ) { $link = DatabaseBase::factory( $type )->getSoftwareLink(); - $dbSupport .= wfMsgNoTrans( "config-support-$type", $link ) . "\n"; + $dbSupport .= wfMessage( "config-support-$type", $link )->plain() . "\n"; } $this->addHTML( $this->parent->getInfoBox( - wfMsg( 'config-support-info', $dbSupport ) ) ); + wfMessage( 'config-support-info', trim( $dbSupport ) )->text() ) ); foreach ( $this->parent->getVar( '_CompiledDBs' ) as $type ) { $installer = $this->parent->getDBInstaller( $type ); @@ -460,7 +476,7 @@ class WebInstaller_DBConnect extends WebInstallerPage { $settings .= Html::openElement( 'div', array( 'id' => 'DB_wrapper_' . $type, 'class' => 'dbWrapper' ) ) . - Html::element( 'h3', array(), wfMsg( 'config-header-' . $type ) ) . + Html::element( 'h3', array(), wfMessage( 'config-header-' . $type )->text() ) . $installer->getConnectForm() . "</div>\n"; } @@ -539,7 +555,7 @@ class WebInstaller_Upgrade extends WebInstallerPage { $this->startForm(); $this->addHTML( $this->parent->getInfoBox( - wfMsgNoTrans( 'config-can-upgrade', $GLOBALS['wgVersion'] ) ) ); + wfMessage( 'config-can-upgrade', $GLOBALS['wgVersion'] )->plain() ) ); $this->endForm(); } @@ -554,11 +570,11 @@ class WebInstaller_Upgrade extends WebInstallerPage { $this->parent->disableLinkPopups(); $this->addHTML( $this->parent->getInfoBox( - wfMsgNoTrans( $msg, + wfMessage( $msg, $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/index' . $this->getVar( 'wgScriptExtension' ) - ), 'tick-32.png' + )->plain(), 'tick-32.png' ) ); $this->parent->restoreLinkPopups(); @@ -619,7 +635,10 @@ class WebInstaller_Name extends WebInstallerPage { // Set wgMetaNamespace to something valid before we show the form. // $wgMetaNamespace defaults to $wgSiteName which is 'MediaWiki' $metaNS = $this->getVar( 'wgMetaNamespace' ); - $this->setVar( 'wgMetaNamespace', wfMsgForContent( 'config-ns-other-default' ) ); + $this->setVar( + 'wgMetaNamespace', + wfMessage( 'config-ns-other-default' )->inContentLanguage()->text() + ); $this->addHTML( $this->parent->getTextBox( array( @@ -667,7 +686,7 @@ class WebInstaller_Name extends WebInstallerPage { 'help' => $this->parent->getHelpBox( 'config-subscribe-help' ) ) ) . $this->getFieldSetEnd() . - $this->parent->getInfoBox( wfMsg( 'config-almost-done' ) ) . + $this->parent->getInfoBox( wfMessage( 'config-almost-done' )->text() ) . $this->parent->getRadioSet( array( 'var' => '_SkipOptional', 'itemLabelPrefix' => 'config-optional-', @@ -705,7 +724,7 @@ class WebInstaller_Name extends WebInstallerPage { $name = preg_replace( '/__+/', '_', $name ); $name = ucfirst( trim( $name, '_' ) ); } elseif ( $nsType == 'generic' ) { - $name = wfMsg( 'config-ns-generic' ); + $name = wfMessage( 'config-ns-generic' )->text(); } else { // other $name = $this->getVar( 'wgMetaNamespace' ); } @@ -817,7 +836,7 @@ class WebInstaller_Options extends WebInstallerPage { 'itemLabelPrefix' => 'config-profile-', 'values' => array_keys( $this->parent->rightsProfiles ), ) ) . - $this->parent->getInfoBox( wfMsgNoTrans( 'config-profile-help' ) ) . + $this->parent->getInfoBox( wfMessage( 'config-profile-help' )->plain() ) . # Licensing $this->parent->getRadioSet( array( @@ -1030,7 +1049,7 @@ class WebInstaller_Options extends WebInstallerPage { 'href' => $this->getCCPartnerUrl(), 'onclick' => $expandJs, ), - wfMsg( 'config-cc-again' ) + wfMessage( 'config-cc-again' )->text() ) . "</p>\n" . "<script type=\"text/javascript\">\n" . @@ -1076,7 +1095,7 @@ class WebInstaller_Options extends WebInstallerPage { if ( isset( $entry['text'] ) ) { $this->setVar( 'wgRightsText', $entry['text'] ); } else { - $this->setVar( 'wgRightsText', wfMsg( 'config-license-' . $code ) ); + $this->setVar( 'wgRightsText', wfMessage( 'config-license-' . $code )->text() ); } $this->setVar( 'wgRightsUrl', $entry['url'] ); $this->setVar( 'wgRightsIcon', $entry['icon'] ); @@ -1149,14 +1168,14 @@ class WebInstaller_Install extends WebInstallerPage { $this->endForm( $continue, $back ); } else { $this->startForm(); - $this->addHTML( $this->parent->getInfoBox( wfMsgNoTrans( 'config-install-begin' ) ) ); + $this->addHTML( $this->parent->getInfoBox( wfMessage( 'config-install-begin' )->plain() ) ); $this->endForm(); } return true; } public function startStage( $step ) { - $this->addHTML( "<li>" . wfMsgHtml( "config-install-$step" ) . wfMsg( 'ellipsis') ); + $this->addHTML( "<li>" . wfMessage( "config-install-$step" )->escaped() . wfMessage( 'ellipsis')->escaped() ); if ( $step == 'extension-tables' ) { $this->startLiveBox(); } @@ -1171,7 +1190,7 @@ class WebInstaller_Install extends WebInstallerPage { $this->endLiveBox(); } $msg = $status->isOk() ? 'config-install-step-done' : 'config-install-step-failed'; - $html = wfMsgHtml( 'word-separator' ) . wfMsgHtml( $msg ); + $html = wfMessage( 'word-separator' )->escaped() . wfMessage( $msg )->escaped(); if ( !$status->isOk() ) { $html = "<span class=\"error\">$html</span>"; } @@ -1203,13 +1222,13 @@ class WebInstaller_Complete extends WebInstallerPage { $this->parent->disableLinkPopups(); $this->addHTML( $this->parent->getInfoBox( - wfMsgNoTrans( 'config-install-done', + wfMessage( 'config-install-done', $lsUrl, $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/index' . $this->getVar( 'wgScriptExtension' ), '<downloadlink/>' - ), 'tick-32.png' + )->plain(), 'tick-32.png' ) ); $this->parent->restoreLinkPopups(); @@ -1230,7 +1249,7 @@ class WebInstaller_Restart extends WebInstallerPage { } $this->startForm(); - $s = $this->parent->getWarningBox( wfMsgNoTrans( 'config-help-restart' ) ); + $s = $this->parent->getWarningBox( wfMessage( 'config-help-restart' )->plain() ); $this->addHTML( $s ); $this->endForm( 'restart' ); } @@ -1250,7 +1269,11 @@ abstract class WebInstaller_Document extends WebInstallerPage { } public function getFileContents() { - return file_get_contents( dirname( __FILE__ ) . '/../../' . $this->getFileName() ); + $file = __DIR__ . '/../../' . $this->getFileName(); + if( ! file_exists( $file ) ) { + return wfMessage( 'config-nofile', $file )->plain(); + } + return file_get_contents( $file ); } } @@ -1260,7 +1283,15 @@ class WebInstaller_Readme extends WebInstaller_Document { } class WebInstaller_ReleaseNotes extends WebInstaller_Document { - protected function getFileName() { return 'RELEASE-NOTES'; } + protected function getFileName() { + global $wgVersion; + + if(! preg_match( '/^(\d+)\.(\d+).*/i', $wgVersion, $result ) ) { + throw new MWException('Variable $wgVersion has an invalid value.'); + } + + return 'RELEASE-NOTES-' . $result[1] . '.' . $result[2]; + } } class WebInstaller_UpgradeDoc extends WebInstaller_Document { diff --git a/includes/interwiki/Interwiki.php b/includes/interwiki/Interwiki.php index 3aaa1c52..eacf9a87 100644 --- a/includes/interwiki/Interwiki.php +++ b/includes/interwiki/Interwiki.php @@ -1,7 +1,23 @@ <?php /** + * Interwiki table entry. + * + * 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 - * Interwiki table entry */ /** @@ -42,7 +58,7 @@ class Interwiki { * Fetch an Interwiki object * * @param $prefix String: interwiki prefix to use - * @return Interwiki Object, or null if not valid + * @return Interwiki|null|bool */ static public function fetch( $prefix ) { global $wgContLang; @@ -130,6 +146,7 @@ class Interwiki { $value = ''; } + return $value; } @@ -164,7 +181,7 @@ class Interwiki { $db = wfGetDB( DB_SLAVE ); - $row = $db->fetchRow( $db->select( 'interwiki', '*', array( 'iw_prefix' => $prefix ), + $row = $db->fetchRow( $db->select( 'interwiki', self::selectFields(), array( 'iw_prefix' => $prefix ), __METHOD__ ) ); $iw = Interwiki::loadFromArray( $row ); if ( $iw ) { @@ -288,7 +305,7 @@ class Interwiki { } $res = $db->select( 'interwiki', - array( 'iw_prefix', 'iw_url', 'iw_api', 'iw_wikiid', 'iw_local', 'iw_trans' ), + self::selectFields(), $where, __METHOD__, array( 'ORDER BY' => 'iw_prefix' ) ); $retval = array(); @@ -389,4 +406,20 @@ class Interwiki { $msg = wfMessage( 'interwiki-desc-' . $this->mPrefix )->inContentLanguage(); return !$msg->exists() ? '' : $msg; } + + /** + * Return the list of interwiki fields that should be selected to create + * a new interwiki object. + * @return array + */ + public static function selectFields() { + return array( + 'iw_prefix', + 'iw_url', + 'iw_api', + 'iw_wikiid', + 'iw_local', + 'iw_trans' + ); + } } diff --git a/includes/job/DoubleRedirectJob.php b/includes/job/DoubleRedirectJob.php index 2b7cd7c8..08af9975 100644 --- a/includes/job/DoubleRedirectJob.php +++ b/includes/job/DoubleRedirectJob.php @@ -1,6 +1,21 @@ <?php /** - * Job to fix double redirects after moving a page + * Job to fix double redirects after moving a page. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup JobQueue @@ -21,7 +36,7 @@ class DoubleRedirectJob extends Job { /** * Insert jobs into the job queue to fix redirects to the given title - * @param $reason String: the reason for the fix, see message double-redirect-fixed-<reason> + * @param $reason String: the reason for the fix, see message "double-redirect-fixed-<reason>" * @param $redirTitle Title: the title which has changed, redirects pointing to this title are fixed * @param $destTitle bool Not used */ @@ -74,7 +89,7 @@ class DoubleRedirectJob extends Job { return false; } - $targetRev = Revision::newFromTitle( $this->title ); + $targetRev = Revision::newFromTitle( $this->title, false, Revision::READ_LATEST ); if ( !$targetRev ) { wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" ); return true; @@ -126,8 +141,9 @@ class DoubleRedirectJob extends Job { $oldUser = $wgUser; $wgUser = $this->getUser(); $article = WikiPage::factory( $this->title ); - $reason = wfMsgForContent( 'double-redirect-fixed-' . $this->reason, - $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() ); + $reason = wfMessage( 'double-redirect-fixed-' . $this->reason, + $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() + )->inContentLanguage()->text(); $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() ); $wgUser = $oldUser; @@ -139,7 +155,7 @@ class DoubleRedirectJob extends Job { * * @param $title Title * - * @return false if the specified title is not a redirect, or if it is a circular redirect + * @return bool if the specified title is not a redirect, or if it is a circular redirect */ public static function getFinalDestination( $title ) { $dbw = wfGetDB( DB_MASTER ); @@ -179,7 +195,7 @@ class DoubleRedirectJob extends Job { */ function getUser() { if ( !self::$user ) { - self::$user = User::newFromName( wfMsgForContent( 'double-redirect-fixer' ), false ); + self::$user = User::newFromName( wfMessage( 'double-redirect-fixer' )->inContentLanguage()->text(), false ); # FIXME: newFromName could return false on a badly configured wiki. if ( !self::$user->isLoggedIn() ) { self::$user->addToDatabase(); diff --git a/includes/job/EmaillingJob.php b/includes/job/EmaillingJob.php index 89b74a41..d3599882 100644 --- a/includes/job/EmaillingJob.php +++ b/includes/job/EmaillingJob.php @@ -2,6 +2,21 @@ /** * Old job for notification emails. * + * 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 JobQueue */ diff --git a/includes/job/EnotifNotifyJob.php b/includes/job/EnotifNotifyJob.php index eb154ece..b4c925e9 100644 --- a/includes/job/EnotifNotifyJob.php +++ b/includes/job/EnotifNotifyJob.php @@ -2,6 +2,21 @@ /** * Job for notification emails. * + * 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 JobQueue */ diff --git a/includes/job/JobQueue.php b/includes/job/Job.php index e7c66719..d777a5d4 100644 --- a/includes/job/JobQueue.php +++ b/includes/job/Job.php @@ -1,15 +1,26 @@ <?php /** - * Job queue base code + * Job queue base code. + * + * 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 * @defgroup JobQueue JobQueue */ -if ( !defined( 'MEDIAWIKI' ) ) { - die( "This file is part of MediaWiki, it is not a valid entry point\n" ); -} - /** * Class to both describe a background job and handle jobs. * @@ -56,7 +67,7 @@ abstract class Job { $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); + $dbw->begin( __METHOD__ ); $row = $dbw->selectRow( 'job', @@ -67,7 +78,7 @@ abstract class Job { ); if ( $row === false ) { - $dbw->commit(); + $dbw->commit( __METHOD__ ); wfProfileOut( __METHOD__ ); return false; } @@ -75,7 +86,7 @@ abstract class Job { /* Ensure we "own" this row */ $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); $affected = $dbw->affectedRows(); - $dbw->commit(); + $dbw->commit( __METHOD__ ); if ( $affected == 0 ) { wfProfileOut( __METHOD__ ); @@ -102,7 +113,6 @@ abstract class Job { * @return Job or false if there's no jobs */ static function pop( $offset = 0 ) { - global $wgJobTypesExcludedFromDefaultQueue; wfProfileIn( __METHOD__ ); $dbr = wfGetDB( DB_SLAVE ); @@ -113,12 +123,9 @@ abstract class Job { NB: If random fetch previously was used, offset will always be ahead of few entries */ - $conditions = array(); - if ( count( $wgJobTypesExcludedFromDefaultQueue ) != 0 ) { - foreach ( $wgJobTypesExcludedFromDefaultQueue as $cmdType ) { - $conditions[] = "job_cmd != " . $dbr->addQuotes( $cmdType ); - } - } + + $conditions = self::defaultQueueConditions(); + $offset = intval( $offset ); $options = array( 'ORDER BY' => 'job_id', 'USE INDEX' => 'PRIMARY' ); @@ -146,13 +153,12 @@ abstract class Job { $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); $affected = $dbw->affectedRows(); - $dbw->commit(); if ( !$affected ) { // Failed, someone else beat us to it // Try getting a random row - $row = $dbw->selectRow( 'job', array( 'MIN(job_id) as minjob', - 'MAX(job_id) as maxjob' ), '1=1', __METHOD__ ); + $row = $dbw->selectRow( 'job', array( 'minjob' => 'MIN(job_id)', + 'maxjob' => 'MAX(job_id)' ), '1=1', __METHOD__ ); if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) { // No jobs to get wfProfileOut( __METHOD__ ); @@ -170,7 +176,6 @@ abstract class Job { // Delete the random row $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); $affected = $dbw->affectedRows(); - $dbw->commit(); if ( !$affected ) { // Random job gone before we exclusively deleted it @@ -186,6 +191,12 @@ abstract class Job { $namespace = $row->job_namespace; $dbkey = $row->job_title; $title = Title::makeTitleSafe( $namespace, $dbkey ); + + if ( is_null( $title ) ) { + wfProfileOut( __METHOD__ ); + return false; + } + $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id ); // Remove any duplicates it may have later in the queue @@ -200,11 +211,12 @@ abstract class Job { * * @param $command String: Job command * @param $title Title: Associated title - * @param $params Array: Job parameters + * @param $params Array|bool: Job parameters * @param $id Int: Job identifier + * @throws MWException * @return Job */ - static function factory( $command, $title, $params = false, $id = 0 ) { + static function factory( $command, Title $title, $params = false, $id = 0 ) { global $wgJobClasses; if( isset( $wgJobClasses[$command] ) ) { $class = $wgJobClasses[$command]; @@ -260,16 +272,16 @@ abstract class Job { $rows[] = $job->insertFields(); if ( count( $rows ) >= 50 ) { # Do a small transaction to avoid slave lag - $dbw->begin(); + $dbw->begin( __METHOD__ ); $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); - $dbw->commit(); + $dbw->commit( __METHOD__ ); $rows = array(); } } if ( $rows ) { // last chunk - $dbw->begin(); + $dbw->begin( __METHOD__ ); $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); - $dbw->commit(); + $dbw->commit( __METHOD__ ); } wfIncrStats( 'job-insert', count( $jobs ) ); } @@ -302,6 +314,27 @@ abstract class Job { wfIncrStats( 'job-insert', count( $jobs ) ); } + + /** + * SQL conditions to apply on most JobQueue queries + * + * Whenever we exclude jobs types from the default queue, we want to make + * sure that queries to the job queue actually ignore them. + * + * @return array SQL conditions suitable for Database:: methods + */ + static function defaultQueueConditions( ) { + global $wgJobTypesExcludedFromDefaultQueue; + $conditions = array(); + if ( count( $wgJobTypesExcludedFromDefaultQueue ) > 0 ) { + $dbr = wfGetDB( DB_SLAVE ); + foreach ( $wgJobTypesExcludedFromDefaultQueue as $cmdType ) { + $conditions[] = "job_cmd != " . $dbr->addQuotes( $cmdType ); + } + } + return $conditions; + } + /*------------------------------------------------------------------------- * Non-static functions *------------------------------------------------------------------------*/ @@ -309,8 +342,8 @@ abstract class Job { /** * @param $command * @param $title - * @param $params array - * @param int $id + * @param $params array|bool + * @param $id int */ function __construct( $command, $title, $params = false, $id = 0 ) { $this->command = $command; @@ -368,11 +401,12 @@ abstract class Job { $fields = $this->insertFields(); unset( $fields['job_id'] ); + unset( $fields['job_timestamp'] ); $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); + $dbw->begin( __METHOD__ ); $dbw->delete( 'job', $fields, __METHOD__ ); $affected = $dbw->affectedRows(); - $dbw->commit(); + $dbw->commit( __METHOD__ ); if ( $affected ) { wfIncrStats( 'job-dup-delete', $affected ); } diff --git a/includes/job/RefreshLinksJob.php b/includes/job/RefreshLinksJob.php index 1aa206f0..b23951c6 100644 --- a/includes/job/RefreshLinksJob.php +++ b/includes/job/RefreshLinksJob.php @@ -2,6 +2,21 @@ /** * Job to update links for a given title. * + * 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 JobQueue */ @@ -22,7 +37,6 @@ class RefreshLinksJob extends Job { * @return boolean success */ function run() { - global $wgParser, $wgContLang; wfProfileIn( __METHOD__ ); $linkCache = LinkCache::singleton(); @@ -34,24 +48,41 @@ class RefreshLinksJob extends Job { return false; } - $revision = Revision::newFromTitle( $this->title ); + # Wait for the DB of the current/next slave DB handle to catch up to the master. + # This way, we get the correct page_latest for templates or files that just changed + # milliseconds ago, having triggered this job to begin with. + if ( isset( $this->params['masterPos'] ) ) { + wfGetLB()->waitFor( $this->params['masterPos'] ); + } + + $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL ); if ( !$revision ) { - $this->error = 'refreshLinks: Article not found "' . $this->title->getPrefixedDBkey() . '"'; + $this->error = 'refreshLinks: Article not found "' . + $this->title->getPrefixedDBkey() . '"'; wfProfileOut( __METHOD__ ); - return false; + return false; // XXX: what if it was just deleted? } - wfProfileIn( __METHOD__.'-parse' ); - $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() ); - wfProfileOut( __METHOD__.'-parse' ); - wfProfileIn( __METHOD__.'-update' ); - $update = new LinksUpdate( $this->title, $parserOutput, false ); - $update->doUpdate(); - wfProfileOut( __METHOD__.'-update' ); + self::runForTitleInternal( $this->title, $revision, __METHOD__ ); + wfProfileOut( __METHOD__ ); return true; } + + public static function runForTitleInternal( Title $title, Revision $revision, $fname ) { + global $wgParser, $wgContLang; + + wfProfileIn( $fname . '-parse' ); + $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); + $parserOutput = $wgParser->parse( + $revision->getText(), $title, $options, true, true, $revision->getId() ); + wfProfileOut( $fname . '-parse' ); + + wfProfileIn( $fname . '-update' ); + $updates = $parserOutput->getSecondaryDataUpdates( $title, false ); + DataUpdate::runUpdates( $updates ); + wfProfileOut( $fname . '-update' ); + } } /** @@ -61,6 +92,7 @@ class RefreshLinksJob extends Job { * @ingroup JobQueue */ class RefreshLinksJob2 extends Job { + const MAX_TITLES_RUN = 10; function __construct( $title, $params, $id = 0 ) { parent::__construct( 'refreshLinks2', $title, $params, $id ); @@ -71,60 +103,100 @@ class RefreshLinksJob2 extends Job { * @return boolean success */ function run() { - global $wgParser, $wgContLang; - wfProfileIn( __METHOD__ ); $linkCache = LinkCache::singleton(); $linkCache->clear(); - if( is_null( $this->title ) ) { + if ( is_null( $this->title ) ) { $this->error = "refreshLinks2: Invalid title"; wfProfileOut( __METHOD__ ); return false; - } - if( !isset($this->params['start']) || !isset($this->params['end']) ) { + } elseif ( !isset( $this->params['start'] ) || !isset( $this->params['end'] ) ) { $this->error = "refreshLinks2: Invalid params"; wfProfileOut( __METHOD__ ); return false; } + // Back compat for pre-r94435 jobs $table = isset( $this->params['table'] ) ? $this->params['table'] : 'templatelinks'; - $titles = $this->title->getBacklinkCache()->getLinks( - $table, $this->params['start'], $this->params['end']); - - # Not suitable for page load triggered job running! - # Gracefully switch to refreshLinks jobs if this happens. - if( php_sapi_name() != 'cli' ) { + + // Avoid slave lag when fetching templates + if ( isset( $this->params['masterPos'] ) ) { + $masterPos = $this->params['masterPos']; + } elseif ( wfGetLB()->getServerCount() > 1 ) { + $masterPos = wfGetLB()->getMasterPos(); + } else { + $masterPos = false; + } + + $titles = $this->title->getBacklinkCache()->getLinks( + $table, $this->params['start'], $this->params['end'] ); + + if ( $titles->count() > self::MAX_TITLES_RUN ) { + # We don't want to parse too many pages per job as it can starve other jobs. + # If there are too many pages to parse, break this up into smaller jobs. By passing + # in the master position here we can cut down on the time spent waiting for slaves to + # catch up by the runners handling these jobs since time will have passed between now + # and when they pop these jobs off the queue. + $start = 0; // batch start + $end = 0; // batch end + $bsize = 0; // batch size + $first = true; // first of batch + $jobs = array(); + foreach ( $titles as $title ) { + $start = $first ? $title->getArticleId() : $start; + $end = $title->getArticleId(); + $first = false; + if ( ++$bsize >= self::MAX_TITLES_RUN ) { + $jobs[] = new RefreshLinksJob2( $this->title, array( + 'table' => $table, + 'start' => $start, + 'end' => $end, + 'masterPos' => $masterPos + ) ); + $first = true; + $start = $end = $bsize = 0; + } + } + if ( $bsize > 0 ) { // group remaining pages into a job + $jobs[] = new RefreshLinksJob2( $this->title, array( + 'table' => $table, + 'start' => $start, + 'end' => $end, + 'masterPos' => $masterPos + ) ); + } + Job::batchInsert( $jobs ); + } elseif ( php_sapi_name() != 'cli' ) { + # Not suitable for page load triggered job running! + # Gracefully switch to refreshLinks jobs if this happens. $jobs = array(); foreach ( $titles as $title ) { - $jobs[] = new RefreshLinksJob( $title, '' ); + $jobs[] = new RefreshLinksJob( $title, array( 'masterPos' => $masterPos ) ); } Job::batchInsert( $jobs ); - - wfProfileOut( __METHOD__ ); - return true; - } - $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - # Re-parse each page that transcludes this page and update their tracking links... - foreach ( $titles as $title ) { - $revision = Revision::newFromTitle( $title ); - if ( !$revision ) { - $this->error = 'refreshLinks: Article not found "' . $title->getPrefixedDBkey() . '"'; - wfProfileOut( __METHOD__ ); - return false; + } else { + # Wait for the DB of the current/next slave DB handle to catch up to the master. + # This way, we get the correct page_latest for templates or files that just changed + # milliseconds ago, having triggered this job to begin with. + if ( $masterPos ) { + wfGetLB()->waitFor( $masterPos ); + } + # Re-parse each page that transcludes this page and update their tracking links... + foreach ( $titles as $title ) { + $revision = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); + if ( !$revision ) { + $this->error = 'refreshLinks: Article not found "' . + $title->getPrefixedDBkey() . '"'; + continue; // skip this page + } + RefreshLinksJob::runForTitleInternal( $title, $revision, __METHOD__ ); + wfWaitForSlaves(); } - wfProfileIn( __METHOD__.'-parse' ); - $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() ); - wfProfileOut( __METHOD__.'-parse' ); - wfProfileIn( __METHOD__.'-update' ); - $update = new LinksUpdate( $title, $parserOutput, false ); - $update->doUpdate(); - wfProfileOut( __METHOD__.'-update' ); - wfWaitForSlaves(); } - wfProfileOut( __METHOD__ ); + wfProfileOut( __METHOD__ ); return true; } } diff --git a/includes/job/UploadFromUrlJob.php b/includes/job/UploadFromUrlJob.php index 26f6e4ba..e06f68e4 100644 --- a/includes/job/UploadFromUrlJob.php +++ b/includes/job/UploadFromUrlJob.php @@ -2,6 +2,21 @@ /** * Job for asynchronous upload-by-url. * + * 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 JobQueue */ @@ -67,10 +82,10 @@ class UploadFromUrlJob extends Job { if ( $this->params['leaveMessage'] ) { $this->user->leaveUserMessage( - wfMsg( 'upload-warning-subj' ), - wfMsg( 'upload-warning-msg', + wfMessage( 'upload-warning-subj' )->text(), + wfMessage( 'upload-warning-msg', $key, - $this->params['url'] ) + $this->params['url'] )->text() ); } else { wfSetupSession( $this->params['sessionId'] ); @@ -104,17 +119,17 @@ class UploadFromUrlJob extends Job { protected function leaveMessage( $status ) { if ( $this->params['leaveMessage'] ) { if ( $status->isGood() ) { - $this->user->leaveUserMessage( wfMsg( 'upload-success-subj' ), - wfMsg( 'upload-success-msg', + $this->user->leaveUserMessage( wfMessage( 'upload-success-subj' )->text(), + wfMessage( 'upload-success-msg', $this->upload->getTitle()->getText(), $this->params['url'] - ) ); + )->text() ); } else { - $this->user->leaveUserMessage( wfMsg( 'upload-failure-subj' ), - wfMsg( 'upload-failure-msg', + $this->user->leaveUserMessage( wfMessage( 'upload-failure-subj' )->text(), + wfMessage( 'upload-failure-msg', $status->getWikiText(), $this->params['url'] - ) ); + )->text() ); } } else { wfSetupSession( $this->params['sessionId'] ); diff --git a/includes/json/FormatJson.php b/includes/json/FormatJson.php index f7373e45..f67700c9 100644 --- a/includes/json/FormatJson.php +++ b/includes/json/FormatJson.php @@ -1,15 +1,26 @@ <?php /** - * Simple wrapper for json_econde and json_decode that falls back on Services_JSON class + * Simple wrapper for json_econde and json_decode that falls back on Services_JSON class. + * + * 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 */ -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} - -require_once dirname( __FILE__ ) . '/Services_JSON.php'; +require_once __DIR__ . '/Services_JSON.php'; /** * JSON formatter wrapper class @@ -30,14 +41,11 @@ class FormatJson { * @return string */ public static function encode( $value, $isHtml = false ) { - // Some versions of PHP have a broken json_encode, see PHP bug - // 46944. Test encoding an affected character (U+20000) to - // avoid this. - if ( !function_exists( 'json_encode' ) || $isHtml || strtolower( json_encode( "\xf0\xa0\x80\x80" ) ) != '"\ud840\udc00"' ) { + if ( !function_exists( 'json_encode' ) || ( $isHtml && version_compare( PHP_VERSION, '5.4.0', '<' ) ) ) { $json = new Services_JSON(); return $json->encode( $value, $isHtml ); } else { - return json_encode( $value ); + return json_encode( $value, $isHtml ? JSON_PRETTY_PRINT : 0 ); } } @@ -49,7 +57,7 @@ class FormatJson { * * @return Mixed: the value encoded in json in appropriate PHP type. * Values true, false and null (case-insensitive) are returned as true, false - * and &null; respectively. &null; is returned if the json cannot be + * and "&null;" respectively. "&null;" is returned if the json cannot be * decoded or if the encoded data is deeper than the recursion limit. */ public static function decode( $value, $assoc = false ) { diff --git a/includes/json/Services_JSON.php b/includes/json/Services_JSON.php index b2090dce..398ed6a2 100644 --- a/includes/json/Services_JSON.php +++ b/includes/json/Services_JSON.php @@ -826,6 +826,7 @@ class Services_JSON /** * @todo Ultimately, this should just call PEAR::isError() + * @return bool */ function isError($data, $code = null) { diff --git a/includes/libs/CSSJanus.php b/includes/libs/CSSJanus.php index c8fc296b..4ebbc497 100644 --- a/includes/libs/CSSJanus.php +++ b/includes/libs/CSSJanus.php @@ -1,5 +1,7 @@ <?php /** + * PHP port of CSSJanus. + * * 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 @@ -15,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @file */ /** @@ -122,7 +125,7 @@ class CSSJanus { * @param $css String: stylesheet to transform * @param $swapLtrRtlInURL Boolean: If true, swap 'ltr' and 'rtl' in URLs * @param $swapLeftRightInURL Boolean: If true, swap 'left' and 'right' in URLs - * @return Transformed stylesheet + * @return string Transformed stylesheet */ public static function transform( $css, $swapLtrRtlInURL = false, $swapLeftRightInURL = false ) { // We wrap tokens in ` , not ~ like the original implementation does. @@ -265,10 +268,17 @@ class CSSJanus { * @return string */ private static function fixBackgroundPosition( $css ) { - $css = preg_replace_callback( self::$patterns['bg_horizontal_percentage'], + $replaced = preg_replace_callback( self::$patterns['bg_horizontal_percentage'], array( 'self', 'calculateNewBackgroundPosition' ), $css ); - $css = preg_replace_callback( self::$patterns['bg_horizontal_percentage_x'], + if ( $replaced !== null ) { + // Check for null; sometimes preg_replace_callback() returns null here for some weird reason + $css = $replaced; + } + $replaced = preg_replace_callback( self::$patterns['bg_horizontal_percentage_x'], array( 'self', 'calculateNewBackgroundPosition' ), $css ); + if ( $replaced !== null ) { + $css = $replaced; + } return $css; } diff --git a/includes/libs/CSSMin.php b/includes/libs/CSSMin.php index 4f4b28bb..fc75cdcc 100644 --- a/includes/libs/CSSMin.php +++ b/includes/libs/CSSMin.php @@ -1,5 +1,7 @@ <?php /** + * Minification of CSS stylesheets. + * * Copyright 2010 Wikimedia Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,12 +14,6 @@ * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS * OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. - */ - -/** - * Transforms CSS data - * - * This class provides minification, URL remapping, URL extracting, and data-URL embedding. * * @file * @version 0.1.1 -- 2010-09-11 @@ -25,6 +21,12 @@ * @copyright Copyright 2010 Wikimedia Foundation * @license http://www.apache.org/licenses/LICENSE-2.0 */ + +/** + * Transforms CSS data + * + * This class provides minification, URL remapping, URL extracting, and data-URL embedding. + */ class CSSMin { /* Constants */ @@ -150,6 +152,13 @@ class CSSMin { $offset = $match[0][1] + strlen( $match[0][0] ) + $lengthIncrease; continue; } + + // Guard against double slashes, because "some/remote/../foo.png" + // resolves to "some/remote/foo.png" on (some?) clients (bug 27052). + if ( substr( $remote, -1 ) == '/' ) { + $remote = substr( $remote, 0, -1 ); + } + // Shortcuts $embed = $match['embed'][0]; $pre = $match['pre'][0]; @@ -157,10 +166,9 @@ class CSSMin { $query = $match['query'][0]; $url = "{$remote}/{$match['file'][0]}"; $file = "{$local}/{$match['file'][0]}"; - // bug 27052 - Guard against double slashes, because foo//../bar - // apparently resolves to foo/bar on (some?) clients - $url = preg_replace( '#([^:])//+#', '\1/', $url ); + $replacement = false; + if ( $local !== false && file_exists( $file ) ) { // Add version parameter as a time-stamp in ISO 8601 format, // using Z for the timezone, meaning GMT diff --git a/includes/libs/GenericArrayObject.php b/includes/libs/GenericArrayObject.php new file mode 100644 index 00000000..b4b9d610 --- /dev/null +++ b/includes/libs/GenericArrayObject.php @@ -0,0 +1,244 @@ +<?php + +/** + * Extends ArrayObject and does two things: + * + * Allows for deriving classes to easily intercept additions + * and deletions for purposes such as additional indexing. + * + * Enforces the objects to be of a certain type, so this + * can be replied upon, much like if this had true support + * for generics, which sadly enough is not possible in PHP. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @since 1.20 + * + * @file + * @ingroup Diff + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class GenericArrayObject extends ArrayObject { + + /** + * Returns the name of an interface/class that the element should implement/extend. + * + * @since 1.20 + * + * @return string + */ + public abstract function getObjectType(); + + /** + * @see SiteList::getNewOffset() + * @since 1.20 + * @var integer + */ + protected $indexOffset = 0; + + /** + * Finds a new offset for when appending an element. + * The base class does this, so it would be better to integrate, + * but there does not appear to be any way to do this... + * + * @since 1.20 + * + * @return integer + */ + protected function getNewOffset() { + while ( true ) { + if ( !$this->offsetExists( $this->indexOffset ) ) { + return $this->indexOffset; + } + + $this->indexOffset++; + } + } + + /** + * Constructor. + * @see ArrayObject::__construct + * + * @since 1.20 + * + * @param null|array $input + * @param int $flags + * @param string $iterator_class + */ + public function __construct( $input = null, $flags = 0, $iterator_class = 'ArrayIterator' ) { + parent::__construct( array(), $flags, $iterator_class ); + + if ( !is_null( $input ) ) { + foreach ( $input as $offset => $value ) { + $this->offsetSet( $offset, $value ); + } + } + } + + /** + * @see ArrayObject::append + * + * @since 1.20 + * + * @param mixed $value + */ + public function append( $value ) { + $this->setElement( null, $value ); + } + + /** + * @see ArrayObject::offsetSet() + * + * @since 1.20 + * + * @param mixed $index + * @param mixed $value + */ + public function offsetSet( $index, $value ) { + $this->setElement( $index, $value ); + } + + /** + * Returns if the provided value has the same type as the elements + * that can be added to this ArrayObject. + * + * @since 1.20 + * + * @param mixed $value + * + * @return boolean + */ + protected function hasValidType( $value ) { + $class = $this->getObjectType(); + return $value instanceof $class; + } + + /** + * Method that actually sets the element and holds + * all common code needed for set operations, including + * type checking and offset resolving. + * + * If you want to do additional indexing or have code that + * otherwise needs to be executed whenever an element is added, + * you can overload @see preSetElement. + * + * @since 1.20 + * + * @param mixed $index + * @param mixed $value + * + * @throws InvalidArgumentException + */ + protected function setElement( $index, $value ) { + if ( !$this->hasValidType( $value ) ) { + throw new InvalidArgumentException( + 'Can only add ' . $this->getObjectType() . ' implementing objects to ' . get_called_class() . '.' + ); + } + + if ( is_null( $index ) ) { + $index = $this->getNewOffset(); + } + + if ( $this->preSetElement( $index, $value ) ) { + parent::offsetSet( $index, $value ); + } + } + + /** + * Gets called before a new element is added to the ArrayObject. + * + * At this point the index is always set (ie not null) and the + * value is always of the type returned by @see getObjectType. + * + * Should return a boolean. When false is returned the element + * does not get added to the ArrayObject. + * + * @since 1.20 + * + * @param integer|string $index + * @param mixed $value + * + * @return boolean + */ + protected function preSetElement( $index, $value ) { + return true; + } + + /** + * @see Serializable::serialize + * + * @since 1.20 + * + * @return string + */ + public function serialize() { + return serialize( $this->getSerializationData() ); + } + + /** + * Returns an array holding all the data that should go into serialization calls. + * This is intended to allow overloading without having to reimplement the + * behaviour of this base class. + * + * @since 1.20 + * + * @return array + */ + protected function getSerializationData() { + return array( + 'data' => $this->getArrayCopy(), + 'index' => $this->indexOffset, + ); + } + + /** + * @see Serializable::unserialize + * + * @since 1.20 + * + * @param string $serialization + * + * @return array + */ + public function unserialize( $serialization ) { + $serializationData = unserialize( $serialization ); + + foreach ( $serializationData['data'] as $offset => $value ) { + // Just set the element, bypassing checks and offset resolving, + // as these elements have already gone through this. + parent::offsetSet( $offset, $value ); + } + + $this->indexOffset = $serializationData['index']; + + return $serializationData; + } + + /** + * Returns if the ArrayObject has no elements. + * + * @since 1.20 + * + * @return boolean + */ + public function isEmpty() { + return $this->count() === 0; + } + +} diff --git a/includes/libs/HttpStatus.php b/includes/libs/HttpStatus.php index 2985c652..78d81803 100644 --- a/includes/libs/HttpStatus.php +++ b/includes/libs/HttpStatus.php @@ -1,5 +1,26 @@ <?php /** + * List of HTTP status codes. + * + * 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 + */ + +/** * @todo document */ class HttpStatus { diff --git a/includes/libs/IEContentAnalyzer.php b/includes/libs/IEContentAnalyzer.php index 01e72e68..cfc7f536 100644 --- a/includes/libs/IEContentAnalyzer.php +++ b/includes/libs/IEContentAnalyzer.php @@ -1,4 +1,10 @@ <?php +/** + * Simulation of Microsoft Internet Explorer's MIME type detection algorithm. + * + * @file + * @todo Define the exact license of this file. + */ /** * This class simulates Microsoft Internet Explorer's terribly broken and diff --git a/includes/libs/IEUrlExtension.php b/includes/libs/IEUrlExtension.php index e00e6663..e9cfa997 100644 --- a/includes/libs/IEUrlExtension.php +++ b/includes/libs/IEUrlExtension.php @@ -1,4 +1,24 @@ <?php +/** + * Checks for validity of requested URL's extension. + * + * 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 + */ /** * Internet Explorer derives a cache filename from a URL, and then in certain @@ -35,8 +55,8 @@ class IEUrlExtension { * * If the a variable is unset in $_SERVER, it should be unset in $vars. * - * @param $vars A subset of $_SERVER. - * @param $extWhitelist Extensions which are allowed, assumed harmless. + * @param $vars array A subset of $_SERVER. + * @param $extWhitelist array Extensions which are allowed, assumed harmless. * @return bool */ public static function areServerVarsBad( $vars, $extWhitelist = array() ) { @@ -73,7 +93,7 @@ class IEUrlExtension { * a potentially harmful file extension. * * @param $urlPart string The right-hand portion of a URL - * @param $extWhitelist An array of file extensions which may occur in this + * @param $extWhitelist array An array of file extensions which may occur in this * URL, and which should be allowed. * @return bool */ diff --git a/includes/libs/JavaScriptMinifier.php b/includes/libs/JavaScriptMinifier.php index baf93385..0b4be9ae 100644 --- a/includes/libs/JavaScriptMinifier.php +++ b/includes/libs/JavaScriptMinifier.php @@ -2,17 +2,19 @@ /** * JavaScript Minifier * + * @file + * @author Paul Copperman <paul.copperman@gmail.com> + * @license Choose any of Apache, MIT, GPL, LGPL + */ + +/** * This class is meant to safely minify javascript code, while leaving syntactically correct * programs intact. Other libraries, such as JSMin require a certain coding style to work * correctly. OTOH, libraries like jsminplus, that do parse the code correctly are rather * slow, because they construct a complete parse tree before outputting the code minified. * So this class is meant to allow arbitrary (but syntactically correct) input, while being * fast enough to be used for on-the-fly minifying. - * - * Author: Paul Copperman <paul.copperman@gmail.com> - * License: choose any of Apache, MIT, GPL, LGPL */ - class JavaScriptMinifier { /* Class constants */ diff --git a/includes/libs/jsminplus.php b/includes/libs/jsminplus.php index 8ed08d74..7c4e32bd 100644 --- a/includes/libs/jsminplus.php +++ b/includes/libs/jsminplus.php @@ -1,5 +1,4 @@ <?php - /** * JSMinPlus version 1.4 * @@ -25,6 +24,7 @@ * * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip * + * @file */ /* ***** BEGIN LICENSE BLOCK ***** diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php index 4aa6a826..37560d80 100644 --- a/includes/logging/LogEntry.php +++ b/includes/logging/LogEntry.php @@ -7,6 +7,21 @@ * - formatting log entries based on database fields * - user is now part of the action message * + * 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 * @author Niklas Laxström * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later @@ -97,6 +112,7 @@ abstract class LogEntryBase implements LogEntry { /** * Whether the parameters for this log are stored in new or * old format. + * @return bool */ public function isLegacy() { return false; @@ -344,7 +360,7 @@ class ManualLogEntry extends LogEntryBase { * * @since 1.19 * - * @param $parameters Associative array + * @param $parameters array Associative array */ public function setParameters( $parameters ) { $this->parameters = $parameters; @@ -431,7 +447,7 @@ class ManualLogEntry extends LogEntryBase { 'log_user_text' => $this->getPerformer()->getName(), 'log_namespace' => $this->getTarget()->getNamespace(), 'log_title' => $this->getTarget()->getDBkey(), - 'log_page' => $this->getTarget()->getArticleId(), + 'log_page' => $this->getTarget()->getArticleID(), 'log_comment' => $comment, 'log_params' => serialize( (array) $this->getParameters() ), ); @@ -457,18 +473,29 @@ class ManualLogEntry extends LogEntryBase { $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() ); $user = $this->getPerformer(); + $ip = ""; + if ( $user->isAnon() ) { + /* + * "MediaWiki default" and friends may have + * no IP address in their name + */ + if ( IP::isIPAddress( $user->getName() ) ) { + $ip = $user->getName(); + } + } $rc = RecentChange::newLogEntry( $this->getTimestamp(), $logpage, $user, - $formatter->getIRCActionText(), // Used for IRC feeds - $user->isAnon() ? $user->getName() : '', + $formatter->getPlainActionText(), + $ip, $this->getType(), $this->getSubtype(), $this->getTarget(), $this->getComment(), serialize( (array) $this->getParameters() ), - $newId + $newId, + $formatter->getIRCActionComment() // Used for IRC feeds ); if ( $to === 'rc' || $to === 'rcandudp' ) { @@ -494,10 +521,16 @@ class ManualLogEntry extends LogEntryBase { return $this->parameters; } + /** + * @return User + */ public function getPerformer() { return $this->performer; } + /** + * @return Title + */ public function getTarget() { return $this->target; } diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php index 437670d0..4de1a974 100644 --- a/includes/logging/LogEventsList.php +++ b/includes/logging/LogEventsList.php @@ -23,52 +23,47 @@ * @file */ -class LogEventsList { +class LogEventsList extends ContextSource { const NO_ACTION_LINK = 1; const NO_EXTRA_USER_LINKS = 2; + const USE_REVDEL_CHECKBOXES = 4; - /** - * @var Skin - */ - private $skin; - - /** - * @var OutputPage - */ - private $out; public $flags; /** * @var Array */ - protected $message; + protected $mDefaultQuery; /** - * @var Array + * Constructor. + * The first two parameters used to be $skin and $out, but now only a context + * is needed, that's why there's a second unused parameter. + * + * @param $context IContextSource Context to use; formerly it was Skin object. + * @param $unused void Unused; used to be an OutputPage object. + * @param $flags int flags; can be a combinaison of self::NO_ACTION_LINK, + * self::NO_EXTRA_USER_LINKS or self::USE_REVDEL_CHECKBOXES. */ - protected $mDefaultQuery; + public function __construct( $context, $unused = null, $flags = 0 ) { + if ( $context instanceof IContextSource ) { + $this->setContext( $context ); + } else { + // Old parameters, $context should be a Skin object + $this->setContext( $context->getContext() ); + } - public function __construct( $skin, $out, $flags = 0 ) { - $this->skin = $skin; - $this->out = $out; $this->flags = $flags; - $this->preCacheMessages(); } /** - * As we use the same small set of messages in various methods and that - * they are called often, we call them once and save them in $this->message + * Deprecated alias for getTitle(); do not use. + * + * @deprecated in 1.20; use getTitle() instead. + * @return Title object */ - private function preCacheMessages() { - // Precache various messages - if( !isset( $this->message ) ) { - $messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink', - 'revertmove', 'undeletelink', 'undeleteviewlink', 'revdel-restore', 'hist', 'diff', - 'pipe-separator', 'revdel-restore-deleted', 'revdel-restore-visible' ); - foreach( $messages as $msg ) { - $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); - } - } + public function getDisplayTitle() { + return $this->getTitle(); } /** @@ -80,12 +75,13 @@ class LogEventsList { wfDeprecated( __METHOD__, '1.19' ); // If only one log type is used, then show a special message... $headerType = (count($type) == 1) ? $type[0] : ''; + $out = $this->getOutput(); if( LogPage::isLogType( $headerType ) ) { $page = new LogPage( $headerType ); - $this->out->setPageTitle( $page->getName()->text() ); - $this->out->addHTML( $page->getDescription()->parseAsBlock() ); + $out->setPageTitle( $page->getName()->text() ); + $out->addHTML( $page->getDescription()->parseAsBlock() ); } else { - $this->out->addHTML( wfMsgExt('alllogstext',array('parseinline')) ); + $out->addHTML( $this->msg( 'alllogstext' )->parse() ); } } @@ -105,16 +101,14 @@ class LogEventsList { $month = '', $filter = null, $tagFilter='' ) { global $wgScript, $wgMiserMode; - $action = $wgScript; $title = SpecialPage::getTitleFor( 'Log' ); - $special = $title->getPrefixedDBkey(); // For B/C, we take strings, but make sure they are converted... $types = ($types === '') ? array() : (array)$types; $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter ); - $html = Html::hidden( 'title', $special ); + $html = Html::hidden( 'title', $title->getPrefixedDBkey() ); // Basic selectors $html .= $this->getTypeMenu( $types ) . "\n"; @@ -141,15 +135,15 @@ class LogEventsList { } // Submit button - $html .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ); + $html .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ); // Fieldset - $html = Xml::fieldset( wfMsg( 'log' ), $html ); + $html = Xml::fieldset( $this->msg( 'log' )->text(), $html ); // Form wrapping - $html = Xml::tags( 'form', array( 'action' => $action, 'method' => 'get' ), $html ); + $html = Xml::tags( 'form', array( 'action' => $wgScript, 'method' => 'get' ), $html ); - $this->out->addHTML( $html ); + $this->getOutput()->addHTML( $html ); } /** @@ -157,9 +151,8 @@ class LogEventsList { * @return String: Formatted HTML */ private function getFilterLinks( $filter ) { - global $wgLang; // show/hide links - $messages = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) ); + $messages = array( $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ); // Option value -> message mapping $links = array(); $hiddens = ''; // keep track for "go" button @@ -172,26 +165,23 @@ class LogEventsList { $hideVal = 1 - intval($val); $query[$queryKey] = $hideVal; - $link = Linker::link( - $this->getDisplayTitle(), + $link = Linker::linkKnown( + $this->getTitle(), $messages[$hideVal], array(), - $query, - array( 'known', 'noclasses' ) + $query ); - $links[$type] = wfMsgHtml( "log-show-hide-{$type}", $link ); + $links[$type] = $this->msg( "log-show-hide-{$type}" )->rawParams( $link )->escaped(); $hiddens .= Html::hidden( "hide_{$type}_log", $val ) . "\n"; } // Build links - return '<small>'.$wgLang->pipeList( $links ) . '</small>' . $hiddens; + return '<small>'.$this->getLanguage()->pipeList( $links ) . '</small>' . $hiddens; } private function getDefaultQuery() { - global $wgRequest; - if ( !isset( $this->mDefaultQuery ) ) { - $this->mDefaultQuery = $wgRequest->getQueryValues(); + $this->mDefaultQuery = $this->getRequest()->getQueryValues(); unset( $this->mDefaultQuery['title'] ); unset( $this->mDefaultQuery['dir'] ); unset( $this->mDefaultQuery['offset'] ); @@ -204,20 +194,6 @@ class LogEventsList { } /** - * Get the Title object of the page the links should point to. - * This is NOT the Title of the page the entries should be restricted to. - * - * @return Title object - */ - public function getDisplayTitle() { - return $this->out->getTitle(); - } - - public function getContext() { - return $this->out->getContext(); - } - - /** * @param $queryTypes Array * @return String: Formatted HTML */ @@ -234,14 +210,12 @@ class LogEventsList { * @since 1.19 */ public function getTypeSelector() { - global $wgUser; - $typesByName = array(); // Temporary array // First pass to load the log names foreach( LogPage::validTypes() as $type ) { $page = new LogPage( $type ); $restriction = $page->getRestriction(); - if ( $wgUser->isAllowed( $restriction ) ) { + if ( $this->getUser()->isAllowed( $restriction ) ) { $typesByName[$type] = $page->getName()->text(); } } @@ -268,7 +242,7 @@ class LogEventsList { */ private function getUserInput( $user ) { return '<span style="white-space: nowrap">' . - Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user ) . + Xml::inputLabel( $this->msg( 'specialloguserlabel' )->text(), 'user', 'mw-log-user', 15, $user ) . '</span>'; } @@ -278,7 +252,7 @@ class LogEventsList { */ private function getTitleInput( $title ) { return '<span style="white-space: nowrap">' . - Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title ) . + Xml::inputLabel( $this->msg( 'speciallogtitlelabel' )->text(), 'page', 'mw-log-page', 20, $title ) . '</span>'; } @@ -288,7 +262,7 @@ class LogEventsList { */ private function getTitlePattern( $pattern ) { return '<span style="white-space: nowrap">' . - Xml::checkLabel( wfMsg( 'log-title-wildcard' ), 'pattern', 'pattern', $pattern ) . + Xml::checkLabel( $this->msg( 'log-title-wildcard' )->text(), 'pattern', 'pattern', $pattern ) . '</span>'; } @@ -297,14 +271,13 @@ class LogEventsList { * @return string */ private function getExtraInputs( $types ) { - global $wgRequest; - $offender = $wgRequest->getVal('offender'); + $offender = $this->getRequest()->getVal( 'offender' ); $user = User::newFromName( $offender, false ); if( !$user || ($user->getId() == 0 && !IP::isIPAddress($offender) ) ) { $offender = ''; // Blank field if invalid } if( count($types) == 1 && $types[0] == 'suppress' ) { - return Xml::inputLabel( wfMsg('revdelete-offender'), 'offender', + return Xml::inputLabel( $this->msg( 'revdelete-offender' )->text(), 'offender', 'mw-log-offender', 20, $offender ); } return ''; @@ -331,169 +304,38 @@ class LogEventsList { public function logLine( $row ) { $entry = DatabaseLogEntry::newFromRow( $row ); $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->getContext() ); $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) ); + $title = $entry->getTarget(); + $time = htmlspecialchars( $this->getLanguage()->userTimeAndDate( + $entry->getTimestamp(), $this->getUser() ) ); + $action = $formatter->getActionText(); - $comment = $formatter->getComment(); - $classes = array( 'mw-logline-' . $entry->getType() ); - $title = $entry->getTarget(); - $time = $this->logTimestamp( $entry ); + if ( $this->flags & self::NO_ACTION_LINK ) { + $revert = ''; + } else { + $revert = $formatter->getActionLinks(); + if ( $revert != '' ) { + $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>'; + } + } - // Extract extra parameters - $paramArray = LogPage::extractParams( $row->log_params ); - // Add review/revert links and such... - $revert = $this->logActionLinks( $row, $title, $paramArray, $comment ); + $comment = $formatter->getComment(); // Some user can hide log items and have review links $del = $this->getShowHideLinks( $row ); - if( $del != '' ) $del .= ' '; // Any tags... list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' ); - $classes = array_merge( $classes, $newClasses ); - - return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ), - $del . "$time $action $comment $revert $tagDisplay" ) . "\n"; - } - - private function logTimestamp( LogEntry $entry ) { - global $wgLang; - $time = $wgLang->timeanddate( wfTimestamp( TS_MW, $entry->getTimestamp() ), true ); - return htmlspecialchars( $time ); - } + $classes = array_merge( + array( 'mw-logline-' . $entry->getType() ), + $newClasses + ); - /** - * @TODO: split up! - * - * @param $row - * @param Title $title - * @param Array $paramArray - * @param $comment - * @return String - */ - private function logActionLinks( $row, $title, $paramArray, &$comment ) { - global $wgUser; - if( ( $this->flags & self::NO_ACTION_LINK ) // we don't want to see the action - || self::isDeleted( $row, LogPage::DELETED_ACTION ) ) // action is hidden - { - return ''; - } - $revert = ''; - if( self::typeAction( $row, 'move', 'move', 'move' ) && !empty( $paramArray[0] ) ) { - $destTitle = Title::newFromText( $paramArray[0] ); - if( $destTitle ) { - $revert = '(' . Linker::link( - SpecialPage::getTitleFor( 'Movepage' ), - $this->message['revertmove'], - array(), - array( - 'wpOldTitle' => $destTitle->getPrefixedDBkey(), - 'wpNewTitle' => $title->getPrefixedDBkey(), - 'wpReason' => wfMsgForContent( 'revertmove' ), - 'wpMovetalk' => 0 - ), - array( 'known', 'noclasses' ) - ) . ')'; - } - // Show undelete link - } elseif( self::typeAction( $row, array( 'delete', 'suppress' ), 'delete', 'deletedhistory' ) ) { - if( !$wgUser->isAllowed( 'undelete' ) ) { - $viewdeleted = $this->message['undeleteviewlink']; - } else { - $viewdeleted = $this->message['undeletelink']; - } - $revert = '(' . Linker::link( - SpecialPage::getTitleFor( 'Undelete' ), - $viewdeleted, - array(), - array( 'target' => $title->getPrefixedDBkey() ), - array( 'known', 'noclasses' ) - ) . ')'; - // Show unblock/change block link - } elseif( self::typeAction( $row, array( 'block', 'suppress' ), array( 'block', 'reblock' ), 'block' ) ) { - $revert = '(' . - Linker::link( - SpecialPage::getTitleFor( 'Unblock', $row->log_title ), - $this->message['unblocklink'], - array(), - array(), - 'known' - ) . - $this->message['pipe-separator'] . - Linker::link( - SpecialPage::getTitleFor( 'Block', $row->log_title ), - $this->message['change-blocklink'], - array(), - array(), - 'known' - ) . - ')'; - // Show change protection link - } elseif( self::typeAction( $row, 'protect', array( 'modify', 'protect', 'unprotect' ) ) ) { - $revert .= ' (' . - Linker::link( $title, - $this->message['hist'], - array(), - array( - 'action' => 'history', - 'offset' => $row->log_timestamp - ) - ); - if( $wgUser->isAllowed( 'protect' ) ) { - $revert .= $this->message['pipe-separator'] . - Linker::link( $title, - $this->message['protect_change'], - array(), - array( 'action' => 'protect' ), - 'known' ); - } - $revert .= ')'; - // Show unmerge link - } elseif( self::typeAction( $row, 'merge', 'merge', 'mergehistory' ) ) { - $revert = '(' . Linker::link( - SpecialPage::getTitleFor( 'MergeHistory' ), - $this->message['revertmerge'], - array(), - array( - 'target' => $paramArray[0], - 'dest' => $title->getPrefixedDBkey(), - 'mergepoint' => $paramArray[1] - ), - array( 'known', 'noclasses' ) - ) . ')'; - // If an edit was hidden from a page give a review link to the history - } elseif( self::typeAction( $row, array( 'delete', 'suppress' ), 'revision', 'deletedhistory' ) ) { - $revert = RevisionDeleter::getLogLinks( $title, $paramArray, - $this->message ); - // Hidden log items, give review link - } elseif( self::typeAction( $row, array( 'delete', 'suppress' ), 'event', 'deletedhistory' ) ) { - if( count($paramArray) >= 1 ) { - $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); - // $paramArray[1] is a CSV of the IDs - $query = $paramArray[0]; - // Link to each hidden object ID, $paramArray[1] is the url param - $revert = '(' . Linker::link( - $revdel, - $this->message['revdel-restore'], - array(), - array( - 'target' => $title->getPrefixedText(), - 'type' => 'logging', - 'ids' => $query - ), - array( 'known', 'noclasses' ) - ) . ')'; - } - // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters. - } else { - wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray, - &$comment, &$revert, $row->log_timestamp ) ); - } - if( $revert != '' ) { - $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>'; - } - return $revert; + return Html::rawElement( 'li', array( 'class' => $classes ), + "$del $time $action $comment $revert $tagDisplay" ) . "\n"; } /** @@ -501,28 +343,33 @@ class LogEventsList { * @return string */ private function getShowHideLinks( $row ) { - global $wgUser; - if( ( $this->flags & self::NO_ACTION_LINK ) // we don't want to see the links + if( ( $this->flags == self::NO_ACTION_LINK ) // we don't want to see the links || $row->log_type == 'suppress' ) { // no one can hide items from the suppress log return ''; } $del = ''; - // Don't show useless link to people who cannot hide revisions - if( $wgUser->isAllowed( 'deletedhistory' ) ) { - if( $row->log_deleted || $wgUser->isAllowed( 'deleterevision' ) ) { - $canHide = $wgUser->isAllowed( 'deleterevision' ); - // If event was hidden from sysops - if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) { - $del = Linker::revDeleteLinkDisabled( $canHide ); + $user = $this->getUser(); + // Don't show useless checkbox to people who cannot hide log entries + if( $user->isAllowed( 'deletedhistory' ) ) { + if( $row->log_deleted || $user->isAllowed( 'deletelogentry' ) ) { + $canHide = $user->isAllowed( 'deletelogentry' ); + if ( $this->flags & self::USE_REVDEL_CHECKBOXES ) { // Show checkboxes instead of links. + if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) { // If event was hidden from sysops + $del = Xml::check( 'deleterevisions', false, array( 'disabled' => 'disabled' ) ); + } else { + $del = Xml::check( 'showhiderevisions', false, array( 'name' => 'ids[' . $row->log_id . ']' ) ); + } } else { - $target = SpecialPage::getTitleFor( 'Log', $row->log_type ); - $query = array( - 'target' => $target->getPrefixedDBkey(), - 'type' => 'logging', - 'ids' => $row->log_id, - ); - $del = Linker::revDeleteLink( $query, - self::isDeleted( $row, LogPage::DELETED_RESTRICTED ), $canHide ); + if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) { // If event was hidden from sysops + $del = Linker::revDeleteLinkDisabled( $canHide ); + } else { + $query = array( + 'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(), + 'type' => 'logging', + 'ids' => $row->log_id, + ); + $del = Linker::revDeleteLink( $query, self::isDeleted( $row, LogPage::DELETED_RESTRICTED ), $canHide ); + } } } } @@ -606,15 +453,15 @@ class LogEventsList { * @param $types String|Array Log types to show * @param $page String|Title The page title to show log entries for * @param $user String The user who made the log entries - * @param $param Associative Array with the following additional options: + * @param $param array Associative Array with the following additional options: * - lim Integer Limit of items to show, default is 50 * - conds Array Extra conditions for the query (e.g. "log_action != 'revision'") * - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty * if set to true (default), "No matching items in log" is displayed if loglist is empty * - msgKey Array If you want a nice box with a message, set this to the key of the message. * First element is the message key, additional optional elements are parameters for the key - * that are processed with wfMsgExt and option 'parse' - * - offset Set to overwrite offset parameter in $wgRequest + * that are processed with wfMessage + * - offset Set to overwrite offset parameter in WebRequest * set to '' to unset offset * - wrap String Wrap the message in html (usually something like "<div ...>$1</div>"). * - flags Integer display flags (NO_ACTION_LINK,NO_EXTRA_USER_LINKS) @@ -652,9 +499,9 @@ class LogEventsList { } # Insert list of top 50 (or top $lim) items - $loglist = new LogEventsList( $context->getSkin(), $context->getOutput(), $flags ); + $loglist = new LogEventsList( $context, null, $flags ); $pager = new LogPager( $loglist, $types, $user, $page, '', $conds ); - if ( isset( $param['offset'] ) ) { # Tell pager to ignore $wgRequest offset + if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset $pager->setOffset( $param['offset'] ); } if( $lim > 0 ) $pager->mLimit = $lim; @@ -665,11 +512,11 @@ class LogEventsList { $s = '<div class="mw-warning-with-logexcerpt">'; if ( count( $msgKey ) == 1 ) { - $s .= wfMsgExt( $msgKey[0], array( 'parse' ) ); + $s .= $context->msg( $msgKey[0] )->parseAsBlock(); } else { // Process additional arguments $args = $msgKey; array_shift( $args ); - $s .= wfMsgExt( $msgKey[0], array( 'parse' ), $args ); + $s .= $context->msg( $msgKey[0], $args )->parseAsBlock(); } } $s .= $loglist->beginLogEventsList() . @@ -678,7 +525,7 @@ class LogEventsList { } else { if ( $showIfEmpty ) { $s = Html::rawElement( 'div', array( 'class' => 'mw-warning-logempty' ), - wfMsgExt( 'logempty', array( 'parseinline' ) ) ); + $context->msg( 'logempty' )->parse() ); } } if( $pager->getNumRows() > $pager->mLimit ) { # Show "Full log" link @@ -697,7 +544,7 @@ class LogEventsList { $urlParam['type'] = $types[0]; $s .= Linker::link( SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'log-fulllog' ), + $context->msg( 'log-fulllog' )->escaped(), array(), $urlParam ); diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php index 24490eed..7586bb65 100644 --- a/includes/logging/LogFormatter.php +++ b/includes/logging/LogFormatter.php @@ -2,6 +2,21 @@ /** * Contains classes for formatting log entries * + * 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 * @author Niklas Laxström * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later @@ -94,24 +109,24 @@ class LogFormatter { /** * Set the visibility restrictions for displaying content. - * If set to public, and an item is deleted, then it will be replaced + * If set to public, and an item is deleted, then it will be replaced * with a placeholder even if the context user is allowed to view it. * @param $audience integer self::FOR_THIS_USER or self::FOR_PUBLIC */ public function setAudience( $audience ) { $this->audience = ( $audience == self::FOR_THIS_USER ) - ? self::FOR_THIS_USER + ? self::FOR_THIS_USER : self::FOR_PUBLIC; } /** * Check if a log item can be displayed * @param $field integer LogPage::DELETED_* constant - * @return bool + * @return bool */ protected function canView( $field ) { if ( $this->audience == self::FOR_THIS_USER ) { - return LogEventsList::userCanBitfield( + return LogEventsList::userCanBitfield( $this->entry->getDeleted(), $field, $this->context->getUser() ); } else { return !$this->entry->isDeleted( $field ); @@ -148,14 +163,34 @@ class LogFormatter { * @see getActionText() * @return string text */ + public function getIRCActionComment() { + $actionComment = $this->getIRCActionText(); + $comment = $this->entry->getComment(); + + if ( $comment != '' ) { + if ( $actionComment == '' ) { + $actionComment = $comment; + } else { + $actionComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment; + } + } + + return $actionComment; + } + + /** + * Even uglier hack to maintain backwards compatibilty with IRC bots + * (bug 34508). + * @see getActionText() + * @return string text + */ public function getIRCActionText() { $this->plaintext = true; - $text = $this->getActionText(); + $this->irctext = true; $entry = $this->entry; $parameters = $entry->getParameters(); // @see LogPage::actionText() - $msgOpts = array( 'parsemag', 'escape', 'replaceafter', 'content' ); // Text of title the action is aimed at. $target = $entry->getTarget()->getPrefixedText() ; $text = null; @@ -164,11 +199,13 @@ class LogFormatter { switch( $entry->getSubtype() ) { case 'move': $movesource = $parameters['4::target']; - $text = wfMsgExt( '1movedto2', $msgOpts, $target, $movesource ); + $text = wfMessage( '1movedto2' ) + ->rawParams( $target, $movesource )->inContentLanguage()->escaped(); break; case 'move_redir': $movesource = $parameters['4::target']; - $text = wfMsgExt( '1movedto2_redir', $msgOpts, $target, $movesource ); + $text = wfMessage( '1movedto2_redir' ) + ->rawParams( $target, $movesource )->inContentLanguage()->escaped(); break; case 'move-noredirect': break; @@ -180,10 +217,12 @@ class LogFormatter { case 'delete': switch( $entry->getSubtype() ) { case 'delete': - $text = wfMsgExt( 'deletedarticle', $msgOpts, $target ); + $text = wfMessage( 'deletedarticle' ) + ->rawParams( $target )->inContentLanguage()->escaped(); break; case 'restore': - $text = wfMsgExt( 'undeletedarticle', $msgOpts, $target ); + $text = wfMessage( 'undeletedarticle' ) + ->rawParams( $target )->inContentLanguage()->escaped(); break; //case 'revision': // Revision deletion //case 'event': // Log deletion @@ -197,24 +236,46 @@ class LogFormatter { // Create a diff link to the patrolled revision if ( $entry->getSubtype() === 'patrol' ) { $diffLink = htmlspecialchars( - wfMsgForContent( 'patrol-log-diff', $parameters['4::curid'] ) ); - $text = wfMsgForContent( 'patrol-log-line', $diffLink, "[[$target]]", "" ); + wfMessage( 'patrol-log-diff', $parameters['4::curid'] ) + ->inContentLanguage()->text() ); + $text = wfMessage( 'patrol-log-line', $diffLink, "[[$target]]", "" ) + ->inContentLanguage()->text(); } else { // broken?? } break; + case 'protect': + switch( $entry->getSubtype() ) { + case 'protect': + $text = wfMessage( 'protectedarticle' ) + ->rawParams( $target . ' ' . $parameters[0] )->inContentLanguage()->escaped(); + break; + case 'unprotect': + $text = wfMessage( 'unprotectedarticle' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + case 'modify': + $text = wfMessage( 'modifiedarticleprotection' ) + ->rawParams( $target . ' ' . $parameters[0] )->inContentLanguage()->escaped(); + break; + } + break; + case 'newusers': switch( $entry->getSubtype() ) { case 'newusers': case 'create': - $text = wfMsgExt( 'newuserlog-create-entry', $msgOpts /* no params */ ); + $text = wfMessage( 'newuserlog-create-entry' ) + ->inContentLanguage()->escaped(); break; case 'create2': - $text = wfMsgExt( 'newuserlog-create2-entry', $msgOpts, $target ); + $text = wfMessage( 'newuserlog-create2-entry' ) + ->rawParams( $target )->inContentLanguage()->escaped(); break; case 'autocreate': - $text = wfMsgExt( 'newuserlog-autocreate-entry', $msgOpts /* no params */ ); + $text = wfMessage( 'newuserlog-autocreate-entry' ) + ->inContentLanguage()->escaped(); break; } break; @@ -222,14 +283,17 @@ class LogFormatter { case 'upload': switch( $entry->getSubtype() ) { case 'upload': - $text = wfMsgExt( 'uploadedimage', $msgOpts, $target ); + $text = wfMessage( 'uploadedimage' ) + ->rawParams( $target )->inContentLanguage()->escaped(); break; case 'overwrite': - $text = wfMsgExt( 'overwroteimage', $msgOpts, $target ); + $text = wfMessage( 'overwroteimage' ) + ->rawParams( $target )->inContentLanguage()->escaped(); break; } break; + // case 'suppress' --private log -- aaron (sign your messages so we know who to blame in a few years :-D) // default: } @@ -238,6 +302,7 @@ class LogFormatter { } $this->plaintext = false; + $this->irctext = false; return $text; } @@ -266,7 +331,7 @@ class LogFormatter { * Returns a sentence describing the log action. Usually * a Message object is returned, but old style log types * and entries might return pre-escaped html string. - * @return Message|pre-escaped html + * @return Message|string pre-escaped html */ protected function getActionMessage() { $message = $this->msg( $this->getMessageKey() ); @@ -289,6 +354,15 @@ class LogFormatter { } /** + * Returns extra links that comes after the action text, like "revert", etc. + * + * @return string + */ + public function getActionLinks() { + return ''; + } + + /** * Extracts the optional extra parameters for use in action messages. * The array indexes start from number 3. * @return array @@ -373,6 +447,7 @@ class LogFormatter { * Provides the name of the user who performed the log action. * Used as part of log action message or standalone, depending * which parts of the log entry has been hidden. + * @return String */ public function getPerformerElement() { if ( $this->canView( LogPage::DELETED_USER ) ) { @@ -442,9 +517,7 @@ class LogFormatter { * @return Message */ protected function msg( $key ) { - return wfMessage( $key ) - ->inLanguage( $this->context->getLanguage() ) - ->title( $this->context->getTitle() ); + return $this->context->msg( $key ); } protected function makeUserLink( User $user ) { @@ -457,11 +530,9 @@ class LogFormatter { ); if ( $this->linkFlood ) { - $element .= Linker::userToolLinks( + $element .= Linker::userToolLinksRedContribs( $user->getId(), $user->getName(), - true, // Red if no edits - 0, // Flags $user->getEditCount() ); } @@ -488,6 +559,41 @@ class LogFormatter { * @since 1.19 */ class LegacyLogFormatter extends LogFormatter { + + /** + * Backward compatibility for extension changing the comment from + * the LogLine hook. This will be set by the first call on getComment(), + * then it might be modified by the hook when calling getActionLinks(), + * so that the modified value will be returned when calling getComment() + * a second time. + * + * @var string|null + */ + private $comment = null; + + /** + * Cache for the result of getActionLinks() so that it does not need to + * run multiple times depending on the order that getComment() and + * getActionLinks() are called. + * + * @var string|null + */ + private $revert = null; + + public function getComment() { + if ( $this->comment === null ) { + $this->comment = parent::getComment(); + } + + // Make sure we execute the LogLine hook so that we immediately return + // the correct value. + if ( $this->revert === null ) { + $this->getActionLinks(); + } + + return $this->comment; + } + protected function getActionMessage() { $entry = $this->entry; $action = LogPage::actionText( @@ -500,9 +606,104 @@ class LegacyLogFormatter extends LogFormatter { ); $performer = $this->getPerformerElement(); - return $performer . $this->msg( 'word-separator' )->text() . $action; + if ( !$this->irctext ) { + $action = $performer . $this->msg( 'word-separator' )->text() . $action; + } + + return $action; } + public function getActionLinks() { + if ( $this->revert !== null ) { + return $this->revert; + } + + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { + return $this->revert = ''; + } + + $title = $this->entry->getTarget(); + $type = $this->entry->getType(); + $subtype = $this->entry->getSubtype(); + + // Show unblock/change block link + if ( ( $type == 'block' || $type == 'suppress' ) && ( $subtype == 'block' || $subtype == 'reblock' ) ) { + if ( !$this->context->getUser()->isAllowed( 'block' ) ) { + return ''; + } + + $links = array( + Linker::linkKnown( + SpecialPage::getTitleFor( 'Unblock', $title->getDBkey() ), + $this->msg( 'unblocklink' )->escaped() + ), + Linker::linkKnown( + SpecialPage::getTitleFor( 'Block', $title->getDBkey() ), + $this->msg( 'change-blocklink' )->escaped() + ) + ); + return $this->msg( 'parentheses' )->rawParams( + $this->context->getLanguage()->pipeList( $links ) )->escaped(); + // Show change protection link + } elseif ( $type == 'protect' && ( $subtype == 'protect' || $subtype == 'modify' || $subtype == 'unprotect' ) ) { + $links = array( + Linker::link( $title, + $this->msg( 'hist' )->escaped(), + array(), + array( + 'action' => 'history', + 'offset' => $this->entry->getTimestamp() + ) + ) + ); + if ( $this->context->getUser()->isAllowed( 'protect' ) ) { + $links[] = Linker::linkKnown( + $title, + $this->msg( 'protect_change' )->escaped(), + array(), + array( 'action' => 'protect' ) + ); + } + return $this->msg( 'parentheses' )->rawParams( + $this->context->getLanguage()->pipeList( $links ) )->escaped(); + // Show unmerge link + } elseif( $type == 'merge' && $subtype == 'merge' ) { + if ( !$this->context->getUser()->isAllowed( 'mergehistory' ) ) { + return ''; + } + + $params = $this->extractParameters(); + $revert = Linker::linkKnown( + SpecialPage::getTitleFor( 'MergeHistory' ), + $this->msg( 'revertmerge' )->escaped(), + array(), + array( + 'target' => $params[3], + 'dest' => $title->getPrefixedDBkey(), + 'mergepoint' => $params[4] + ) + ); + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + } + + // Do nothing. The implementation is handled by the hook modifiying the + // passed-by-ref parameters. This also changes the default value so that + // getComment() and getActionLinks() do not call them indefinitely. + $this->revert = ''; + + // This is to populate the $comment member of this instance so that it + // can be modified when calling the hook just below. + if ( $this->comment === null ) { + $this->getComment(); + } + + $params = $this->entry->getParameters(); + + wfRunHooks( 'LogLine', array( $type, $subtype, $title, $params, + &$this->comment, &$this->revert, $this->entry->getTimestamp() ) ); + + return $this->revert; + } } /** @@ -532,6 +733,34 @@ class MoveLogFormatter extends LogFormatter { $params[3] = Message::rawParam( $newname ); return $params; } + + public function getActionLinks() { + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden + || $this->entry->getSubtype() !== 'move' + || !$this->context->getUser()->isAllowed( 'move' ) ) + { + return ''; + } + + $params = $this->extractParameters(); + $destTitle = Title::newFromText( $params[3] ); + if ( !$destTitle ) { + return ''; + } + + $revert = Linker::linkKnown( + SpecialPage::getTitleFor( 'Movepage' ), + $this->msg( 'revertmove' )->escaped(), + array(), + array( + 'wpOldTitle' => $destTitle->getPrefixedDBkey(), + 'wpNewTitle' => $this->entry->getTarget()->getPrefixedDBkey(), + 'wpReason' => $this->msg( 'revertmove' )->inContentLanguage()->text(), + 'wpMovetalk' => 0 + ) + ); + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + } } /** @@ -601,6 +830,107 @@ class DeleteLogFormatter extends LogFormatter { return (int) $string; } } + + public function getActionLinks() { + $user = $this->context->getUser(); + if ( !$user->isAllowed( 'deletedhistory' ) || $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { + return ''; + } + + switch ( $this->entry->getSubtype() ) { + case 'delete': // Show undelete link + if( $user->isAllowed( 'undelete' ) ) { + $message = 'undeletelink'; + } else { + $message = 'undeleteviewlink'; + } + $revert = Linker::linkKnown( + SpecialPage::getTitleFor( 'Undelete' ), + $this->msg( $message )->escaped(), + array(), + array( 'target' => $this->entry->getTarget()->getPrefixedDBkey() ) + ); + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + + case 'revision': // If an edit was hidden from a page give a review link to the history + $params = $this->extractParameters(); + if ( !isset( $params[3] ) || !isset( $params[4] ) ) { + return ''; + } + + // Different revision types use different URL params... + $key = $params[3]; + // This is a CSV of the IDs + $ids = explode( ',', $params[4] ); + + $links = array(); + + // If there's only one item, we can show a diff link + if ( count( $ids ) == 1 ) { + // Live revision diffs... + if ( $key == 'oldid' || $key == 'revision' ) { + $links[] = Linker::linkKnown( + $this->entry->getTarget(), + $this->msg( 'diff' )->escaped(), + array(), + array( + 'diff' => intval( $ids[0] ), + 'unhide' => 1 + ) + ); + // Deleted revision diffs... + } elseif ( $key == 'artimestamp' || $key == 'archive' ) { + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Undelete' ), + $this->msg( 'diff' )->escaped(), + array(), + array( + 'target' => $this->entry->getTarget()->getPrefixedDBKey(), + 'diff' => 'prev', + 'timestamp' => $ids[0] + ) + ); + } + } + + // View/modify link... + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Revisiondelete' ), + $this->msg( 'revdel-restore' )->escaped(), + array(), + array( + 'target' => $this->entry->getTarget()->getPrefixedText(), + 'type' => $key, + 'ids' => implode( ',', $ids ), + ) + ); + + return $this->msg( 'parentheses' )->rawParams( + $this->context->getLanguage()->pipeList( $links ) )->escaped(); + + case 'event': // Hidden log items, give review link + $params = $this->extractParameters(); + if ( !isset( $params[3] ) ) { + return ''; + } + // This is a CSV of the IDs + $query = $params[3]; + // Link to each hidden object ID, $params[1] is the url param + $revert = Linker::linkKnown( + SpecialPage::getTitleFor( 'Revisiondelete' ), + $this->msg( 'revdel-restore' )->escaped(), + array(), + array( + 'target' => $this->entry->getTarget()->getPrefixedText(), + 'type' => 'logging', + 'ids' => $query + ) + ); + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + default: + return ''; + } + } } /** @@ -619,7 +949,6 @@ class PatrolLogFormatter extends LogFormatter { protected function getMessageParameters() { $params = parent::getMessageParameters(); - $newParams = array_slice( $params, 0, 3 ); $target = $this->entry->getTarget(); $oldid = $params[3]; @@ -637,8 +966,8 @@ class PatrolLogFormatter extends LogFormatter { $revlink = htmlspecialchars( $revision ); } - $newParams[3] = Message::rawParam( $revlink ); - return $newParams; + $params[3] = Message::rawParam( $revlink ); + return $params; } } @@ -670,4 +999,12 @@ class NewUsersLogFormatter extends LogFormatter { } return parent::getComment(); } + + public function getPreloadTitles() { + if ( $this->entry->getSubtype() === 'create2' ) { + //add the user talk to LinkBatch for the userLink + return array( Title::makeTitle( NS_USER_TALK, $this->entry->getTarget()->getText() ) ); + } + return array(); + } } diff --git a/includes/logging/LogPage.php b/includes/logging/LogPage.php index bbb4de8f..d96a5ea5 100644 --- a/includes/logging/LogPage.php +++ b/includes/logging/LogPage.php @@ -68,7 +68,7 @@ class LogPage { } /** - * @return bool|int|null + * @return int log_id of the inserted log entry */ protected function saveContent() { global $wgLogRestrictions; @@ -86,7 +86,7 @@ class LogPage { 'log_user_text' => $this->doer->getName(), 'log_namespace' => $this->target->getNamespace(), 'log_title' => $this->target->getDBkey(), - 'log_page' => $this->target->getArticleId(), + 'log_page' => $this->target->getArticleID(), 'log_comment' => $this->comment, 'log_params' => $this->params ); @@ -100,12 +100,12 @@ class LogPage { RecentChange::notifyLog( $now, $titleObj, $this->doer, $this->getRcComment(), '', $this->type, $this->action, $this->target, $this->comment, - $this->params, $newId + $this->params, $newId, $this->getRcCommentIRC() ); } elseif( $this->sendToUDP ) { # Don't send private logs to UDP if( isset( $wgLogRestrictions[$this->type] ) && $wgLogRestrictions[$this->type] != '*' ) { - return true; + return $newId; } # Notify external application via UDP. @@ -114,7 +114,7 @@ class LogPage { $rc = RecentChange::newLogEntry( $now, $titleObj, $this->doer, $this->getRcComment(), '', $this->type, $this->action, $this->target, $this->comment, - $this->params, $newId + $this->params, $newId, $this->getRcCommentIRC() ); $rc->notifyRC2UDP(); } @@ -133,7 +133,28 @@ class LogPage { if ( $rcComment == '' ) { $rcComment = $this->comment; } else { - $rcComment .= wfMsgForContent( 'colon-separator' ) . $this->comment; + $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . + $this->comment; + } + } + + return $rcComment; + } + + /** + * Get the RC comment from the last addEntry() call for IRC + * + * @return string + */ + public function getRcCommentIRC() { + $rcComment = $this->ircActionText; + + if( $this->comment != '' ) { + if ( $rcComment == '' ) { + $rcComment = $this->comment; + } else { + $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . + $this->comment; } } @@ -175,11 +196,10 @@ class LogPage { * @deprecated in 1.19, warnings in 1.21. Use getName() */ public static function logName( $type ) { - wfDeprecated( __METHOD__, '1.19' ); global $wgLogNames; if( isset( $wgLogNames[$type] ) ) { - return str_replace( '_', ' ', wfMsg( $wgLogNames[$type] ) ); + return str_replace( '_', ' ', wfMessage( $wgLogNames[$type] )->text() ); } else { // Bogus log types? Perhaps an extension was removed. return $type; @@ -195,9 +215,8 @@ class LogPage { * @deprecated in 1.19, warnings in 1.21. Use getDescription() */ public static function logHeader( $type ) { - wfDeprecated( __METHOD__, '1.19' ); global $wgLogHeaders; - return wfMsgExt( $wgLogHeaders[$type], array( 'parseinline' ) ); + return wfMessage( $wgLogHeaders[$type] )->parse(); } /** @@ -230,17 +249,20 @@ class LogPage { if( isset( $wgLogActions[$key] ) ) { if( is_null( $title ) ) { - $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'language' => $langObj ) ); + $rv = wfMessage( $wgLogActions[$key] )->inLanguage( $langObj )->escaped(); } else { $titleLink = self::getTitleLink( $type, $langObjOrNull, $title, $params ); if( preg_match( '/^rights\/(rights|autopromote)/', $key ) ) { - $rightsnone = wfMsgExt( 'rightsnone', array( 'parsemag', 'language' => $langObj ) ); + $rightsnone = wfMessage( 'rightsnone' )->inLanguage( $langObj )->text(); if( $skin ) { + $username = $title->getText(); foreach ( $params as &$param ) { $groupArray = array_map( 'trim', explode( ',', $param ) ); - $groupArray = array_map( array( 'User', 'getGroupMember' ), $groupArray ); + foreach( $groupArray as &$group ) { + $group = User::getGroupMember( $group, $username ); + } $param = $wgLang->listToText( $groupArray ); } } @@ -255,7 +277,7 @@ class LogPage { } if( count( $params ) == 0 ) { - $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'replaceafter', 'language' => $langObj ), $titleLink ); + $rv = wfMessage( $wgLogActions[$key] )->rawParams( $titleLink )->inLanguage( $langObj )->escaped(); } else { $details = ''; array_unshift( $params, $titleLink ); @@ -282,11 +304,11 @@ class LogPage { // Cascading flag... if( $params[2] ) { - $details .= ' [' . wfMsgExt( 'protect-summary-cascade', array( 'parsemag', 'language' => $langObj ) ) . ']'; + $details .= ' [' . wfMessage( 'protect-summary-cascade' )->inLanguage( $langObj )->text() . ']'; } } - $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'replaceafter', 'language' => $langObj ), $params ) . $details; + $rv = wfMessage( $wgLogActions[$key] )->rawParams( $params )->inLanguage( $langObj )->escaped() . $details; } } } else { @@ -399,7 +421,12 @@ class LogPage { # Use the language name for log titles, rather than Log/X if( $name == 'Log' ) { - $titleLink = '(' . Linker::link( $title, LogPage::logName( $par ) ) . ')'; + $logPage = new LogPage( $par ); + $titleLink = Linker::link( $title, $logPage->getName()->escaped() ); + $titleLink = wfMessage( 'parentheses' ) + ->inLanguage( $lang ) + ->rawParams( $titleLink ) + ->escaped(); } else { $titleLink = Linker::link( $title ); } @@ -417,11 +444,10 @@ class LogPage { * @param $action String: one of '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'move_redir' * @param $target Title object * @param $comment String: description associated - * @param $params Array: parameters passed later to wfMsg.* functions + * @param $params Array: parameters passed later to wfMessage function * @param $doer User object: the user doing the action * - * @return bool|int|null - * @TODO: make this use LogEntry::saveContent() + * @return int log_id of the inserted log entry */ public function addEntry( $action, $target, $comment, $params = array(), $doer = null ) { global $wgContLang; @@ -461,6 +487,7 @@ class LogPage { $formatter->setContext( $context ); $this->actionText = $formatter->getPlainActionText(); + $this->ircActionText = $formatter->getIRCActionText(); return $this->saveContent(); } @@ -522,7 +549,7 @@ class LogPage { * Convert a comma-delimited list of block log flags * into a more readable (and translated) form * - * @param $flags Flags to format + * @param $flags string Flags to format * @param $lang Language object to use * @return String */ @@ -533,7 +560,8 @@ class LogPage { for( $i = 0; $i < count( $flags ); $i++ ) { $flags[$i] = self::formatBlockFlag( $flags[$i], $lang ); } - return '(' . $lang->commaList( $flags ) . ')'; + return wfMessage( 'parentheses' )->inLanguage( $lang ) + ->rawParams( $lang->commaList( $flags ) )->escaped(); } else { return ''; } diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php index 16781a6e..ea1be8e0 100644 --- a/includes/logging/LogPager.php +++ b/includes/logging/LogPager.php @@ -131,6 +131,7 @@ class LogPager extends ReverseChronologicalPager { * Set the log reader to return only entries by the given user. * * @param $name String: (In)valid user name + * @return bool */ private function limitPerformer( $name ) { if( $name == '' ) { @@ -166,6 +167,7 @@ class LogPager extends ReverseChronologicalPager { * * @param $page String or Title object: Title name * @param $pattern String + * @return bool */ private function limitTitle( $page, $pattern ) { global $wgMiserMode; diff --git a/includes/logging/PatrolLog.php b/includes/logging/PatrolLog.php index 04fdc4f2..911fffc0 100644 --- a/includes/logging/PatrolLog.php +++ b/includes/logging/PatrolLog.php @@ -1,12 +1,31 @@ <?php - /** - * Class containing static functions for working with - * logs of patrol events + * Specific methods for the patrol log. + * + * 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 * @author Rob Church <robchur@gmail.com> * @author Niklas Laxström */ + +/** + * Class containing static functions for working with + * logs of patrol events + */ class PatrolLog { /** @@ -14,10 +33,11 @@ class PatrolLog { * * @param $rc Mixed: change identifier or RecentChange object * @param $auto Boolean: was this patrol event automatic? + * @param $user User: user performing the action or null to use $wgUser * * @return bool */ - public static function record( $rc, $auto = false ) { + public static function record( $rc, $auto = false, User $user = null ) { if ( !$rc instanceof RecentChange ) { $rc = RecentChange::newFromId( $rc ); if ( !is_object( $rc ) ) { @@ -25,19 +45,20 @@ class PatrolLog { } } - $title = Title::makeTitleSafe( $rc->getAttribute( 'rc_namespace' ), $rc->getAttribute( 'rc_title' ) ); - if( $title ) { - $entry = new ManualLogEntry( 'patrol', 'patrol' ); - $entry->setTarget( $title ); - $entry->setParameters( self::buildParams( $rc, $auto ) ); - $entry->setPerformer( User::newFromName( $rc->getAttribute( 'rc_user_text' ), false ) ); - $logid = $entry->insert(); - if ( !$auto ) { - $entry->publish( $logid, 'udp' ); - } - return true; + if ( !$user ) { + global $wgUser; + $user = $wgUser; + } + + $entry = new ManualLogEntry( 'patrol', 'patrol' ); + $entry->setTarget( $rc->getTitle() ); + $entry->setParameters( self::buildParams( $rc, $auto ) ); + $entry->setPerformer( $user ); + $logid = $entry->insert(); + if ( !$auto ) { + $entry->publish( $logid, 'udp' ); } - return false; + return true; } /** diff --git a/includes/media/BMP.php b/includes/media/BMP.php index 6886e950..a515c635 100644 --- a/includes/media/BMP.php +++ b/includes/media/BMP.php @@ -1,6 +1,21 @@ <?php /** - * Handler for Microsoft's bitmap format + * Handler for Microsoft's bitmap format. + * + * 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 Media diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 619485cc..99ac854b 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -1,6 +1,21 @@ <?php /** - * Generic handler for bitmap images + * Generic handler for bitmap images. + * + * 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 Media @@ -152,8 +167,11 @@ class BitmapHandler extends ImageHandler { if ( $flags & self::TRANSFORM_LATER ) { wfDebug( __METHOD__ . ": Transforming later per flags.\n" ); - return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], - $scalerParams['clientHeight'], false ); + $params = array( + 'width' => $scalerParams['clientWidth'], + 'height' => $scalerParams['clientHeight'] + ); + return new ThumbnailImage( $image, $dstUrl, false, $params ); } # Try to make a target path for the thumbnail @@ -205,8 +223,11 @@ class BitmapHandler extends ImageHandler { } elseif ( $mto ) { return $mto; } else { - return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], - $scalerParams['clientHeight'], $dstPath ); + $params = array( + 'width' => $scalerParams['clientWidth'], + 'height' => $scalerParams['clientHeight'] + ); + return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } } @@ -243,14 +264,17 @@ class BitmapHandler extends ImageHandler { * client side * * @param $image File File associated with this thumbnail - * @param $params array Array with scaler params + * @param $scalerParams array Array with scaler params * @return ThumbnailImage * - * @fixme no rotation support + * @todo fixme: no rotation support */ - protected function getClientScalingThumbnailImage( $image, $params ) { - return new ThumbnailImage( $image, $image->getURL(), - $params['clientWidth'], $params['clientHeight'], null ); + protected function getClientScalingThumbnailImage( $image, $scalerParams ) { + $params = array( + 'width' => $scalerParams['clientWidth'], + 'height' => $scalerParams['clientHeight'] + ); + return new ThumbnailImage( $image, $image->getURL(), null, $params ); } /** @@ -259,7 +283,7 @@ class BitmapHandler extends ImageHandler { * @param $image File File associated with this thumbnail * @param $params array Array with scaler params * - * @return MediaTransformError Error object if error occured, false (=no error) otherwise + * @return MediaTransformError Error object if error occurred, false (=no error) otherwise */ protected function transformImageMagick( $image, $params ) { # use ImageMagick @@ -358,7 +382,7 @@ class BitmapHandler extends ImageHandler { * @param $image File File associated with this thumbnail * @param $params array Array with scaler params * - * @return MediaTransformError Error object if error occured, false (=no error) otherwise + * @return MediaTransformError Error object if error occurred, false (=no error) otherwise */ protected function transformImageMagickExt( $image, $params ) { global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea; @@ -435,7 +459,7 @@ class BitmapHandler extends ImageHandler { * @param $image File File associated with this thumbnail * @param $params array Array with scaler params * - * @return MediaTransformError Error object if error occured, false (=no error) otherwise + * @return MediaTransformError Error object if error occurred, false (=no error) otherwise */ protected function transformCustom( $image, $params ) { # Use a custom convert command @@ -462,7 +486,7 @@ class BitmapHandler extends ImageHandler { } /** - * Log an error that occured in an external process + * Log an error that occurred in an external process * * @param $retval int * @param $err int @@ -491,7 +515,7 @@ class BitmapHandler extends ImageHandler { * @param $image File File associated with this thumbnail * @param $params array Array with scaler params * - * @return MediaTransformError Error object if error occured, false (=no error) otherwise + * @return MediaTransformError Error object if error occurred, false (=no error) otherwise */ protected function transformGd( $image, $params ) { # Use PHP's builtin GD library functions. @@ -509,7 +533,7 @@ class BitmapHandler extends ImageHandler { if ( !isset( $typemap[$params['mimeType']] ) ) { $err = 'Image type not supported'; wfDebug( "$err\n" ); - $errMsg = wfMsg( 'thumbnail_image-type' ); + $errMsg = wfMessage( 'thumbnail_image-type' )->text(); return $this->getMediaTransformError( $params, $errMsg ); } list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']]; @@ -517,14 +541,14 @@ class BitmapHandler extends ImageHandler { if ( !function_exists( $loader ) ) { $err = "Incomplete GD library configuration: missing function $loader"; wfDebug( "$err\n" ); - $errMsg = wfMsg( 'thumbnail_gd-library', $loader ); + $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text(); return $this->getMediaTransformError( $params, $errMsg ); } if ( !file_exists( $params['srcPath'] ) ) { $err = "File seems to be missing: {$params['srcPath']}"; wfDebug( "$err\n" ); - $errMsg = wfMsg( 'thumbnail_image-missing', $params['srcPath'] ); + $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text(); return $this->getMediaTransformError( $params, $errMsg ); } @@ -572,6 +596,7 @@ class BitmapHandler extends ImageHandler { /** * Escape a string for ImageMagick's property input (e.g. -set -comment) * See InterpretImageProperties() in magick/property.c + * @return mixed|string */ function escapeMagickProperty( $s ) { // Double the backslashes @@ -599,6 +624,7 @@ class BitmapHandler extends ImageHandler { * * @param $path string The file path * @param $scene string The scene specification, or false if there is none + * @return string */ function escapeMagickInput( $path, $scene = false ) { # Die on initial metacharacters (caller should prepend path) @@ -616,6 +642,7 @@ class BitmapHandler extends ImageHandler { /** * Escape a string for ImageMagick's output filename. See * InterpretImageFilename() in magick/image.c. + * @return string */ function escapeMagickOutput( $path, $scene = false ) { $path = str_replace( '%', '%%', $path ); @@ -628,6 +655,7 @@ class BitmapHandler extends ImageHandler { * * @param $path string The file path * @param $scene string The scene specification, or false if there is none + * @return string */ protected function escapeMagickPath( $path, $scene = false ) { # Die on format specifiers (other than drive letters). The regex is diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php index 746dddda..0a195547 100644 --- a/includes/media/BitmapMetadataHandler.php +++ b/includes/media/BitmapMetadataHandler.php @@ -1,13 +1,36 @@ <?php /** -Class to deal with reconciling and extracting metadata from bitmap images. -This is meant to comply with http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf + * Extraction of metadata from different bitmap image types. + * + * 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 Media + */ -This sort of acts as an intermediary between MediaHandler::getMetadata -and the various metadata extractors. - -@todo other image formats. -*/ +/** + * Class to deal with reconciling and extracting metadata from bitmap images. + * This is meant to comply with http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf + * + * This sort of acts as an intermediary between MediaHandler::getMetadata + * and the various metadata extractors. + * + * @todo other image formats. + * @ingroup Media + */ class BitmapMetadataHandler { private $metadata = array(); @@ -122,7 +145,7 @@ class BitmapMetadataHandler { /** Main entry point for jpeg's. * * @param $filename string filename (with full path) - * @return metadata result array. + * @return array metadata result array. * @throws MWException on invalid file. */ static function Jpeg ( $filename ) { @@ -193,7 +216,7 @@ class BitmapMetadataHandler { * They don't really have native metadata, so just merges together * XMP and image comment. * - * @param $filename full path to file + * @param $filename string full path to file * @return Array metadata array */ static public function GIF ( $filename ) { diff --git a/includes/media/Bitmap_ClientOnly.php b/includes/media/Bitmap_ClientOnly.php index 3c5d9738..63af2552 100644 --- a/includes/media/Bitmap_ClientOnly.php +++ b/includes/media/Bitmap_ClientOnly.php @@ -1,6 +1,21 @@ <?php /** - * Handler for bitmap images that will be resized by clients + * Handler for bitmap images that will be resized by clients. + * + * 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 Media @@ -37,7 +52,6 @@ class BitmapHandler_ClientOnly extends BitmapHandler { if ( !$this->normaliseParams( $image, $params ) ) { return new TransformParameterError( $params ); } - return new ThumbnailImage( $image, $image->getURL(), $params['width'], - $params['height'], $image->getLocalRefPath() ); + return new ThumbnailImage( $image, $image->getURL(), $image->getLocalRefPath(), $params ); } } diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index dedbee0d..84672e05 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -1,6 +1,21 @@ <?php /** - * Handler for DjVu images + * Handler for DjVu images. + * + * 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 Media @@ -123,7 +138,7 @@ class DjVuHandler extends ImageHandler { $width = isset( $params['width'] ) ? $params['width'] : 0; $height = isset( $params['height'] ) ? $params['height'] : 0; return new MediaTransformError( 'thumbnail_error', $width, $height, - wfMsg( 'djvu_no_xml' ) ); + wfMessage( 'djvu_no_xml' )->text() ); } if ( !$this->normaliseParams( $image, $params ) ) { @@ -131,20 +146,35 @@ class DjVuHandler extends ImageHandler { } $width = $params['width']; $height = $params['height']; - $srcPath = $image->getLocalRefPath(); $page = $params['page']; if ( $page > $this->pageCount( $image ) ) { - return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) ); + return new MediaTransformError( + 'thumbnail_error', + $width, + $height, + wfMessage( 'djvu_page_error' )->text() + ); } if ( $flags & self::TRANSFORM_LATER ) { - return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page ); + $params = array( + 'width' => $width, + 'height' => $height, + 'page' => $page + ); + return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { - return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'thumbnail_dest_directory' ) ); + return new MediaTransformError( + 'thumbnail_error', + $width, + $height, + wfMessage( 'thumbnail_dest_directory' )->text() + ); } + $srcPath = $image->getLocalRefPath(); # Use a subshell (brackets) to aggregate stderr from both pipeline commands # before redirecting it to the overall stdout. This works in both Linux and Windows XP. $cmd = '(' . wfEscapeShellArg( $wgDjvuRenderer ) . " -format=ppm -page={$page}" . @@ -167,7 +197,12 @@ class DjVuHandler extends ImageHandler { wfHostname(), $retval, trim($err), $cmd ) ); return new MediaTransformError( 'thumbnail_error', $width, $height, $err ); } else { - return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page ); + $params = array( + 'width' => $width, + 'height' => $height, + 'page' => $page + ); + return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } } @@ -191,6 +226,7 @@ class DjVuHandler extends ImageHandler { * Cache a document tree for the DjVu XML metadata * @param $image File * @param $gettext Boolean: DOCUMENT (Default: false) + * @return bool */ function getMetaTree( $image , $gettext = false ) { if ( isset( $image->dejaMetaTree ) ) { diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php index 80b7408c..6aef562b 100644 --- a/includes/media/DjVuImage.php +++ b/includes/media/DjVuImage.php @@ -1,6 +1,6 @@ <?php /** - * DjVu image handler + * DjVu image handler. * * Copyright © 2006 Brion Vibber <brion@pobox.com> * http://www.mediawiki.org/ @@ -21,6 +21,7 @@ * http://www.gnu.org/copyleft/gpl.html * * @file + * @ingroup Media */ /** @@ -284,6 +285,7 @@ EOR; /** * Hack to temporarily work around djvutoxml bug + * @return bool|string */ function convertDumpToXML( $dump ) { if ( strval( $dump ) == '' ) { diff --git a/includes/media/Exif.php b/includes/media/Exif.php index a4acdfe0..784a6018 100644 --- a/includes/media/Exif.php +++ b/includes/media/Exif.php @@ -1,5 +1,7 @@ <?php /** + * Extraction and validation of image metadata. + * * 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 @@ -368,6 +370,12 @@ class Exif { $this->exifGPStoNumber( 'GPSDestLongitude' ); if ( isset( $this->mFilteredExifData['GPSAltitude'] ) && isset( $this->mFilteredExifData['GPSAltitudeRef'] ) ) { + + // We know altitude data is a <num>/<denom> from the validation functions ran earlier. + // But multiplying such a string by -1 doesn't work well, so convert. + list( $num, $denom ) = explode( '/', $this->mFilteredExifData['GPSAltitude'] ); + $this->mFilteredExifData['GPSAltitude'] = $num / $denom; + if ( $this->mFilteredExifData['GPSAltitudeRef'] === "\1" ) { $this->mFilteredExifData['GPSAltitude'] *= - 1; } @@ -549,6 +557,7 @@ class Exif { */ /** * Get $this->mRawExifData + * @return array */ function getData() { return $this->mRawExifData; diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php index 7b9867f7..34a1f511 100644 --- a/includes/media/ExifBitmap.php +++ b/includes/media/ExifBitmap.php @@ -1,5 +1,22 @@ <?php /** + * Handler for bitmap images with exif metadata. + * + * 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 Media */ @@ -182,7 +199,8 @@ class ExifBitmapHandler extends BitmapHandler { * * @param string $data * @return int 0, 90, 180 or 270 - * @fixme orientation can include flipping as well; see if this is an issue! + * @todo FIXME orientation can include flipping as well; see if this is an + * issue! */ protected function getRotationForExif( $data ) { if ( !$data ) { diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php index 91cb6914..843c1fa2 100644 --- a/includes/media/FormatMetadata.php +++ b/includes/media/FormatMetadata.php @@ -1,5 +1,7 @@ <?php /** + * Formating of image metadata values into human readable form. + * * 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 @@ -98,14 +100,20 @@ class FormatMetadata { ) { continue; } - $tags[$tag] = intval( $h[0] / $h[1] ) + $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2, '0', STR_PAD_LEFT ) . ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT ) . ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT ); - $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] ); - // the 1971:01:01 is just a placeholder, and not shown to user. - if ( $time && intval( $time ) > 0 ) { - $tags[$tag] = $wgLang->time( $time ); + try { + $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] ); + // the 1971:01:01 is just a placeholder, and not shown to user. + if ( $time && intval( $time ) > 0 ) { + $tags[$tag] = $wgLang->time( $time ); + } + } catch ( TimestampException $e ) { + // This shouldn't happen, but we've seen bad formats + // such as 4-digit seconds in the wild. + // leave $tags[$tag] as-is } continue; } @@ -231,7 +239,7 @@ class FormatMetadata { case 'dc-date': case 'DateTimeMetadata': if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) { - $val = wfMsg( 'exif-unknowndate' ); + $val = wfMessage( 'exif-unknowndate' )->text(); } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', $val ) ) { // Full date. $time = wfTimestamp( TS_MW, $val ); @@ -307,7 +315,7 @@ class FormatMetadata { 'redeye' => ( $val & bindec( '01000000' ) ) >> 6, // 'reserved' => ($val & bindec( '10000000' )) >> 7, ); - + $flashMsgs = array(); # We do not need to handle unknown values since all are used. foreach ( $flashDecode as $subTag => $subValue ) { # We do not need any message for zeroed values. @@ -589,7 +597,7 @@ class FormatMetadata { case 'Software': if ( is_array( $val ) ) { //if its a software, version array. - $val = wfMsg( 'exif-software-version-value', $val[0], $val[1] ); + $val = wfMessage( 'exif-software-version-value', $val[0], $val[1] )->text(); } else { $val = self::msg( $tag, '', $val ); } @@ -597,8 +605,8 @@ class FormatMetadata { case 'ExposureTime': // Show the pretty fraction as well as decimal version - $val = wfMsg( 'exif-exposuretime-format', - self::formatFraction( $val ), self::formatNum( $val ) ); + $val = wfMessage( 'exif-exposuretime-format', + self::formatFraction( $val ), self::formatNum( $val ) )->text(); break; case 'ISOSpeedRatings': // If its = 65535 that means its at the @@ -611,13 +619,13 @@ class FormatMetadata { } break; case 'FNumber': - $val = wfMsg( 'exif-fnumber-format', - self::formatNum( $val ) ); + $val = wfMessage( 'exif-fnumber-format', + self::formatNum( $val ) )->text(); break; case 'FocalLength': case 'FocalLengthIn35mmFilm': - $val = wfMsg( 'exif-focallength-format', - self::formatNum( $val ) ); + $val = wfMessage( 'exif-focallength-format', + self::formatNum( $val ) )->text(); break; case 'MaxApertureValue': @@ -631,14 +639,14 @@ class FormatMetadata { if ( is_numeric( $val ) ) { $fNumber = pow( 2, $val / 2 ); if ( $fNumber !== false ) { - $val = wfMsg( 'exif-maxaperturevalue-value', + $val = wfMessage( 'exif-maxaperturevalue-value', self::formatNum( $val ), self::formatNum( $fNumber, 2 ) - ); + )->text(); } } break; - + case 'iimCategory': switch( strtolower( $val ) ) { // See pg 29 of IPTC photo @@ -694,7 +702,7 @@ class FormatMetadata { case 'PixelYDimension': case 'ImageWidth': case 'ImageLength': - $val = self::formatNum( $val ) . ' ' . wfMsg( 'unit-pixel' ); + $val = self::formatNum( $val ) . ' ' . wfMessage( 'unit-pixel' )->text(); break; // Do not transform fields with pure text. @@ -800,7 +808,7 @@ class FormatMetadata { break; case 'LanguageCode': - $lang = $wgLang->getLanguageName( strtolower( $val ) ); + $lang = Language::fetchLanguageName( strtolower( $val ), $wgLang->getCode() ); if ($lang) { $val = htmlspecialchars( $lang ); } else { @@ -825,14 +833,14 @@ class FormatMetadata { * This turns an array of (for example) authors into a bulleted list. * * This is public on the basis it might be useful outside of this class. - * + * * @param $vals Array array of values * @param $type String Type of array (either lang, ul, ol). * lang = language assoc array with keys being the lang code * ul = unordered list, ol = ordered list * type can also come from the '_type' member of $vals. * @param $noHtml Boolean If to avoid returning anything resembling - * html. (Ugly hack for backwards compatibility with old mediawiki). + * html. (Ugly hack for backwards compatibility with old mediawiki). * @return String single value (in wiki-syntax). */ public static function flattenArray( $vals, $type = 'ul', $noHtml = false ) { @@ -874,7 +882,7 @@ class FormatMetadata { // If default is set, save it for later, // as we don't know if it's equal to // one of the lang codes. (In xmp - // you specify the language for a + // you specify the language for a // default property by having both // a default prop, and one in the language // that are identical) @@ -937,11 +945,11 @@ class FormatMetadata { * @param $lang String lang code of item or false * @param $default Boolean if it is default value. * @param $noHtml Boolean If to avoid html (for back-compat) - * @return language item (Note: despite how this looks, - * this is treated as wikitext not html). + * @throws MWException + * @return string language item (Note: despite how this looks, + * this is treated as wikitext not html). */ private static function langItem( $value, $lang, $default = false, $noHtml = false ) { - global $wgContLang; if ( $lang === false && $default === false) { throw new MWException('$lang and $default cannot both ' . 'be false.'); @@ -956,21 +964,21 @@ class FormatMetadata { if ( $lang === false ) { if ( $noHtml ) { - return wfMsg( 'metadata-langitem-default', - $wrappedValue ) . "\n\n"; + return wfMessage( 'metadata-langitem-default', + $wrappedValue )->text() . "\n\n"; } /* else */ return '<li class="mw-metadata-lang-default">' - . wfMsg( 'metadata-langitem-default', - $wrappedValue ) + . wfMessage( 'metadata-langitem-default', + $wrappedValue )->text() . "</li>\n"; } $lowLang = strtolower( $lang ); - $langName = $wgContLang->getLanguageName( $lowLang ); + $langName = Language::fetchLanguageName( $lowLang ); if ( $langName === '' ) { //try just the base language name. (aka en-US -> en ). list( $langPrefix ) = explode( '-', $lowLang, 2 ); - $langName = $wgContLang->getLanguageName( $langPrefix ); + $langName = Language::fetchLanguageName( $langPrefix ); if ( $langName === '' ) { // give up. $langName = $lang; @@ -979,8 +987,8 @@ class FormatMetadata { // else we have a language specified if ( $noHtml ) { - return '*' . wfMsg( 'metadata-langitem', - $wrappedValue, $langName, $lang ); + return '*' . wfMessage( 'metadata-langitem', + $wrappedValue, $langName, $lang )->text(); } /* else: */ $item = '<li class="mw-metadata-lang-code-' @@ -989,8 +997,8 @@ class FormatMetadata { $item .= ' mw-metadata-lang-default'; } $item .= '" lang="' . $lang . '">'; - $item .= wfMsg( 'metadata-langitem', - $wrappedValue, $langName, $lang ); + $item .= wfMessage( 'metadata-langitem', + $wrappedValue, $langName, $lang )->text(); $item .= "</li>\n"; return $item; } @@ -1004,24 +1012,22 @@ class FormatMetadata { * @param $val String: the value of the tag * @param $arg String: an argument to pass ($1) * @param $arg2 String: a 2nd argument to pass ($2) - * @return string A wfMsg of "exif-$tag-$val" in lower case + * @return string A wfMessage of "exif-$tag-$val" in lower case */ static function msg( $tag, $val, $arg = null, $arg2 = null ) { global $wgContLang; if ($val === '') $val = 'value'; - return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 ); + return wfMessage( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text(); } /** * Format a number, convert numbers from fractions into floating point * numbers, joins arrays of numbers with commas. * - * @private - * * @param $num Mixed: the value to format - * @param $round digits to round to or false. + * @param $round float|int|bool digits to round to or false. * @return mixed A floating point number or whatever we were fed */ static function formatNum( $num, $round = false ) { @@ -1102,8 +1108,9 @@ class FormatMetadata { return $a; } - /** Fetch the human readable version of a news code. - * A news code is an 8 digit code. The first two + /** + * Fetch the human readable version of a news code. + * A news code is an 8 digit code. The first two * digits are a general classification, so we just * translate that. * @@ -1111,7 +1118,7 @@ class FormatMetadata { * a string, not an int. * * @param $val String: The 8 digit news code. - * @return The human readable form + * @return string The human readable form */ static private function convertNewsCode( $val ) { if ( !preg_match( '/^\d{8}$/D', $val ) ) { @@ -1183,7 +1190,7 @@ class FormatMetadata { * Format a coordinate value, convert numbers from floating point * into degree minute second representation. * - * @param $coord Array: degrees, minutes and seconds + * @param $coord int degrees, minutes and seconds * @param $type String: latitude or longitude (for if its a NWS or E) * @return mixed A floating point number or whatever we were fed */ @@ -1193,17 +1200,14 @@ class FormatMetadata { $nCoord = -$coord; if ( $type === 'latitude' ) { $ref = 'S'; - } - elseif ( $type === 'longitude' ) { + } elseif ( $type === 'longitude' ) { $ref = 'W'; } - } - else { + } else { $nCoord = $coord; if ( $type === 'latitude' ) { $ref = 'N'; - } - elseif ( $type === 'longitude' ) { + } elseif ( $type === 'longitude' ) { $ref = 'E'; } } @@ -1216,7 +1220,7 @@ class FormatMetadata { $min = self::formatNum( $min ); $sec = self::formatNum( $sec ); - return wfMsg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord ); + return wfMessage( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text(); } /** @@ -1274,7 +1278,7 @@ class FormatMetadata { // Todo: This can potentially be multi-line. // Need to check how that works in XMP. $street = '<span class="extended-address">' - . htmlspecialchars( + . htmlspecialchars( $vals['CiAdrExtadr'] ) . '</span>'; } @@ -1321,7 +1325,7 @@ class FormatMetadata { } if ( isset( $vals['CiAdrPcode'] ) ) { $postal = '<span class="postal-code">' - . htmlspecialchars( + . htmlspecialchars( $vals['CiAdrPcode'] ) . '</span>'; } @@ -1337,9 +1341,9 @@ class FormatMetadata { . htmlspecialchars( $vals['CiUrlWork'] ) . '</span>'; } - return wfMsg( 'exif-contact-value', $email, $url, + return wfMessage( 'exif-contact-value', $email, $url, $street, $city, $region, $postal, $country, - $tel ); + $tel )->text(); } } } @@ -1352,12 +1356,19 @@ class FormatMetadata { **/ class FormatExif { var $meta; - function FormatExif ( $meta ) { + + /** + * @param $meta array + */ + function FormatExif( $meta ) { wfDeprecated(__METHOD__); $this->meta = $meta; } - function getFormattedData ( ) { + /** + * @return array + */ + function getFormattedData() { return FormatMetadata::getFormattedData( $this->meta ); } } diff --git a/includes/media/GIF.php b/includes/media/GIF.php index 32618e94..84b9b8ca 100644 --- a/includes/media/GIF.php +++ b/includes/media/GIF.php @@ -2,6 +2,21 @@ /** * Handler for GIF images. * + * 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 Media */ @@ -78,6 +93,17 @@ class GIFHandler extends BitmapHandler { return false; } + /** + * We cannot animate thumbnails that are bigger than a particular size + * @param File $file + * @return bool + */ + function canAnimateThumbnail( $file ) { + global $wgMaxAnimatedGifArea; + $answer = $this->getImageArea( $file ) <= $wgMaxAnimatedGifArea; + return $answer; + } + function getMetadataType( $image ) { return 'parsed-gif'; } @@ -127,11 +153,11 @@ class GIFHandler extends BitmapHandler { $info[] = $original; if ( $metadata['looped'] ) { - $info[] = wfMsgExt( 'file-info-gif-looped', 'parseinline' ); + $info[] = wfMessage( 'file-info-gif-looped' )->parse(); } if ( $metadata['frameCount'] > 1 ) { - $info[] = wfMsgExt( 'file-info-gif-frames', 'parseinline', $metadata['frameCount'] ); + $info[] = wfMessage( 'file-info-gif-frames' )->numParams( $metadata['frameCount'] )->parse(); } if ( $metadata['duration'] ) { diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php index 5dbeb8f8..5fc5c1a7 100644 --- a/includes/media/GIFMetadataExtractor.php +++ b/includes/media/GIFMetadataExtractor.php @@ -7,6 +7,21 @@ * Deliberately not using MWExceptions to avoid external dependencies, encouraging * redistribution. * + * 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 Media */ @@ -286,7 +301,7 @@ class GIFMetadataExtractor { * sub-blocks in the returned value. Normally this is false, * except XMP is weird and does a hack where you need to keep * these length bytes. - * @return The data. + * @return string The data. */ static function readBlock( $fh, $includeLengths = false ) { $data = ''; diff --git a/includes/media/IPTC.php b/includes/media/IPTC.php index 1d19791c..8fd3552f 100644 --- a/includes/media/IPTC.php +++ b/includes/media/IPTC.php @@ -1,8 +1,31 @@ <?php /** -*Class for some IPTC functions. + * Class for some IPTC functions. + * + * 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 Media + */ -*/ +/** + * Class for some IPTC functions. + * + * @ingroup Media + */ class IPTC { /** @@ -395,10 +418,10 @@ class IPTC { /** * Helper function to convert charset for iptc values. - * @param $data Mixed String or Array: The iptc string + * @param $data string|array The iptc string * @param $charset String: The charset * - * @return string + * @return string|array */ private static function convIPTC ( $data, $charset ) { if ( is_array( $data ) ) { diff --git a/includes/media/ImageHandler.php b/includes/media/ImageHandler.php new file mode 100644 index 00000000..61759074 --- /dev/null +++ b/includes/media/ImageHandler.php @@ -0,0 +1,249 @@ +<?php +/** + * Media-handling base classes and generic functionality. + * + * 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 Media + */ + +/** + * Media handler abstract base class for images + * + * @ingroup Media + */ +abstract class ImageHandler extends MediaHandler { + + /** + * @param $file File + * @return bool + */ + function canRender( $file ) { + return ( $file->getWidth() && $file->getHeight() ); + } + + function getParamMap() { + return array( 'img_width' => 'width' ); + } + + function validateParam( $name, $value ) { + if ( in_array( $name, array( 'width', 'height' ) ) ) { + if ( $value <= 0 ) { + return false; + } else { + return true; + } + } else { + return false; + } + } + + function makeParamString( $params ) { + if ( isset( $params['physicalWidth'] ) ) { + $width = $params['physicalWidth']; + } elseif ( isset( $params['width'] ) ) { + $width = $params['width']; + } else { + throw new MWException( 'No width specified to '.__METHOD__ ); + } + # Removed for ProofreadPage + #$width = intval( $width ); + return "{$width}px"; + } + + function parseParamString( $str ) { + $m = false; + if ( preg_match( '/^(\d+)px$/', $str, $m ) ) { + return array( 'width' => $m[1] ); + } else { + return false; + } + } + + function getScriptParams( $params ) { + return array( 'width' => $params['width'] ); + } + + /** + * @param $image File + * @param $params + * @return bool + */ + function normaliseParams( $image, &$params ) { + $mimeType = $image->getMimeType(); + + if ( !isset( $params['width'] ) ) { + return false; + } + + if ( !isset( $params['page'] ) ) { + $params['page'] = 1; + } else { + if ( $params['page'] > $image->pageCount() ) { + $params['page'] = $image->pageCount(); + } + + if ( $params['page'] < 1 ) { + $params['page'] = 1; + } + } + + $srcWidth = $image->getWidth( $params['page'] ); + $srcHeight = $image->getHeight( $params['page'] ); + + if ( isset( $params['height'] ) && $params['height'] != -1 ) { + # Height & width were both set + if ( $params['width'] * $srcHeight > $params['height'] * $srcWidth ) { + # Height is the relative smaller dimension, so scale width accordingly + $params['width'] = self::fitBoxWidth( $srcWidth, $srcHeight, $params['height'] ); + + if ( $params['width'] == 0 ) { + # Very small image, so we need to rely on client side scaling :( + $params['width'] = 1; + } + + $params['physicalWidth'] = $params['width']; + } else { + # Height was crap, unset it so that it will be calculated later + unset( $params['height'] ); + } + } + + if ( !isset( $params['physicalWidth'] ) ) { + # Passed all validations, so set the physicalWidth + $params['physicalWidth'] = $params['width']; + } + + # Because thumbs are only referred to by width, the height always needs + # to be scaled by the width to keep the thumbnail sizes consistent, + # even if it was set inside the if block above + $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, + $params['physicalWidth'] ); + + # Set the height if it was not validated in the if block higher up + if ( !isset( $params['height'] ) || $params['height'] == -1 ) { + $params['height'] = $params['physicalHeight']; + } + + + if ( !$this->validateThumbParams( $params['physicalWidth'], + $params['physicalHeight'], $srcWidth, $srcHeight, $mimeType ) ) { + return false; + } + return true; + } + + /** + * Validate thumbnail parameters and fill in the correct height + * + * @param $width Integer: specified width (input/output) + * @param $height Integer: height (output only) + * @param $srcWidth Integer: width of the source image + * @param $srcHeight Integer: height of the source image + * @param $mimeType + * @return bool False to indicate that an error should be returned to the user. + */ + function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) { + $width = intval( $width ); + + # Sanity check $width + if( $width <= 0) { + wfDebug( __METHOD__.": Invalid destination width: $width\n" ); + return false; + } + if ( $srcWidth <= 0 ) { + wfDebug( __METHOD__.": Invalid source width: $srcWidth\n" ); + return false; + } + + $height = File::scaleHeight( $srcWidth, $srcHeight, $width ); + if ( $height == 0 ) { + # Force height to be at least 1 pixel + $height = 1; + } + return true; + } + + /** + * @param $image File + * @param $script + * @param $params + * @return bool|ThumbnailImage + */ + function getScriptedTransform( $image, $script, $params ) { + if ( !$this->normaliseParams( $image, $params ) ) { + return false; + } + $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) ); + + if( $image->mustRender() || $params['width'] < $image->getWidth() ) { + return new ThumbnailImage( $image, $url, false, $params ); + } + } + + function getImageSize( $image, $path ) { + wfSuppressWarnings(); + $gis = getimagesize( $path ); + wfRestoreWarnings(); + return $gis; + } + + /** + * @param $file File + * @return string + */ + function getShortDesc( $file ) { + global $wgLang; + $nbytes = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); + $widthheight = wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->escaped(); + + return "$widthheight ($nbytes)"; + } + + /** + * @param $file File + * @return string + */ + function getLongDesc( $file ) { + global $wgLang; + $pages = $file->pageCount(); + $size = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); + if ( $pages === false || $pages <= 1 ) { + $msg = wfMessage( 'file-info-size' )->numParams( $file->getWidth(), + $file->getHeight() )->params( $size, + $file->getMimeType() )->parse(); + } else { + $msg = wfMessage( 'file-info-size-pages' )->numParams( $file->getWidth(), + $file->getHeight() )->params( $size, + $file->getMimeType() )->numParams( $pages )->parse(); + } + return $msg; + } + + /** + * @param $file File + * @return string + */ + function getDimensionsString( $file ) { + $pages = $file->pageCount(); + if ( $pages > 1 ) { + return wfMessage( 'widthheightpage' )->numParams( $file->getWidth(), $file->getHeight(), $pages )->text(); + } else { + return wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->text(); + } + } +} diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php index 7033409b..a15b6524 100644 --- a/includes/media/Jpeg.php +++ b/includes/media/Jpeg.php @@ -1,5 +1,22 @@ <?php /** + * Handler for JPEG images. + * + * 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 Media */ diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php index 224b4a2b..8d7e43b9 100644 --- a/includes/media/JpegMetadataExtractor.php +++ b/includes/media/JpegMetadataExtractor.php @@ -1,10 +1,34 @@ <?php /** -* Class for reading jpegs and extracting metadata. -* see also BitmapMetadataHandler. -* -* Based somewhat on GIFMetadataExtrator. -*/ + * Extraction of JPEG image metadata. + * + * 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 Media + */ + +/** + * Class for reading jpegs and extracting metadata. + * see also BitmapMetadataHandler. + * + * Based somewhat on GIFMetadataExtrator. + * + * @ingroup Media + */ class JpegMetadataExtractor { const MAX_JPEG_SEGMENTS = 200; @@ -143,13 +167,17 @@ class JpegMetadataExtractor { /** * Helper function for jpegSegmentSplitter * @param &$fh FileHandle for jpeg file - * @return data content of segment. + * @return string data content of segment. */ private static function jpegExtractMarker( &$fh ) { $size = wfUnpack( "nint", fread( $fh, 2 ), 2 ); - if ( $size['int'] <= 2 ) throw new MWException( "invalid marker size in jpeg" ); + if ( $size['int'] <= 2 ) { + throw new MWException( "invalid marker size in jpeg" ); + } $segment = fread( $fh, $size['int'] - 2 ); - if ( strlen( $segment ) !== $size['int'] - 2 ) throw new MWException( "Segment shorter than expected" ); + if ( strlen( $segment ) !== $size['int'] - 2 ) { + throw new MWException( "Segment shorter than expected" ); + } return $segment; } diff --git a/includes/media/Generic.php b/includes/media/MediaHandler.php index 271d3a8d..965099fd 100644 --- a/includes/media/Generic.php +++ b/includes/media/MediaHandler.php @@ -1,6 +1,21 @@ <?php /** - * Media-handling base classes and generic functionality + * Media-handling base classes and generic functionality. + * + * 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 Media @@ -160,6 +175,7 @@ abstract class MediaHandler { * MediaHandler::METADATA_GOOD for if the metadata is a-ok, * MediaHanlder::METADATA_COMPATIBLE if metadata is old but backwards * compatible (which may or may not trigger a metadata reload). + * @return bool */ function isMetadataValid( $image, $metadata ) { return self::METADATA_GOOD; @@ -173,6 +189,7 @@ abstract class MediaHandler { * Used when the repository has a thumbnailScriptUrl option configured. * * Return false to fall back to the regular getTransform(). + * @return bool */ function getScriptedTransform( $image, $script, $params ) { return false; @@ -186,6 +203,7 @@ abstract class MediaHandler { * @param $dstPath String: filesystem destination path * @param $dstUrl String: Destination URL to use in output HTML * @param $params Array: Arbitrary set of parameters validated by $this->validateParam() + * @return MediaTransformOutput */ final function getTransform( $image, $dstPath, $dstUrl, $params ) { return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER ); @@ -227,27 +245,46 @@ abstract class MediaHandler { /** * True if the handled types can be transformed + * @return bool */ function canRender( $file ) { return true; } /** * True if handled types cannot be displayed directly in a browser * but can be rendered + * @return bool */ function mustRender( $file ) { return false; } /** * True if the type has multi-page capabilities + * @return bool */ function isMultiPage( $file ) { return false; } /** * Page count for a multi-page document, false if unsupported or unknown + * @return bool */ function pageCount( $file ) { return false; } /** * The material is vectorized and thus scaling is lossless + * @return bool */ function isVectorized( $file ) { return false; } /** + * The material is an image, and is animated. + * In particular, video material need not return true. + * @note Before 1.20, this was a method of ImageHandler only + * @return bool + */ + function isAnimatedImage( $file ) { return false; } + /** + * If the material is animated, we can animate the thumbnail + * @since 1.20 + * @return bool If material is not animated, handler may return any value. + */ + function canAnimateThumbnail( $file ) { return true; } + /** * False if the handler is disabled for all files + * @return bool */ function isEnabled() { return true; } @@ -258,6 +295,8 @@ abstract class MediaHandler { * Returns false if unknown or if the document is not multi-page. * * @param $image File + * @param $page Unused, left for backcompatibility? + * @return array */ function getPageDimensions( $image, $page ) { $gis = $this->getImageSize( $image, $image->getLocalRefPath() ); @@ -270,6 +309,7 @@ abstract class MediaHandler { /** * Generic getter for text layer. * Currently overloaded by PDF and DjVu handlers + * @return bool */ function getPageText( $image, $page ) { return false; @@ -300,6 +340,7 @@ abstract class MediaHandler { * all the formatting according to some standard. That makes it possible * to do things like visual indication of grouped and chained streams * in ogg container files. + * @return bool */ function formatMetadata( $image ) { return false; @@ -344,7 +385,7 @@ abstract class MediaHandler { */ function visibleMetadataFields() { $fields = array(); - $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) ); + $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() ); foreach( $lines as $line ) { $matches = array(); if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { @@ -471,7 +512,7 @@ abstract class MediaHandler { * match the handler class, a Status object should be returned containing * relevant errors. * - * @param $fileName The local path to the file. + * @param $fileName string The local path to the file. * @return Status object */ function verifyUpload( $fileName ) { @@ -482,9 +523,9 @@ abstract class MediaHandler { * Check for zero-sized thumbnails. These can be generated when * no disk space is available or some other error occurs * - * @param $dstPath The location of the suspect file - * @param $retval Return value of some shell process, file will be deleted if this is non-zero - * @return true if removed, false otherwise + * @param $dstPath string The location of the suspect file + * @param $retval int Return value of some shell process, file will be deleted if this is non-zero + * @return bool True if removed, false otherwise */ function removeBadFile( $dstPath, $retval = 0 ) { if( file_exists( $dstPath ) ) { @@ -509,7 +550,7 @@ abstract class MediaHandler { /** * Remove files from the purge list - * + * * @param array $files * @param array $options */ @@ -517,235 +558,3 @@ abstract class MediaHandler { // Do nothing } } - -/** - * Media handler abstract base class for images - * - * @ingroup Media - */ -abstract class ImageHandler extends MediaHandler { - - /** - * @param $file File - * @return bool - */ - function canRender( $file ) { - return ( $file->getWidth() && $file->getHeight() ); - } - - function getParamMap() { - return array( 'img_width' => 'width' ); - } - - function validateParam( $name, $value ) { - if ( in_array( $name, array( 'width', 'height' ) ) ) { - if ( $value <= 0 ) { - return false; - } else { - return true; - } - } else { - return false; - } - } - - function makeParamString( $params ) { - if ( isset( $params['physicalWidth'] ) ) { - $width = $params['physicalWidth']; - } elseif ( isset( $params['width'] ) ) { - $width = $params['width']; - } else { - throw new MWException( 'No width specified to '.__METHOD__ ); - } - # Removed for ProofreadPage - #$width = intval( $width ); - return "{$width}px"; - } - - function parseParamString( $str ) { - $m = false; - if ( preg_match( '/^(\d+)px$/', $str, $m ) ) { - return array( 'width' => $m[1] ); - } else { - return false; - } - } - - function getScriptParams( $params ) { - return array( 'width' => $params['width'] ); - } - - /** - * @param $image File - * @param $params - * @return bool - */ - function normaliseParams( $image, &$params ) { - $mimeType = $image->getMimeType(); - - if ( !isset( $params['width'] ) ) { - return false; - } - - if ( !isset( $params['page'] ) ) { - $params['page'] = 1; - } else { - if ( $params['page'] > $image->pageCount() ) { - $params['page'] = $image->pageCount(); - } - - if ( $params['page'] < 1 ) { - $params['page'] = 1; - } - } - - $srcWidth = $image->getWidth( $params['page'] ); - $srcHeight = $image->getHeight( $params['page'] ); - - if ( isset( $params['height'] ) && $params['height'] != -1 ) { - # Height & width were both set - if ( $params['width'] * $srcHeight > $params['height'] * $srcWidth ) { - # Height is the relative smaller dimension, so scale width accordingly - $params['width'] = self::fitBoxWidth( $srcWidth, $srcHeight, $params['height'] ); - - if ( $params['width'] == 0 ) { - # Very small image, so we need to rely on client side scaling :( - $params['width'] = 1; - } - - $params['physicalWidth'] = $params['width']; - } else { - # Height was crap, unset it so that it will be calculated later - unset( $params['height'] ); - } - } - - if ( !isset( $params['physicalWidth'] ) ) { - # Passed all validations, so set the physicalWidth - $params['physicalWidth'] = $params['width']; - } - - # Because thumbs are only referred to by width, the height always needs - # to be scaled by the width to keep the thumbnail sizes consistent, - # even if it was set inside the if block above - $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, - $params['physicalWidth'] ); - - # Set the height if it was not validated in the if block higher up - if ( !isset( $params['height'] ) || $params['height'] == -1 ) { - $params['height'] = $params['physicalHeight']; - } - - - if ( !$this->validateThumbParams( $params['physicalWidth'], - $params['physicalHeight'], $srcWidth, $srcHeight, $mimeType ) ) { - return false; - } - return true; - } - - /** - * Validate thumbnail parameters and fill in the correct height - * - * @param $width Integer: specified width (input/output) - * @param $height Integer: height (output only) - * @param $srcWidth Integer: width of the source image - * @param $srcHeight Integer: height of the source image - * @param $mimeType Unused - * @return false to indicate that an error should be returned to the user. - */ - function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) { - $width = intval( $width ); - - # Sanity check $width - if( $width <= 0) { - wfDebug( __METHOD__.": Invalid destination width: $width\n" ); - return false; - } - if ( $srcWidth <= 0 ) { - wfDebug( __METHOD__.": Invalid source width: $srcWidth\n" ); - return false; - } - - $height = File::scaleHeight( $srcWidth, $srcHeight, $width ); - if ( $height == 0 ) { - # Force height to be at least 1 pixel - $height = 1; - } - return true; - } - - /** - * @param $image File - * @param $script - * @param $params - * @return bool|ThumbnailImage - */ - function getScriptedTransform( $image, $script, $params ) { - if ( !$this->normaliseParams( $image, $params ) ) { - return false; - } - $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) ); - $page = isset( $params['page'] ) ? $params['page'] : false; - - if( $image->mustRender() || $params['width'] < $image->getWidth() ) { - return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page ); - } - } - - function getImageSize( $image, $path ) { - wfSuppressWarnings(); - $gis = getimagesize( $path ); - wfRestoreWarnings(); - return $gis; - } - - function isAnimatedImage( $image ) { - return false; - } - - /** - * @param $file File - * @return string - */ - function getShortDesc( $file ) { - global $wgLang; - $nbytes = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); - $widthheight = wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->escaped(); - - return "$widthheight ($nbytes)"; - } - - /** - * @param $file File - * @return string - */ - function getLongDesc( $file ) { - global $wgLang; - $pages = $file->pageCount(); - $size = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); - if ( $pages === false || $pages <= 1 ) { - $msg = wfMessage( 'file-info-size' )->numParams( $file->getWidth(), - $file->getHeight() )->params( $size, - $file->getMimeType() )->parse(); - } else { - $msg = wfMessage( 'file-info-size-pages' )->numParams( $file->getWidth(), - $file->getHeight() )->params( $size, - $file->getMimeType() )->numParams( $pages )->parse(); - } - return $msg; - } - - /** - * @param $file File - * @return string - */ - function getDimensionsString( $file ) { - $pages = $file->pageCount(); - if ( $pages > 1 ) { - return wfMessage( 'widthheightpage' )->numParams( $file->getWidth(), $file->getHeight(), $pages )->text(); - } else { - return wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->text(); - } - } -} diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php index fcfb2f45..773824cb 100644 --- a/includes/media/MediaTransformOutput.php +++ b/includes/media/MediaTransformOutput.php @@ -2,6 +2,21 @@ /** * Base class for the output of file transformation methods. * + * 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 Media */ @@ -21,28 +36,37 @@ abstract class MediaTransformOutput { protected $storagePath = false; /** - * Get the width of the output box + * @return integer Width of the output box */ public function getWidth() { return $this->width; } /** - * Get the height of the output box + * @return integer Height of the output box */ public function getHeight() { return $this->height; } /** - * @return string The thumbnail URL + * Get the final extension of the thumbnail. + * Returns false for scripted transformations. + * @return string|false + */ + public function getExtension() { + return $this->path ? FileBackend::extensionFromPath( $this->path ) : false; + } + + /** + * @return string|false The thumbnail URL */ public function getUrl() { return $this->url; } /** - * @return string|false The permanent thumbnail storage path + * @return string|bool The permanent thumbnail storage path */ public function getStoragePath() { return $this->storagePath; @@ -69,7 +93,7 @@ abstract class MediaTransformOutput { * custom-url-link Custom URL to link to * custom-title-link Custom Title object to link to * valign vertical-align property, if the output is an inline element - * img-class Class applied to the <img> tag, if there is such a tag + * img-class Class applied to the "<img>" tag, if there is such a tag * * For images, desc-link and file-link are implemented as a click-through. For * sounds and videos, they may be displayed in other ways. @@ -80,6 +104,7 @@ abstract class MediaTransformOutput { /** * This will be overridden to return true in error classes + * @return bool */ public function isError() { return false; @@ -90,7 +115,7 @@ abstract class MediaTransformOutput { * This will return false if there was an error, the * thumbnail is to be handled client-side only, or if * transformation was deferred via TRANSFORM_LATER. - * + * * @return Bool */ public function hasFile() { @@ -113,7 +138,7 @@ abstract class MediaTransformOutput { * Get the path of a file system copy of the thumbnail. * Callers should never write to this path. * - * @return string|false Returns false if there isn't one + * @return string|bool Returns false if there isn't one */ public function getLocalCopyPath() { if ( $this->isError() ) { @@ -132,7 +157,14 @@ abstract class MediaTransformOutput { * @return Bool success */ public function streamFile( $headers = array() ) { - return $this->path && StreamFile::stream( $this->getLocalCopyPath(), $headers ); + if ( !$this->path ) { + return false; + } elseif ( FileBackend::isStoragePath( $this->path ) ) { + $be = $this->file->getRepo()->getBackend(); + return $be->streamFile( array( 'src' => $this->path, 'headers' => $headers ) )->isOK(); + } else { // FS-file + return StreamFile::stream( $this->getLocalCopyPath(), $headers ); + } } /** @@ -182,25 +214,46 @@ class ThumbnailImage extends MediaTransformOutput { * Get a thumbnail object from a file and parameters. * If $path is set to null, the output file is treated as a source copy. * If $path is set to false, no output file will be created. - * + * $parameters should include, as a minimum, (file) 'width' and 'height'. + * It may also include a 'page' parameter for multipage files. + * * @param $file File object * @param $url String: URL path to the thumb - * @param $width Integer: file's width - * @param $height Integer: file's height - * @param $path String|false|null: filesystem path to the thumb - * @param $page Integer: page number, for multipage files + * @param $path String|bool|null: filesystem path to the thumb + * @param $parameters Array: Associative array of parameters * @private */ - function __construct( $file, $url, $width, $height, $path = false, $page = false ) { + function __construct( $file, $url, $path = false, $parameters = array() ) { + # Previous parameters: + # $file, $url, $width, $height, $path = false, $page = false + + if( is_array( $parameters ) ){ + $defaults = array( + 'page' => false + ); + $actualParams = $parameters + $defaults; + } else { + # Using old format, should convert. Later a warning could be added here. + $numArgs = func_num_args(); + $actualParams = array( + 'width' => $path, + 'height' => $parameters, + 'page' => ( $numArgs > 5 ) ? func_get_arg( 5 ) : false + ); + $path = ( $numArgs > 4 ) ? func_get_arg( 4 ) : false; + } + $this->file = $file; $this->url = $url; + $this->path = $path; + # These should be integers when they get here. # If not, there's a bug somewhere. But let's at # least produce valid HTML code regardless. - $this->width = round( $width ); - $this->height = round( $height ); - $this->path = $path; - $this->page = $page; + $this->width = round( $actualParams['width'] ); + $this->height = round( $actualParams['height'] ); + + $this->page = $actualParams['page']; } /** @@ -221,6 +274,9 @@ class ThumbnailImage extends MediaTransformOutput { * custom-url-link Custom URL to link to * custom-title-link Custom Title object to link to * custom target-link Value of the target attribute, for custom-target-link + * parser-extlink-* Attributes added by parser for external links: + * parser-extlink-rel: add rel="nofollow" + * parser-extlink-target: link target, but overridden by custom-target-link * * For images, desc-link and file-link are implemented as a click-through. For * sounds and videos, they may be displayed in other ways. @@ -243,6 +299,11 @@ class ThumbnailImage extends MediaTransformOutput { } if ( !empty( $options['custom-target-link'] ) ) { $linkAttribs['target'] = $options['custom-target-link']; + } elseif ( !empty( $options['parser-extlink-target'] ) ) { + $linkAttribs['target'] = $options['parser-extlink-target']; + } + if ( !empty( $options['parser-extlink-rel'] ) ) { + $linkAttribs['rel'] = $options['parser-extlink-rel']; } } elseif ( !empty( $options['custom-title-link'] ) ) { $title = $options['custom-title-link']; @@ -326,6 +387,6 @@ class TransformParameterError extends MediaTransformError { parent::__construct( 'thumbnail_error', max( isset( $params['width'] ) ? $params['width'] : 0, 120 ), max( isset( $params['height'] ) ? $params['height'] : 0, 120 ), - wfMsg( 'thumbnail_invalid_params' ) ); + wfMessage( 'thumbnail_invalid_params' )->text() ); } } diff --git a/includes/media/PNG.php b/includes/media/PNG.php index 8fe9ecb4..1b329e57 100644 --- a/includes/media/PNG.php +++ b/includes/media/PNG.php @@ -2,6 +2,21 @@ /** * Handler for PNG images. * + * 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 Media */ @@ -65,6 +80,14 @@ class PNGHandler extends BitmapHandler { } return false; } + /** + * We do not support making APNG thumbnails, so always false + * @param $image File + * @return bool false + */ + function canAnimateThumbnail( $image ) { + return false; + } function getMetadataType( $image ) { return 'parsed-png'; @@ -113,13 +136,13 @@ class PNGHandler extends BitmapHandler { $info[] = $original; if ( $metadata['loopCount'] == 0 ) { - $info[] = wfMsgExt( 'file-info-png-looped', 'parseinline' ); + $info[] = wfMessage( 'file-info-png-looped' )->parse(); } elseif ( $metadata['loopCount'] > 1 ) { - $info[] = wfMsgExt( 'file-info-png-repeat', 'parseinline', $metadata['loopCount'] ); + $info[] = wfMessage( 'file-info-png-repeat' )->numParams( $metadata['loopCount'] )->parse(); } if ( $metadata['frameCount'] > 0 ) { - $info[] = wfMsgExt( 'file-info-png-frames', 'parseinline', $metadata['frameCount'] ); + $info[] = wfMessage( 'file-info-png-frames' )->numParams( $metadata['frameCount'] )->parse(); } if ( $metadata['duration'] ) { diff --git a/includes/media/PNGMetadataExtractor.php b/includes/media/PNGMetadataExtractor.php index d3c44d4f..9dcde406 100644 --- a/includes/media/PNGMetadataExtractor.php +++ b/includes/media/PNGMetadataExtractor.php @@ -1,10 +1,26 @@ <?php /** * PNG frame counter and metadata extractor. + * * Slightly derived from GIFMetadataExtractor.php * Deliberately not using MWExceptions to avoid external dependencies, encouraging * redistribution. * + * 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 Media */ diff --git a/includes/media/SVG.php b/includes/media/SVG.php index aac838e1..55fa5547 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -2,6 +2,21 @@ /** * Handler for SVG images. * + * 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 Media */ @@ -49,6 +64,13 @@ class SvgHandler extends ImageHandler { } /** + * We do not support making animated svg thumbnails + */ + function canAnimateThumb( $file ) { + return false; + } + + /** * @param $image File * @param $params * @return bool @@ -93,20 +115,20 @@ class SvgHandler extends ImageHandler { $clientHeight = $params['height']; $physicalWidth = $params['physicalWidth']; $physicalHeight = $params['physicalHeight']; - $srcPath = $image->getLocalRefPath(); if ( $flags & self::TRANSFORM_LATER ) { - return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, - wfMsg( 'thumbnail_dest_directory' ) ); + wfMessage( 'thumbnail_dest_directory' )->text() ); } + $srcPath = $image->getLocalRefPath(); $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight ); if( $status === true ) { - return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } else { return $status; // MediaTransformError } @@ -119,7 +141,7 @@ class SvgHandler extends ImageHandler { * @param string $dstPath * @param string $width * @param string $height - * @return true|MediaTransformError + * @return bool|MediaTransformError */ public function rasterize( $srcPath, $dstPath, $width, $height ) { global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath; @@ -199,15 +221,30 @@ class SvgHandler extends ImageHandler { } /** + * Subtitle for the image. Different from the base + * class so it can be denoted that SVG's have + * a "nominal" resolution, and not a fixed one, + * as well as so animation can be denoted. + * * @param $file File * @return string */ function getLongDesc( $file ) { global $wgLang; - return wfMsgExt( 'svg-long-desc', 'parseinline', - $wgLang->formatNum( $file->getWidth() ), - $wgLang->formatNum( $file->getHeight() ), - $wgLang->formatSize( $file->getSize() ) ); + $size = $wgLang->formatSize( $file->getSize() ); + + if ( $this->isAnimatedImage( $file ) ) { + $msg = wfMessage( 'svg-long-desc-animated' ); + } else { + $msg = wfMessage( 'svg-long-desc' ); + } + + $msg->numParams( + $file->getWidth(), + $file->getHeight() + ); + $msg->Params( $size ); + return $msg->parse(); } function getMetadata( $file, $filename ) { @@ -238,11 +275,19 @@ class SvgHandler extends ImageHandler { } function isMetadataValid( $image, $metadata ) { - return $this->unpackMetadata( $metadata ) !== false; + $meta = $this->unpackMetadata( $metadata ); + if ( $meta === false ) { + return self::METADATA_BAD; + } + if ( !isset( $meta['originalWidth'] ) ) { + // Old but compatible + return self::METADATA_COMPATIBLE; + } + return self::METADATA_GOOD; } function visibleMetadataFields() { - $fields = array( 'title', 'description', 'animated' ); + $fields = array( 'objectname', 'imagedescription' ); return $fields; } @@ -263,8 +308,6 @@ class SvgHandler extends ImageHandler { if ( !$metadata ) { return false; } - unset( $metadata['version'] ); - unset( $metadata['metadata'] ); /* non-formatted XML */ /* TODO: add a formatter $format = new FormatSVG( $metadata ); @@ -275,9 +318,10 @@ class SvgHandler extends ImageHandler { $visibleFields = $this->visibleMetadataFields(); // Rename fields to be compatible with exif, so that - // the labels for these fields work. - $conversion = array( 'width' => 'imagewidth', - 'height' => 'imagelength', + // the labels for these fields work and reuse existing messages. + $conversion = array( + 'originalwidth' => 'imagewidth', + 'originalheight' => 'imagelength', 'description' => 'imagedescription', 'title' => 'objectname', ); @@ -285,6 +329,9 @@ class SvgHandler extends ImageHandler { $tag = strtolower( $name ); if ( isset( $conversion[$tag] ) ) { $tag = $conversion[$tag]; + } else { + // Do not output other metadata not in list + continue; } self::addMeta( $result, in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed', diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php index db9f05fd..851fe428 100644 --- a/includes/media/SVGMetadataExtractor.php +++ b/includes/media/SVGMetadataExtractor.php @@ -1,6 +1,6 @@ <?php /** - * SVGMetadataExtractor.php + * Extraction of SVG image metadata. * * 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 @@ -19,12 +19,15 @@ * * @file * @ingroup Media - * @author Derk-Jan Hartman <hartman _at_ videolan d0t org> + * @author "Derk-Jan Hartman <hartman _at_ videolan d0t org>" * @author Brion Vibber * @copyright Copyright © 2010-2010 Brion Vibber, Derk-Jan Hartman * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License */ +/** + * @ingroup Media + */ class SVGMetadataExtractor { static function getMetadata( $filename ) { $svg = new SVGReader( $filename ); @@ -32,6 +35,9 @@ class SVGMetadataExtractor { } } +/** + * @ingroup Media + */ class SVGReader { const DEFAULT_WIDTH = 512; const DEFAULT_HEIGHT = 512; @@ -77,6 +83,12 @@ class SVGReader { $this->metadata['width'] = self::DEFAULT_WIDTH; $this->metadata['height'] = self::DEFAULT_HEIGHT; + // The size in the units specified by the SVG file + // (for the metadata box) + // Per the SVG spec, if unspecified, default to '100%' + $this->metadata['originalWidth'] = '100%'; + $this->metadata['originalHeight'] = '100%'; + // Because we cut off the end of the svg making an invalid one. Complicated // try catch thing to make sure warnings get restored. Seems like there should // be a better way. @@ -84,6 +96,8 @@ class SVGReader { try { $this->read(); } catch( Exception $e ) { + // Note, if this happens, the width/height will be taken to be 0x0. + // Should we consider it the default 512x512 instead? wfRestoreWarnings(); throw $e; } @@ -99,6 +113,7 @@ class SVGReader { /** * Read the SVG + * @return bool */ public function read() { $keepReading = $this->reader->read(); @@ -132,6 +147,11 @@ class SVGReader { $this->readField( $tag, 'description' ); } elseif ( $isSVG && $tag == 'metadata' && $type == XmlReader::ELEMENT ) { $this->readXml( $tag, 'metadata' ); + } elseif ( $isSVG && $tag == 'script' ) { + // We normally do not allow scripted svgs. + // However its possible to configure MW to let them + // in, and such files should be considered animated. + $this->metadata['animated'] = true; } elseif ( $tag !== '#text' ) { $this->debug( "Unhandled top-level XML tag $tag" ); @@ -212,6 +232,11 @@ class SVGReader { break; } elseif ( $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::ELEMENT ) { switch( $this->reader->localName ) { + case 'script': + // Normally we disallow files with + // <script>, but its possible + // to configure MW to disable + // such checks. case 'animate': case 'set': case 'animateMotion': @@ -248,7 +273,7 @@ class SVGReader { /** * Parse the attributes of an SVG element * - * The parser has to be in the start element of <svg> + * The parser has to be in the start element of "<svg>" */ private function handleSVGAttribs( ) { $defaultWidth = self::DEFAULT_WIDTH; @@ -271,9 +296,11 @@ class SVGReader { } if( $this->reader->getAttribute('width') ) { $width = $this->scaleSVGUnit( $this->reader->getAttribute('width'), $defaultWidth ); + $this->metadata['originalWidth'] = $this->reader->getAttribute( 'width' ); } if( $this->reader->getAttribute('height') ) { $height = $this->scaleSVGUnit( $this->reader->getAttribute('height'), $defaultHeight ); + $this->metadata['originalHeight'] = $this->reader->getAttribute( 'height' ); } if( !isset( $width ) && !isset( $height ) ) { diff --git a/includes/media/Tiff.php b/includes/media/Tiff.php index 0f317e1a..d95c9074 100644 --- a/includes/media/Tiff.php +++ b/includes/media/Tiff.php @@ -2,6 +2,21 @@ /** * Handler for Tiff images. * + * 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 Media */ diff --git a/includes/media/XCF.php b/includes/media/XCF.php index 806db73c..555fa1fb 100644 --- a/includes/media/XCF.php +++ b/includes/media/XCF.php @@ -7,6 +7,21 @@ * Specification in Gnome repository: * http://svn.gnome.org/viewvc/gimp/trunk/devel-docs/xcf.txt?view=markup * + * 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 Media */ @@ -58,7 +73,7 @@ class XCFHandler extends BitmapHandler { * @author Hashar * * @param $filename String Full path to a XCF file - * @return false|metadata array just like PHP getimagesize() + * @return bool|array metadata array just like PHP getimagesize() */ static function getXCFMetaData( $filename ) { # Decode master structure diff --git a/includes/media/XMP.php b/includes/media/XMP.php index 0dbf5632..36660b3d 100644 --- a/includes/media/XMP.php +++ b/includes/media/XMP.php @@ -1,5 +1,27 @@ <?php /** + * Reader for XMP data containing properties relevant to images. + * + * 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 Media + */ + +/** * Class for reading xmp data containing properties relevant to * images, and spitting out an array that FormatExif accepts. * @@ -191,10 +213,16 @@ class XMPReader { unset( $data['xmp-special'] ); // Convert GPSAltitude to negative if below sea level. - if ( isset( $data['xmp-exif']['GPSAltitudeRef'] ) ) { - if ( $data['xmp-exif']['GPSAltitudeRef'] == '1' - && isset( $data['xmp-exif']['GPSAltitude'] ) - ) { + if ( isset( $data['xmp-exif']['GPSAltitudeRef'] ) + && isset( $data['xmp-exif']['GPSAltitude'] ) + ) { + + // Must convert to a real before multiplying by -1 + // XMPValidate guarantees there will always be a '/' in this value. + list( $nom, $denom ) = explode( '/', $data['xmp-exif']['GPSAltitude'] ); + $data['xmp-exif']['GPSAltitude'] = $nom / $denom; + + if ( $data['xmp-exif']['GPSAltitudeRef'] == '1' ) { $data['xmp-exif']['GPSAltitude'] *= -1; } unset( $data['xmp-exif']['GPSAltitudeRef'] ); @@ -439,13 +467,15 @@ class XMPReader { * generally means we've finished processing a nested structure. * resets some internal variables to indicate that. * - * Note this means we hit the </closing element> not the </rdf:Seq>. + * Note this means we hit the closing element not the "</rdf:Seq>". * - * For example, when processing: + * @par For example, when processing: + * @code{,xml} * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li> * </rdf:Seq> </exif:ISOSpeedRatings> + * @endcode * - * This method is called when we hit the </exif:ISOSpeedRatings> tag. + * This method is called when we hit the "</exif:ISOSpeedRatings>" tag. * * @param $elm String namespace . space . tag name. */ @@ -501,15 +531,17 @@ class XMPReader { * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag ) * Add information about what type of element this is. * - * Note we still have to hit the outer </property> + * Note we still have to hit the outer "</property>" * - * For example, when processing: + * @par For example, when processing: + * @code{,xml} * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li> * </rdf:Seq> </exif:ISOSpeedRatings> + * @endcode * - * This method is called when we hit the </rdf:Seq>. + * This method is called when we hit the "</rdf:Seq>". * (For comparison, we call endElementModeSimple when we - * hit the </rdf:li>) + * hit the "</rdf:li>") * * @param $elm String namespace . ' ' . element name */ @@ -988,7 +1020,7 @@ class XMPReader { * Also does some initial set up for the wrapper element * * @param $parser XMLParser - * @param $elm String namespace <space> element + * @param $elm String namespace "<space>" element * @param $attribs Array attribute name => value */ function startElement( $parser, $elm, $attribs ) { @@ -1071,11 +1103,13 @@ class XMPReader { * Process attributes. * Simple values can be stored as either a tag or attribute * - * Often the initial <rdf:Description> tag just has all the simple + * Often the initial "<rdf:Description>" tag just has all the simple * properties as attributes. * - * Example: + * @par Example: + * @code * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10"> + * @endcode * * @param $attribs Array attribute=>value array. */ diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php index 156d9b50..83b8a102 100644 --- a/includes/media/XMPInfo.php +++ b/includes/media/XMPInfo.php @@ -1,5 +1,27 @@ <?php /** + * Definitions for XMPReader class. + * + * 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 Media + */ + +/** * This class is just a container for a big array * used by XMPReader to determine which XMP items to * extract. diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php index 600d99de..5ce3c00b 100644 --- a/includes/media/XMPValidate.php +++ b/includes/media/XMPValidate.php @@ -1,5 +1,27 @@ <?php /** + * Methods for validating XMP properties. + * + * 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 Media + */ + +/** * This contains some static methods for * validating XMP properties. See XMPInfo and XMPReader classes. * diff --git a/includes/mobile/DeviceDetection.php b/includes/mobile/DeviceDetection.php new file mode 100644 index 00000000..262665be --- /dev/null +++ b/includes/mobile/DeviceDetection.php @@ -0,0 +1,459 @@ +<?php +/** + * Mobile device detection code + * + * Copyright © 2011 Patrick Reilly + * 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 + */ + +/** + * Base for classes describing devices and their capabilities + * @since 1.20 + */ +interface IDeviceProperties { + /** + * @return string: 'html' or 'wml' + */ + function format(); + + /** + * @return bool + */ + function supportsJavaScript(); + + /** + * @return bool + */ + function supportsJQuery(); + + /** + * @return bool + */ + function disableZoom(); +} + +/** + * @since 1.20 + */ +interface IDeviceDetector { + /** + * @param $userAgent + * @param string $acceptHeader + * @return IDeviceProperties + */ + function detectDeviceProperties( $userAgent, $acceptHeader = '' ); + + /** + * @param $deviceName + * @return IDeviceProperties + */ + function getDeviceProperties( $deviceName ); + + /** + * @param $userAgent string + * @param $acceptHeader string + * @return string + */ + function detectDeviceName( $userAgent, $acceptHeader = '' ); +} + +/** + * MediaWiki's default IDeviceProperties implementation + */ +final class DeviceProperties implements IDeviceProperties { + private $device; + + public function __construct( array $deviceCapabilities ) { + $this->device = $deviceCapabilities; + } + + /** + * @return string + */ + function format() { + return $this->device['view_format']; + } + + /** + * @return bool + */ + function supportsJavaScript() { + return $this->device['supports_javascript']; + } + + /** + * @return bool + */ + function supportsJQuery() { + return $this->device['supports_jquery']; + } + + /** + * @return bool + */ + function disableZoom() { + return $this->device['disable_zoom']; + } +} + +/** + * Provides abstraction for a device. + * A device can select which format a request should receive and + * may be extended to provide access to particular device functionality. + * @since 1.20 + */ +class DeviceDetection implements IDeviceDetector { + + private static $formats = array ( + 'html' => array ( + 'view_format' => 'html', + 'css_file_name' => 'default', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'capable' => array ( + 'view_format' => 'html', + 'css_file_name' => 'default', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => true, + ), + 'webkit' => array ( + 'view_format' => 'html', + 'css_file_name' => 'webkit', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => false, + ), + 'ie' => array ( + 'view_format' => 'html', + 'css_file_name' => 'default', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => false, + ), + 'android' => array ( + 'view_format' => 'html', + 'css_file_name' => 'android', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => false, + ), + 'iphone' => array ( + 'view_format' => 'html', + 'css_file_name' => 'iphone', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => false, + ), + 'iphone2' => array ( + 'view_format' => 'html', + 'css_file_name' => 'iphone2', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => true, + ), + 'native_iphone' => array ( + 'view_format' => 'html', + 'css_file_name' => 'default', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => false, + ), + 'palm_pre' => array ( + 'view_format' => 'html', + 'css_file_name' => 'palm_pre', + 'supports_javascript' => true, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'kindle' => array ( + 'view_format' => 'html', + 'css_file_name' => 'kindle', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'kindle2' => array ( + 'view_format' => 'html', + 'css_file_name' => 'kindle', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'blackberry' => array ( + 'view_format' => 'html', + 'css_file_name' => 'blackberry', + 'supports_javascript' => true, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'blackberry-lt5' => array ( + 'view_format' => 'html', + 'css_file_name' => 'blackberry', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'netfront' => array ( + 'view_format' => 'html', + 'css_file_name' => 'simple', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'wap2' => array ( + 'view_format' => 'html', + 'css_file_name' => 'simple', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'psp' => array ( + 'view_format' => 'html', + 'css_file_name' => 'psp', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'ps3' => array ( + 'view_format' => 'html', + 'css_file_name' => 'simple', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'wii' => array ( + 'view_format' => 'html', + 'css_file_name' => 'wii', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => true, + ), + 'operamini' => array ( + 'view_format' => 'html', + 'css_file_name' => 'operamini', + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'operamobile' => array ( + 'view_format' => 'html', + 'css_file_name' => 'operamobile', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => true, + ), + 'nokia' => array ( + 'view_format' => 'html', + 'css_file_name' => 'nokia', + 'supports_javascript' => true, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + 'wml' => array ( + 'view_format' => 'wml', + 'css_file_name' => null, + 'supports_javascript' => false, + 'supports_jquery' => false, + 'disable_zoom' => true, + ), + ); + + /** + * Returns an instance of detection class, overridable by extensions + * @return IDeviceDetector + */ + public static function factory() { + global $wgDeviceDetectionClass; + + static $instance = null; + if ( !$instance ) { + $instance = new $wgDeviceDetectionClass(); + } + return $instance; + } + + /** + * @deprecated: Deprecated, will be removed once detectDeviceProperties() will be deployed everywhere on WMF + * @param $userAgent + * @param string $acceptHeader + * @return array + */ + public function detectDevice( $userAgent, $acceptHeader = '' ) { + $formatName = $this->detectFormatName( $userAgent, $acceptHeader ); + return $this->getDevice( $formatName ); + } + + /** + * @param $userAgent + * @param string $acceptHeader + * @return IDeviceProperties + */ + public function detectDeviceProperties( $userAgent, $acceptHeader = '' ) { + $deviceName = $this->detectDeviceName( $userAgent, $acceptHeader ); + return $this->getDeviceProperties( $deviceName ); + } + + /** + * @deprecated: Deprecated, will be removed once detectDeviceProperties() will be deployed everywhere on WMF + * @param $formatName + * @return array + */ + public function getDevice( $formatName ) { + return ( isset( self::$formats[$formatName] ) ) ? self::$formats[$formatName] : array(); + } + + /** + * @param $deviceName + * @return IDeviceProperties + */ + public function getDeviceProperties( $deviceName ) { + if ( isset( self::$formats[$deviceName] ) ) { + return new DeviceProperties( self::$formats[$deviceName] ); + } else { + return new DeviceProperties( array( + 'view_format' => 'html', + 'css_file_name' => 'default', + 'supports_javascript' => true, + 'supports_jquery' => true, + 'disable_zoom' => true, + ) ); + } + } + + /** + * @deprecated: Renamed to detectDeviceName() + * @param $userAgent string + * @param $acceptHeader string + * @return string + */ + public function detectFormatName( $userAgent, $acceptHeader = '' ) { + return $this->detectDeviceName( $userAgent, $acceptHeader ); + } + + /** + * @param $userAgent string + * @param $acceptHeader string + * @return string + */ + public function detectDeviceName( $userAgent, $acceptHeader = '' ) { + wfProfileIn( __METHOD__ ); + + $deviceName = ''; + if ( preg_match( '/Android/', $userAgent ) ) { + $deviceName = 'android'; + if ( strpos( $userAgent, 'Opera Mini' ) !== false ) { + $deviceName = 'operamini'; + } elseif ( strpos( $userAgent, 'Opera Mobi' ) !== false ) { + $deviceName = 'operamobile'; + } + } elseif ( preg_match( '/MSIE 9.0/', $userAgent ) || + preg_match( '/MSIE 8.0/', $userAgent ) ) { + $deviceName = 'ie'; + } elseif( preg_match( '/MSIE/', $userAgent ) ) { + $deviceName = 'html'; + } elseif ( strpos( $userAgent, 'Opera Mobi' ) !== false ) { + $deviceName = 'operamobile'; + } elseif ( preg_match( '/iPad.* Safari/', $userAgent ) ) { + $deviceName = 'iphone'; + } elseif ( preg_match( '/iPhone.* Safari/', $userAgent ) ) { + if ( strpos( $userAgent, 'iPhone OS 2' ) !== false ) { + $deviceName = 'iphone2'; + } else { + $deviceName = 'iphone'; + } + } elseif ( preg_match( '/iPhone/', $userAgent ) ) { + if ( strpos( $userAgent, 'Opera' ) !== false ) { + $deviceName = 'operamini'; + } else { + $deviceName = 'native_iphone'; + } + } elseif ( preg_match( '/WebKit/', $userAgent ) ) { + if ( preg_match( '/Series60/', $userAgent ) ) { + $deviceName = 'nokia'; + } elseif ( preg_match( '/webOS/', $userAgent ) ) { + $deviceName = 'palm_pre'; + } else { + $deviceName = 'webkit'; + } + } elseif ( preg_match( '/Opera/', $userAgent ) ) { + if ( strpos( $userAgent, 'Nintendo Wii' ) !== false ) { + $deviceName = 'wii'; + } elseif ( strpos( $userAgent, 'Opera Mini' ) !== false ) { + $deviceName = 'operamini'; + } else { + $deviceName = 'operamobile'; + } + } elseif ( preg_match( '/Kindle\/1.0/', $userAgent ) ) { + $deviceName = 'kindle'; + } elseif ( preg_match( '/Kindle\/2.0/', $userAgent ) ) { + $deviceName = 'kindle2'; + } elseif ( preg_match( '/Firefox/', $userAgent ) ) { + $deviceName = 'capable'; + } elseif ( preg_match( '/NetFront/', $userAgent ) ) { + $deviceName = 'netfront'; + } elseif ( preg_match( '/SEMC-Browser/', $userAgent ) ) { + $deviceName = 'wap2'; + } elseif ( preg_match( '/Series60/', $userAgent ) ) { + $deviceName = 'wap2'; + } elseif ( preg_match( '/PlayStation Portable/', $userAgent ) ) { + $deviceName = 'psp'; + } elseif ( preg_match( '/PLAYSTATION 3/', $userAgent ) ) { + $deviceName = 'ps3'; + } elseif ( preg_match( '/SAMSUNG/', $userAgent ) ) { + $deviceName = 'capable'; + } elseif ( preg_match( '/BlackBerry/', $userAgent ) ) { + if( preg_match( '/BlackBerry[^\/]*\/[1-4]\./', $userAgent ) ) { + $deviceName = 'blackberry-lt5'; + } else { + $deviceName = 'blackberry'; + } + } + + if ( $deviceName === '' ) { + if ( strpos( $acceptHeader, 'application/vnd.wap.xhtml+xml' ) !== false ) { + // Should be wap2 + $deviceName = 'html'; + } elseif ( strpos( $acceptHeader, 'vnd.wap.wml' ) !== false ) { + $deviceName = 'wml'; + } else { + $deviceName = 'html'; + } + } + wfProfileOut( __METHOD__ ); + return $deviceName; + } + + /** + * @return array: List of all device-specific stylesheets + */ + public function getCssFiles() { + $files = array(); + + foreach ( self::$formats as $dev ) { + if ( isset( $dev['css_file_name'] ) ) { + $files[] = $dev['css_file_name']; + } + } + return array_unique( $files ); + } +} diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php index d96cb09a..23471e94 100644 --- a/includes/normal/RandomTest.php +++ b/includes/normal/RandomTest.php @@ -57,10 +57,6 @@ function donorm( $str ) { return rtrim( utf8_normalize( $str . "\x01", UtfNormal::UNORM_NFC ), "\x01" ); } -function wfMsg($x) { - return $x; -} - function showDiffs( $a, $b ) { $ota = explode( "\n", str_replace( "\r\n", "\n", $a ) ); $nta = explode( "\n", str_replace( "\r\n", "\n", $b ) ); diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php index b5aad301..08f85bd3 100644 --- a/includes/normal/UtfNormal.php +++ b/includes/normal/UtfNormal.php @@ -190,7 +190,7 @@ class UtfNormal { */ static function loadData() { if( !isset( self::$utfCombiningClass ) ) { - require_once( dirname(__FILE__) . '/UtfNormalData.inc' ); + require_once( __DIR__ . '/UtfNormalData.inc' ); } } @@ -238,6 +238,7 @@ class UtfNormal { * Returns true if the string is _definitely_ in NFC. * Returns false if not or uncertain. * @param $string String: a UTF-8 string, altered on output to be valid UTF-8 safe for XML. + * @return bool */ static function quickIsNFCVerify( &$string ) { # Screen out some characters that eg won't be allowed in XML diff --git a/includes/normal/UtfNormalDefines.php b/includes/normal/UtfNormalDefines.php index 6c4d8b76..5142a414 100644 --- a/includes/normal/UtfNormalDefines.php +++ b/includes/normal/UtfNormalDefines.php @@ -6,6 +6,21 @@ * since this file will not be executed during request startup for a compiled * MediaWiki. * + * 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 UtfNormal */ diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php index e5ae7f72..5872ec34 100644 --- a/includes/normal/UtfNormalTest.php +++ b/includes/normal/UtfNormalTest.php @@ -37,6 +37,7 @@ if( defined( 'PRETTY_UTF8' ) ) { } else { /** * @ignore + * @return string */ function pretty( $string ) { return trim( preg_replace( '/(.)/use', diff --git a/includes/normal/UtfNormalTest2.php b/includes/normal/UtfNormalTest2.php index 28be4838..691bfaa7 100644 --- a/includes/normal/UtfNormalTest2.php +++ b/includes/normal/UtfNormalTest2.php @@ -1,7 +1,22 @@ #!/usr/bin/php <?php /** - * Other tests for the unicode normalization module + * Other tests for the unicode normalization module. + * + * 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 UtfNormal @@ -61,6 +76,7 @@ function normalize_form_kd($c) { return UtfNormal::toNFKD($c); } * following functions to force pure PHP usage. I decided not to * commit that code since might produce a slowdown in the UTF * normalization code just for the sake of these tests. -- hexmode + * @return string */ function normalize_form_c_php($c) { return UtfNormal::toNFC($c, "php"); } function normalize_form_d_php($c) { return UtfNormal::toNFD($c, "php"); } diff --git a/includes/objectcache/APCBagOStuff.php b/includes/objectcache/APCBagOStuff.php index dd4a76e1..1a0de218 100644 --- a/includes/objectcache/APCBagOStuff.php +++ b/includes/objectcache/APCBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using PHP's APC accelerator. + * + * 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 + */ /** * This is a wrapper for APC's shared memory functions @@ -6,28 +27,62 @@ * @ingroup Cache */ class APCBagOStuff extends BagOStuff { + /** + * @param $key string + * @return mixed + */ public function get( $key ) { $val = apc_fetch( $key ); if ( is_string( $val ) ) { - $val = unserialize( $val ); + if ( $this->isInteger( $val ) ) { + $val = intval( $val ); + } else { + $val = unserialize( $val ); + } } return $val; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function set( $key, $value, $exptime = 0 ) { - apc_store( $key, serialize( $value ), $exptime ); + if ( !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + + apc_store( $key, $value, $exptime ); return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { apc_delete( $key ); return true; } + public function incr( $key, $value = 1 ) { + return apc_inc( $key, $value ); + } + + public function decr( $key, $value = 1 ) { + return apc_dec( $key, $value ); + } + + /** + * @return Array + */ public function keys() { $info = apc_cache_info( 'user' ); $list = $info['cache_list']; @@ -40,4 +95,3 @@ class APCBagOStuff extends BagOStuff { return $keys; } } - diff --git a/includes/objectcache/BagOStuff.php b/includes/objectcache/BagOStuff.php index 81ad6621..7bbaff93 100644 --- a/includes/objectcache/BagOStuff.php +++ b/includes/objectcache/BagOStuff.php @@ -56,8 +56,7 @@ abstract class BagOStuff { /** * Get an item with the given key. Returns false if it does not exist. * @param $key string - * - * @return bool|Object + * @return mixed Returns false on failure */ abstract public function get( $key ); @@ -66,6 +65,7 @@ abstract class BagOStuff { * @param $key string * @param $value mixed * @param $exptime int Either an interval in seconds or a unix timestamp for expiry + * @return bool success */ abstract public function set( $key, $value, $exptime = 0 ); @@ -73,19 +73,33 @@ abstract class BagOStuff { * Delete an item. * @param $key string * @param $time int Amount of time to delay the operation (mostly memcached-specific) + * @return bool True if the item was deleted or not found, false on failure */ abstract public function delete( $key, $time = 0 ); + /** + * @param $key string + * @param $timeout integer + * @return bool success + */ public function lock( $key, $timeout = 0 ) { /* stub */ return true; } + /** + * @param $key string + * @return bool success + */ public function unlock( $key ) { /* stub */ return true; } + /** + * @todo: what is this? + * @return Array + */ public function keys() { /* stub */ return array(); @@ -93,12 +107,12 @@ abstract class BagOStuff { /** * Delete all objects expiring before a certain date. - * @param $date The reference date in MW format - * @param $progressCallback Optional, a function which will be called + * @param $date string The reference date in MW format + * @param $progressCallback callback|bool Optional, a function which will be called * regularly during long-running operations with the percentage progress * as the first parameter. * - * @return true on success, false if unimplemented + * @return bool on success, false if unimplemented */ public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { // stub @@ -107,45 +121,83 @@ abstract class BagOStuff { /* *** Emulated functions *** */ - public function add( $key, $value, $exptime = 0 ) { - if ( !$this->get( $key ) ) { - $this->set( $key, $value, $exptime ); + /** + * Get an associative array containing the item for each of the keys that have items. + * @param $keys Array List of strings + * @return Array + */ + public function getMulti( array $keys ) { + $res = array(); + foreach ( $keys as $key ) { + $val = $this->get( $key ); + if ( $val !== false ) { + $res[$key] = $val; + } + } + return $res; + } - return true; + /** + * @param $key string + * @param $value mixed + * @param $exptime integer + * @return bool success + */ + public function add( $key, $value, $exptime = 0 ) { + if ( $this->get( $key ) === false ) { + return $this->set( $key, $value, $exptime ); } + return false; // key already set } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool success + */ public function replace( $key, $value, $exptime = 0 ) { if ( $this->get( $key ) !== false ) { - $this->set( $key, $value, $exptime ); + return $this->set( $key, $value, $exptime ); } + return false; // key not already set } /** + * Increase stored value of $key by $value while preserving its TTL * @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 + * @return integer|bool New value or false on failure */ public function incr( $key, $value = 1 ) { if ( !$this->lock( $key ) ) { - return null; + return false; } - - $value = intval( $value ); - - if ( ( $n = $this->get( $key ) ) !== false ) { - $n += $value; - $this->set( $key, $n ); // exptime? + $n = $this->get( $key ); + if ( $this->isInteger( $n ) ) { // key exists? + $n += intval( $value ); + $this->set( $key, max( 0, $n ) ); // exptime? + } else { + $n = false; } $this->unlock( $key ); return $n; } + /** + * Decrease stored value of $key by $value while preserving its TTL + * @param $key String + * @param $value Integer + * @return integer + */ public function decr( $key, $value = 1 ) { return $this->incr( $key, - $value ); } + /** + * @param $text string + */ public function debug( $text ) { if ( $this->debugMode ) { $class = get_class( $this ); @@ -155,6 +207,8 @@ abstract class BagOStuff { /** * Convert an optionally relative time to an absolute time + * @param $exptime integer + * @return int */ protected function convertExpiry( $exptime ) { if ( ( $exptime != 0 ) && ( $exptime < 86400 * 3650 /* 10 years */ ) ) { @@ -163,6 +217,33 @@ abstract class BagOStuff { return $exptime; } } -} + /** + * Convert an optionally absolute expiry time to a relative time. If an + * absolute time is specified which is in the past, use a short expiry time. + * + * @param $exptime integer + * @return integer + */ + protected function convertToRelative( $exptime ) { + if ( $exptime >= 86400 * 3650 /* 10 years */ ) { + $exptime -= time(); + if ( $exptime <= 0 ) { + $exptime = 1; + } + return $exptime; + } else { + return $exptime; + } + } + /** + * Check if a value is an integer + * + * @param $value mixed + * @return bool + */ + protected function isInteger( $value ) { + return ( is_int( $value ) || ctype_digit( $value ) ); + } +} diff --git a/includes/objectcache/DBABagOStuff.php b/includes/objectcache/DBABagOStuff.php index ade8c0a9..36ced496 100644 --- a/includes/objectcache/DBABagOStuff.php +++ b/includes/objectcache/DBABagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using DBA backend. + * + * 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 + */ /** * Cache that uses DBA as a backend. @@ -7,23 +28,24 @@ * for systems that don't have it. * * On construction you can pass array( 'dir' => '/some/path' ); as a parameter - * to override the default DBA files directory (wgTmpDirectory). + * to override the default DBA files directory (wfTempDir()). * * @ingroup Cache */ class DBABagOStuff extends BagOStuff { var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; + /** + * @param $params array + */ public function __construct( $params ) { global $wgDBAhandler; if ( !isset( $params['dir'] ) ) { - global $wgTmpDirectory; - $params['dir'] = $wgTmpDirectory; + $params['dir'] = wfTempDir(); } - $this->mFile = $params['dir']."/mw-cache-" . wfWikiID(); - $this->mFile .= '.db'; + $this->mFile = $params['dir'] . '/mw-cache-' . wfWikiID() . '.db'; wfDebug( __CLASS__ . ": using cache file {$this->mFile}\n" ); $this->mHandler = $wgDBAhandler; } @@ -35,7 +57,7 @@ class DBABagOStuff extends BagOStuff { * * @return string */ - function encode( $value, $expiry ) { + protected function encode( $value, $expiry ) { # Convert to absolute time $expiry = $this->convertExpiry( $expiry ); @@ -43,11 +65,12 @@ class DBABagOStuff extends BagOStuff { } /** + * @param $blob string * @return array list containing value first and expiry second */ - function decode( $blob ) { + protected function decode( $blob ) { if ( !is_string( $blob ) ) { - return array( null, 0 ); + return array( false, 0 ); } else { return array( unserialize( substr( $blob, 11 ) ), @@ -56,7 +79,10 @@ class DBABagOStuff extends BagOStuff { } } - function getReader() { + /** + * @return resource + */ + protected function getReader() { if ( file_exists( $this->mFile ) ) { $handle = dba_open( $this->mFile, 'rl', $this->mHandler ); } else { @@ -70,7 +96,10 @@ class DBABagOStuff extends BagOStuff { return $handle; } - function getWriter() { + /** + * @return resource + */ + protected function getWriter() { $handle = dba_open( $this->mFile, 'cl', $this->mHandler ); if ( !$handle ) { @@ -80,14 +109,18 @@ class DBABagOStuff extends BagOStuff { return $handle; } - function get( $key ) { + /** + * @param $key string + * @return mixed + */ + public function get( $key ) { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__ . "($key)\n" ); $handle = $this->getReader(); if ( !$handle ) { wfProfileOut( __METHOD__ ); - return null; + return false; } $val = dba_fetch( $key, $handle ); @@ -96,20 +129,26 @@ class DBABagOStuff extends BagOStuff { # Must close ASAP because locks are held dba_close( $handle ); - if ( !is_null( $val ) && $expiry && $expiry < time() ) { + if ( $val !== false && $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; + $val = false; } wfProfileOut( __METHOD__ ); return $val; } - function set( $key, $value, $exptime = 0 ) { + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ + public function set( $key, $value, $exptime = 0 ) { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__ . "($key)\n" ); @@ -128,7 +167,12 @@ class DBABagOStuff extends BagOStuff { return $ret; } - function delete( $key, $time = 0 ) { + /** + * @param $key string + * @param $time int + * @return bool + */ + public function delete( $key, $time = 0 ) { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__ . "($key)\n" ); @@ -138,14 +182,20 @@ class DBABagOStuff extends BagOStuff { return false; } - $ret = dba_delete( $key, $handle ); + $ret = !dba_exists( $key, $handle ) || dba_delete( $key, $handle ); dba_close( $handle ); wfProfileOut( __METHOD__ ); return $ret; } - function add( $key, $value, $exptime = 0 ) { + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ + public function add( $key, $value, $exptime = 0 ) { wfProfileIn( __METHOD__ ); $blob = $this->encode( $value, $exptime ); @@ -163,7 +213,7 @@ class DBABagOStuff extends BagOStuff { if ( !$ret ) { list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) ); - if ( $expiry < time() ) { + if ( $expiry && $expiry < time() ) { # Yes expired, delete and try again dba_delete( $key, $handle ); $ret = dba_insert( $key, $blob, $handle ); @@ -177,6 +227,44 @@ class DBABagOStuff extends BagOStuff { return $ret; } + /** + * @param $key string + * @param $step integer + * @return integer|bool + */ + public function incr( $key, $step = 1 ) { + wfProfileIn( __METHOD__ ); + + $handle = $this->getWriter(); + + if ( !$handle ) { + wfProfileOut( __METHOD__ ); + return false; + } + + list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) ); + if ( $value !== false ) { + if ( $expiry && $expiry < time() ) { + # Key is expired, delete it + dba_delete( $key, $handle ); + wfDebug( __METHOD__ . ": $key expired\n" ); + $value = false; + } else { + $value += $step; + $blob = $this->encode( $value, $expiry ); + + $ret = dba_replace( $key, $blob, $handle ); + $value = $ret ? $value : false; + } + } + + dba_close( $handle ); + + wfProfileOut( __METHOD__ ); + + return ( $value === false ) ? false : (int)$value; + } + function keys() { $reader = $this->getReader(); $k1 = dba_firstkey( $reader ); @@ -196,4 +284,3 @@ class DBABagOStuff extends BagOStuff { return $result; } } - diff --git a/includes/objectcache/EhcacheBagOStuff.php b/includes/objectcache/EhcacheBagOStuff.php index 75aad27a..f86cf157 100644 --- a/includes/objectcache/EhcacheBagOStuff.php +++ b/includes/objectcache/EhcacheBagOStuff.php @@ -1,8 +1,31 @@ <?php +/** + * Object caching using the Ehcache RESTful web service. + * + * 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 + */ /** * Client for the Ehcache RESTful web service - http://ehcache.org/documentation/cache_server.html * TODO: Simplify configuration and add to the installer. + * + * @ingroup Cache */ class EhcacheBagOStuff extends BagOStuff { var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions, @@ -10,6 +33,9 @@ class EhcacheBagOStuff extends BagOStuff { var $curls = array(); + /** + * @param $params array + */ function __construct( $params ) { if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) { throw new MWException( __CLASS__.' requires curl version 7.16.2 or later.' ); @@ -36,6 +62,10 @@ class EhcacheBagOStuff extends BagOStuff { ); } + /** + * @param $key string + * @return bool|mixed + */ public function get( $key ) { wfProfileIn( __METHOD__ ); $response = $this->doItemRequest( $key ); @@ -70,6 +100,12 @@ class EhcacheBagOStuff extends BagOStuff { return $data; } + /** + * @param $key string + * @param $value mixed + * @param $expiry int + * @return bool + */ public function set( $key, $value, $expiry = 0 ) { wfProfileIn( __METHOD__ ); $expiry = $this->convertExpiry( $expiry ); @@ -107,6 +143,11 @@ class EhcacheBagOStuff extends BagOStuff { return $result; } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { wfProfileIn( __METHOD__ ); $response = $this->doItemRequest( $key, @@ -122,6 +163,10 @@ class EhcacheBagOStuff extends BagOStuff { return $result; } + /** + * @param $key string + * @return string + */ protected function getCacheUrl( $key ) { if ( count( $this->servers ) == 1 ) { $server = reset( $this->servers ); @@ -149,6 +194,13 @@ class EhcacheBagOStuff extends BagOStuff { return $this->curls[$cacheUrl]; } + /** + * @param $key string + * @param $data + * @param $type + * @param $ttl + * @return int + */ protected function attemptPut( $key, $data, $type, $ttl ) { // In initial benchmarking, it was 30 times faster to use CURLOPT_POST // than CURLOPT_UPLOAD with CURLOPT_READFUNCTION. This was because @@ -173,6 +225,10 @@ class EhcacheBagOStuff extends BagOStuff { } } + /** + * @param $key string + * @return bool + */ protected function createCache( $key ) { wfDebug( __METHOD__.": creating cache for $key\n" ); $response = $this->doCacheRequest( $key, @@ -185,21 +241,26 @@ class EhcacheBagOStuff extends BagOStuff { wfDebug( __CLASS__.": failed to create cache for $key\n" ); return false; } - if ( $response['http_code'] == 201 /* created */ - || $response['http_code'] == 409 /* already there */ ) - { - return true; - } else { - return false; - } + return ( $response['http_code'] == 201 /* created */ + || $response['http_code'] == 409 /* already there */ ); } + /** + * @param $key string + * @param $curlOptions array + * @return array|bool|mixed + */ protected function doCacheRequest( $key, $curlOptions = array() ) { $cacheUrl = $this->getCacheUrl( $key ); $curl = $this->getCurl( $cacheUrl ); return $this->doRequest( $curl, $cacheUrl, $curlOptions ); } + /** + * @param $key string + * @param $curlOptions array + * @return array|bool|mixed + */ protected function doItemRequest( $key, $curlOptions = array() ) { $cacheUrl = $this->getCacheUrl( $key ); $curl = $this->getCurl( $cacheUrl ); @@ -207,6 +268,13 @@ class EhcacheBagOStuff extends BagOStuff { return $this->doRequest( $curl, $url, $curlOptions ); } + /** + * @param $curl + * @param $url string + * @param $curlOptions array + * @return array|bool|mixed + * @throws MWException + */ protected function doRequest( $curl, $url, $curlOptions = array() ) { if ( array_diff_key( $curlOptions, $this->curlOptions ) ) { // var_dump( array_diff_key( $curlOptions, $this->curlOptions ) ); diff --git a/includes/objectcache/EmptyBagOStuff.php b/includes/objectcache/EmptyBagOStuff.php index 2aee6b12..bd28b241 100644 --- a/includes/objectcache/EmptyBagOStuff.php +++ b/includes/objectcache/EmptyBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Dummy object caching. + * + * 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 + */ /** * A BagOStuff object with no objects in it. Used to provide a no-op object to calling code. @@ -6,14 +27,30 @@ * @ingroup Cache */ class EmptyBagOStuff extends BagOStuff { + + /** + * @param $key string + * @return bool + */ function get( $key ) { return false; } + /** + * @param $key string + * @param $value mixed + * @param $exp int + * @return bool + */ function set( $key, $value, $exp = 0 ) { return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ function delete( $key, $time = 0 ) { return true; } diff --git a/includes/objectcache/HashBagOStuff.php b/includes/objectcache/HashBagOStuff.php index 36773306..799f26a3 100644 --- a/includes/objectcache/HashBagOStuff.php +++ b/includes/objectcache/HashBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using PHP arrays. + * + * 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 + */ /** * This is a test of the interface, mainly. It stores things in an associative @@ -13,6 +34,10 @@ class HashBagOStuff extends BagOStuff { $this->bag = array(); } + /** + * @param $key string + * @return bool + */ protected function expire( $key ) { $et = $this->bag[$key][1]; @@ -25,6 +50,10 @@ class HashBagOStuff extends BagOStuff { return true; } + /** + * @param $key string + * @return bool|mixed + */ function get( $key ) { if ( !isset( $this->bag[$key] ) ) { return false; @@ -37,10 +66,22 @@ class HashBagOStuff extends BagOStuff { return $this->bag[$key][0]; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ function set( $key, $value, $exptime = 0 ) { $this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) ); + return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ function delete( $key, $time = 0 ) { if ( !isset( $this->bag[$key] ) ) { return false; @@ -51,6 +92,9 @@ class HashBagOStuff extends BagOStuff { return true; } + /** + * @return array + */ function keys() { return array_keys( $this->bag ); } diff --git a/includes/objectcache/MemcachedBagOStuff.php b/includes/objectcache/MemcachedBagOStuff.php new file mode 100644 index 00000000..813c2727 --- /dev/null +++ b/includes/objectcache/MemcachedBagOStuff.php @@ -0,0 +1,180 @@ +<?php +/** + * Base class for memcached clients. + * + * 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 + */ + +/** + * Base class for memcached clients. + * + * @ingroup Cache + */ +class MemcachedBagOStuff extends BagOStuff { + protected $client; + + /** + * Fill in the defaults for any parameters missing from $params, using the + * backwards-compatible global variables + */ + protected function applyDefaultParams( $params ) { + if ( !isset( $params['servers'] ) ) { + $params['servers'] = $GLOBALS['wgMemCachedServers']; + } + if ( !isset( $params['debug'] ) ) { + $params['debug'] = $GLOBALS['wgMemCachedDebug']; + } + if ( !isset( $params['persistent'] ) ) { + $params['persistent'] = $GLOBALS['wgMemCachedPersistent']; + } + if ( !isset( $params['compress_threshold'] ) ) { + $params['compress_threshold'] = 1500; + } + if ( !isset( $params['timeout'] ) ) { + $params['timeout'] = $GLOBALS['wgMemCachedTimeout']; + } + if ( !isset( $params['connect_timeout'] ) ) { + $params['connect_timeout'] = 0.5; + } + return $params; + } + + /** + * @param $key string + * @return Mixed + */ + public function get( $key ) { + return $this->client->get( $this->encodeKey( $key ) ); + } + + /** + * @param $key string + * @param $value + * @param $exptime int + * @return bool + */ + public function set( $key, $value, $exptime = 0 ) { + return $this->client->set( $this->encodeKey( $key ), $value, + $this->fixExpiry( $exptime ) ); + } + + /** + * @param $key string + * @param $time int + * @return bool + */ + public function delete( $key, $time = 0 ) { + return $this->client->delete( $this->encodeKey( $key ), $time ); + } + + /** + * @param $key string + * @param $value int + * @param $exptime int (default 0) + * @return Mixed + */ + public function add( $key, $value, $exptime = 0 ) { + return $this->client->add( $this->encodeKey( $key ), $value, + $this->fixExpiry( $exptime ) ); + } + + /** + * @param $key string + * @param $value int + * @param $exptime + * @return Mixed + */ + public function replace( $key, $value, $exptime = 0 ) { + return $this->client->replace( $this->encodeKey( $key ), $value, + $this->fixExpiry( $exptime ) ); + } + + /** + * Get the underlying client object. This is provided for debugging + * purposes. + */ + public function getClient() { + return $this->client; + } + + /** + * Encode a key for use on the wire inside the memcached protocol. + * + * We encode spaces and line breaks to avoid protocol errors. We encode + * the other control characters for compatibility with libmemcached + * verify_key. We leave other punctuation alone, to maximise backwards + * compatibility. + * @param $key string + * @return string + */ + public function encodeKey( $key ) { + return preg_replace_callback( '/[\x00-\x20\x25\x7f]+/', + array( $this, 'encodeKeyCallback' ), $key ); + } + + /** + * @param $m array + * @return string + */ + protected function encodeKeyCallback( $m ) { + return rawurlencode( $m[0] ); + } + + /** + * TTLs higher than 30 days will be detected as absolute TTLs + * (UNIX timestamps), and will result in the cache entry being + * discarded immediately because the expiry is in the past. + * Clamp expiries >30d at 30d, unless they're >=1e9 in which + * case they are likely to really be absolute (1e9 = 2011-09-09) + */ + function fixExpiry( $expiry ) { + if ( $expiry > 2592000 && $expiry < 1000000000 ) { + $expiry = 2592000; + } + return $expiry; + } + + /** + * Decode a key encoded with encodeKey(). This is provided as a convenience + * function for debugging. + * + * @param $key string + * + * @return string + */ + public function decodeKey( $key ) { + return urldecode( $key ); + } + + /** + * Send a debug message to the log + */ + protected function debugLog( $text ) { + global $wgDebugLogGroups; + if( !isset( $wgDebugLogGroups['memcached'] ) ) { + # Prefix message since it will end up in main debug log file + $text = "memcached: $text"; + } + if ( substr( $text, -1 ) !== "\n" ) { + $text .= "\n"; + } + wfDebugLog( 'memcached', $text ); + } +} + diff --git a/includes/objectcache/MemcachedClient.php b/includes/objectcache/MemcachedClient.php index 868ad69f..536ba6ea 100644 --- a/includes/objectcache/MemcachedClient.php +++ b/includes/objectcache/MemcachedClient.php @@ -1,5 +1,7 @@ <?php /** + * Memcached client for PHP. + * * +---------------------------------------------------------------------------+ * | memcached client, PHP | * +---------------------------------------------------------------------------+ @@ -257,7 +259,7 @@ class MWMemcached { $this->_host_dead = array(); $this->_timeout_seconds = 0; - $this->_timeout_microseconds = isset( $args['timeout'] ) ? $args['timeout'] : 100000; + $this->_timeout_microseconds = isset( $args['timeout'] ) ? $args['timeout'] : 500000; $this->_connect_timeout = isset( $args['connect_timeout'] ) ? $args['connect_timeout'] : 0.1; $this->_connect_attempts = 2; @@ -328,27 +330,36 @@ class MWMemcached { $this->stats['delete'] = 1; } $cmd = "delete $key $time\r\n"; - if( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { - $this->_dead_sock( $sock ); + if( !$this->_fwrite( $sock, $cmd ) ) { return false; } - $res = trim( fgets( $sock ) ); + $res = $this->_fgets( $sock ); if ( $this->_debug ) { $this->_debugprint( sprintf( "MemCache: delete %s (%s)\n", $key, $res ) ); } - if ( $res == "DELETED" ) { + if ( $res == "DELETED" || $res == "NOT_FOUND" ) { return true; } + return false; } + /** + * @param $key + * @param $timeout int + * @return bool + */ public function lock( $key, $timeout = 0 ) { /* stub */ return true; } + /** + * @param $key + * @return bool + */ public function unlock( $key ) { /* stub */ return true; @@ -427,8 +438,7 @@ class MWMemcached { } $cmd = "get $key\r\n"; - if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { - $this->_dead_sock( $sock ); + if ( !$this->_fwrite( $sock, $cmd ) ) { wfProfileOut( __METHOD__ ); return false; } @@ -471,7 +481,7 @@ class MWMemcached { $this->stats['get_multi'] = 1; } $sock_keys = array(); - + $socks = array(); foreach ( $keys as $key ) { $sock = $this->get_sock( $key ); if ( !is_resource( $sock ) ) { @@ -479,24 +489,23 @@ class MWMemcached { } $key = is_array( $key ) ? $key[1] : $key; if ( !isset( $sock_keys[$sock] ) ) { - $sock_keys[$sock] = array(); + $sock_keys[ intval( $sock ) ] = array(); $socks[] = $sock; } - $sock_keys[$sock][] = $key; + $sock_keys[ intval( $sock ) ][] = $key; } + $gather = array(); // Send out the requests foreach ( $socks as $sock ) { $cmd = 'get'; - foreach ( $sock_keys[$sock] as $key ) { + foreach ( $sock_keys[ intval( $sock ) ] as $key ) { $cmd .= ' ' . $key; } $cmd .= "\r\n"; - if ( $this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { + if ( $this->_fwrite( $sock, $cmd ) ) { $gather[] = $sock; - } else { - $this->_dead_sock( $sock ); } } @@ -559,12 +568,6 @@ class MWMemcached { * Passes through $cmd to the memcache server connected by $sock; returns * output as an array (null array if no output) * - * NOTE: due to a possible bug in how PHP reads while using fgets(), each - * line may not be terminated by a \r\n. More specifically, my testing - * has shown that, on FreeBSD at least, each line is terminated only - * with a \n. This is with the PHP flag auto_detect_line_endings set - * to falase (the default). - * * @param $sock Resource: socket to send command on * @param $cmd String: command to run * @@ -575,12 +578,13 @@ class MWMemcached { return array(); } - if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { + if ( !$this->_fwrite( $sock, $cmd ) ) { return array(); } + $ret = array(); while ( true ) { - $res = fgets( $sock ); + $res = $this->_fgets( $sock ); $ret[] = $res; if ( preg_match( '/^END/', $res ) ) { break; @@ -717,15 +721,19 @@ class MWMemcached { wfRestoreWarnings(); } if ( !$sock ) { - if ( $this->_debug ) { - $this->_debugprint( "Error connecting to $host: $errstr\n" ); - } + $this->_error_log( "Error connecting to $host: $errstr\n" ); + $this->_dead_host( $host ); return false; } // Initialise timeout stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds ); + // If the connection was persistent, flush the read buffer in case there + // was a previous incomplete request on this connection + if ( $this->_persistent ) { + $this->_flush_read_buffer( $sock ); + } return true; } @@ -744,6 +752,9 @@ class MWMemcached { $this->_dead_host( $host ); } + /** + * @param $host + */ function _dead_host( $host ) { $parts = explode( ':', $host ); $ip = $parts[0]; @@ -769,13 +780,12 @@ class MWMemcached { } if ( $this->_single_sock !== null ) { - $this->_flush_read_buffer( $this->_single_sock ); return $this->sock_to_host( $this->_single_sock ); } $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key ); - if ( $this->_buckets === null ) { + $bu = array(); foreach ( $this->_servers as $v ) { if ( is_array( $v ) ) { for( $i = 0; $i < $v[1]; $i++ ) { @@ -794,7 +804,6 @@ class MWMemcached { $host = $this->_buckets[$hv % $this->_bucketcount]; $sock = $this->sock_to_host( $host ); if ( is_resource( $sock ) ) { - $this->_flush_read_buffer( $sock ); return $sock; } $hv = $this->_hashfunc( $hv . $realkey ); @@ -815,7 +824,7 @@ class MWMemcached { * @access private */ function _hashfunc( $key ) { - # Hash function must on [0,0x7ffffff] + # Hash function must be in [0,0x7ffffff] # We take the first 31 bits of the MD5 hash, which unlike the hash # function used in a previous version of this client, works return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff; @@ -850,11 +859,11 @@ class MWMemcached { } else { $this->stats[$cmd] = 1; } - if ( !$this->_safe_fwrite( $sock, "$cmd $key $amt\r\n" ) ) { - return $this->_dead_sock( $sock ); + if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) { + return null; } - $line = fgets( $sock ); + $line = $this->_fgets( $sock ); $match = array(); if ( !preg_match( '/^(\d+)/', $line, $match ) ) { return null; @@ -870,58 +879,42 @@ class MWMemcached { * * @param $sock Resource: socket to read from * @param $ret Array: returned values + * @return boolean True for success, false for failure * * @access private */ function _load_items( $sock, &$ret ) { while ( 1 ) { - $decl = fgets( $sock ); - if ( $decl == "END\r\n" ) { + $decl = $this->_fgets( $sock ); + if( $decl === false ) { + return false; + } elseif ( $decl == "END" ) { return true; - } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) { + } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)$/', $decl, $match ) ) { list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] ); - $bneed = $len + 2; - $offset = 0; - - while ( $bneed > 0 ) { - $data = fread( $sock, $bneed ); - $n = strlen( $data ); - if ( $n == 0 ) { - break; - } - $offset += $n; - $bneed -= $n; - if ( isset( $ret[$rkey] ) ) { - $ret[$rkey] .= $data; - } else { - $ret[$rkey] = $data; - } + $data = $this->_fread( $sock, $len + 2 ); + if ( $data === false ) { + return false; } - - if ( $offset != $len + 2 ) { - // Something is borked! - if ( $this->_debug ) { - $this->_debugprint( sprintf( "Something is borked! key %s expecting %d got %d length\n", $rkey, $len + 2, $offset ) ); - } - - unset( $ret[$rkey] ); - $this->_close_sock( $sock ); + if ( substr( $data, -2 ) !== "\r\n" ) { + $this->_handle_error( $sock, + 'line ending missing from data block from $1' ); return false; } + $data = substr( $data, 0, -2 ); + $ret[$rkey] = $data; if ( $this->_have_zlib && $flags & self::COMPRESSED ) { $ret[$rkey] = gzuncompress( $ret[$rkey] ); } - $ret[$rkey] = rtrim( $ret[$rkey] ); - if ( $flags & self::SERIALIZED ) { $ret[$rkey] = unserialize( $ret[$rkey] ); } } else { - $this->_debugprint( "Error parsing memcached response\n" ); - return 0; + $this->_handle_error( $sock, 'Error parsing response from $1' ); + return false; } } } @@ -960,15 +953,6 @@ class MWMemcached { $this->stats[$cmd] = 1; } - // TTLs higher than 30 days will be detected as absolute TTLs - // (UNIX timestamps), and will result in the cache entry being - // discarded immediately because the expiry is in the past. - // Clamp expiries >30d at 30d, unless they're >=1e9 in which - // case they are likely to really be absolute (1e9 = 2011-09-09) - if ( $exp > 2592000 && $exp < 1000000000 ) { - $exp = 2592000; - } - $flags = 0; if ( !is_scalar( $val ) ) { @@ -996,11 +980,11 @@ class MWMemcached { $flags |= self::COMPRESSED; } } - if ( !$this->_safe_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) { - return $this->_dead_sock( $sock ); + if ( !$this->_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) { + return false; } - $line = trim( fgets( $sock ) ); + $line = $this->_fgets( $sock ); if ( $this->_debug ) { $this->_debugprint( sprintf( "%s %s (%s)\n", $cmd, $key, $line ) ); @@ -1037,7 +1021,7 @@ class MWMemcached { } if ( !$this->_connect_sock( $sock, $host ) ) { - return $this->_dead_host( $host ); + return null; } // Do not buffer writes @@ -1048,49 +1032,136 @@ class MWMemcached { return $this->_cache_sock[$host]; } - function _debugprint( $str ) { - print( $str ); + /** + * @param $text string + */ + function _debugprint( $text ) { + global $wgDebugLogGroups; + if( !isset( $wgDebugLogGroups['memcached'] ) ) { + # Prefix message since it will end up in main debug log file + $text = "memcached: $text"; + } + wfDebugLog( 'memcached', $text ); + } + + /** + * @param $text string + */ + function _error_log( $text ) { + wfDebugLog( 'memcached-serious', "Memcached error: $text" ); } /** - * Write to a stream, timing out after the correct amount of time + * Write to a stream. If there is an error, mark the socket dead. * - * @return Boolean: false on failure, true on success + * @param $sock The socket + * @param $buf The string to write + * @return bool True on success, false on failure */ - /* - function _safe_fwrite( $f, $buf, $len = false ) { - stream_set_blocking( $f, 0 ); + function _fwrite( $sock, $buf ) { + $bytesWritten = 0; + $bufSize = strlen( $buf ); + while ( $bytesWritten < $bufSize ) { + $result = fwrite( $sock, $buf ); + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout writing to $1' ); + return false; + } + // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3. + if ( $result === false || $result === 0 ) { + $this->_handle_error( $sock, 'error writing to $1' ); + return false; + } + $bytesWritten += $result; + } - if ( $len === false ) { - wfDebug( "Writing " . strlen( $buf ) . " bytes\n" ); - $bytesWritten = fwrite( $f, $buf ); - } else { - wfDebug( "Writing $len bytes\n" ); - $bytesWritten = fwrite( $f, $buf, $len ); + return true; + } + + /** + * Handle an I/O error. Mark the socket dead and log an error. + */ + function _handle_error( $sock, $msg ) { + $peer = stream_socket_get_name( $sock, true /** remote **/ ); + if ( strval( $peer ) === '' ) { + $peer = array_search( $sock, $this->_cache_sock ); + if ( $peer === false ) { + $peer = '[unknown host]'; + } } - $n = stream_select( $r = null, $w = array( $f ), $e = null, 10, 0 ); - # $this->_timeout_seconds, $this->_timeout_microseconds ); + $msg = str_replace( '$1', $peer, $msg ); + $this->_error_log( "$msg\n" ); + $this->_dead_sock( $sock ); + } - wfDebug( "stream_select returned $n\n" ); - stream_set_blocking( $f, 1 ); - return $n == 1; - return $bytesWritten; - }*/ + /** + * Read the specified number of bytes from a stream. If there is an error, + * mark the socket dead. + * + * @param $sock The socket + * @param $len The number of bytes to read + * @return The string on success, false on failure. + */ + function _fread( $sock, $len ) { + $buf = ''; + while ( $len > 0 ) { + $result = fread( $sock, $len ); + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout reading from $1' ); + return false; + } + if ( $result === false ) { + $this->_handle_error( $sock, 'error reading buffer from $1' ); + return false; + } + if ( $result === '' ) { + // This will happen if the remote end of the socket is shut down + $this->_handle_error( $sock, 'unexpected end of file reading from $1' ); + return false; + } + $len -= strlen( $result ); + $buf .= $result; + } + return $buf; + } /** - * Original behaviour + * Read a line from a stream. If there is an error, mark the socket dead. + * The \r\n line ending is stripped from the response. + * + * @param $sock The socket + * @return The string on success, false on failure */ - function _safe_fwrite( $f, $buf, $len = false ) { - if ( $len === false ) { - $bytesWritten = fwrite( $f, $buf ); + function _fgets( $sock ) { + $result = fgets( $sock ); + // fgets() may return a partial line if there is a select timeout after + // a successful recv(), so we have to check for a timeout even if we + // got a string response. + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout reading line from $1' ); + return false; + } + if ( $result === false ) { + $this->_handle_error( $sock, 'error reading line from $1' ); + return false; + } + if ( substr( $result, -2 ) === "\r\n" ) { + $result = substr( $result, 0, -2 ); + } elseif ( substr( $result, -1 ) === "\n" ) { + $result = substr( $result, 0, -1 ); } else { - $bytesWritten = fwrite( $f, $buf, $len ); + $this->_handle_error( $sock, 'line ending missing in response from $1' ); + return false; } - return $bytesWritten; + return $result; } /** * Flush the read buffer of a stream + * @param $f Resource */ function _flush_read_buffer( $f ) { if ( !is_resource( $f ) ) { @@ -1108,12 +1179,8 @@ class MWMemcached { // }}} } -// vim: sts=3 sw=3 et // }}} class MemCachedClientforWiki extends MWMemcached { - function _debugprint( $text ) { - wfDebug( "memcached: $text" ); - } } diff --git a/includes/objectcache/MemcachedPeclBagOStuff.php b/includes/objectcache/MemcachedPeclBagOStuff.php new file mode 100644 index 00000000..76886ebb --- /dev/null +++ b/includes/objectcache/MemcachedPeclBagOStuff.php @@ -0,0 +1,237 @@ +<?php +/** + * Object caching using memcached. + * + * 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 + */ + +/** + * A wrapper class for the PECL memcached client + * + * @ingroup Cache + */ +class MemcachedPeclBagOStuff extends MemcachedBagOStuff { + + /** + * Constructor + * + * Available parameters are: + * - servers: The list of IP:port combinations holding the memcached servers. + * - persistent: Whether to use a persistent connection + * - compress_threshold: The minimum size an object must be before it is compressed + * - timeout: The read timeout in microseconds + * - connect_timeout: The connect timeout in seconds + * - serializer: May be either "php" or "igbinary". Igbinary produces more compact + * values, but serialization is much slower unless the php.ini option + * igbinary.compact_strings is off. + */ + function __construct( $params ) { + $params = $this->applyDefaultParams( $params ); + + if ( $params['persistent'] ) { + // The pool ID must be unique to the server/option combination. + // The Memcached object is essentially shared for each pool ID. + // We can only resuse a pool ID if we keep the config consistent. + $this->client = new Memcached( md5( serialize( $params ) ) ); + if ( count( $this->client->getServerList() ) ) { + wfDebug( __METHOD__ . ": persistent Memcached object already loaded.\n" ); + return; // already initialized; don't add duplicate servers + } + } else { + $this->client = new Memcached; + } + + if ( !isset( $params['serializer'] ) ) { + $params['serializer'] = 'php'; + } + + // The compression threshold is an undocumented php.ini option for some + // reason. There's probably not much harm in setting it globally, for + // compatibility with the settings for the PHP client. + ini_set( 'memcached.compression_threshold', $params['compress_threshold'] ); + + // Set timeouts + $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 ); + $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] ); + $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] ); + $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 ); + + // Set libketama mode since it's recommended by the documentation and + // is as good as any. There's no way to configure libmemcached to use + // hashes identical to the ones currently in use by the PHP client, and + // even implementing one of the libmemcached hashes in pure PHP for + // forwards compatibility would require MWMemcached::get_sock() to be + // rewritten. + $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true ); + + // Set the serializer + switch ( $params['serializer'] ) { + case 'php': + $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP ); + break; + case 'igbinary': + if ( !Memcached::HAVE_IGBINARY ) { + throw new MWException( __CLASS__.': the igbinary extension is not available ' . + 'but igbinary serialization was requested.' ); + } + $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY ); + break; + default: + throw new MWException( __CLASS__.': invalid value for serializer parameter' ); + } + $servers = array(); + foreach ( $params['servers'] as $host ) { + $servers[] = IP::splitHostAndPort( $host ); // (ip, port) + } + $this->client->addServers( $servers ); + } + + /** + * @param $key string + * @return Mixed + */ + public function get( $key ) { + $this->debugLog( "get($key)" ); + return $this->checkResult( $key, parent::get( $key ) ); + } + + /** + * @param $key string + * @param $value + * @param $exptime int + * @return bool + */ + public function set( $key, $value, $exptime = 0 ) { + $this->debugLog( "set($key)" ); + return $this->checkResult( $key, parent::set( $key, $value, $exptime ) ); + } + + /** + * @param $key string + * @param $time int + * @return bool + */ + public function delete( $key, $time = 0 ) { + $this->debugLog( "delete($key)" ); + $result = parent::delete( $key, $time ); + if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) { + // "Not found" is counted as success in our interface + return true; + } else { + return $this->checkResult( $key, $result ); + } + } + + /** + * @param $key string + * @param $value int + * @param $exptime int + * @return Mixed + */ + public function add( $key, $value, $exptime = 0 ) { + $this->debugLog( "add($key)" ); + return $this->checkResult( $key, parent::add( $key, $value, $exptime ) ); + } + + /** + * @param $key string + * @param $value int + * @param $exptime + * @return Mixed + */ + public function replace( $key, $value, $exptime = 0 ) { + $this->debugLog( "replace($key)" ); + return $this->checkResult( $key, parent::replace( $key, $value, $exptime ) ); + } + + /** + * @param $key string + * @param $value int + * @return Mixed + */ + public function incr( $key, $value = 1 ) { + $this->debugLog( "incr($key)" ); + $result = $this->client->increment( $key, $value ); + return $this->checkResult( $key, $result ); + } + + /** + * @param $key string + * @param $value int + * @return Mixed + */ + public function decr( $key, $value = 1 ) { + $this->debugLog( "decr($key)" ); + $result = $this->client->decrement( $key, $value ); + return $this->checkResult( $key, $result ); + } + + /** + * Check the return value from a client method call and take any necessary + * action. Returns the value that the wrapper function should return. At + * present, the return value is always the same as the return value from + * the client, but some day we might find a case where it should be + * different. + * + * @param $key string The key used by the caller, or false if there wasn't one. + * @param $result Mixed The return value + * @return Mixed + */ + protected function checkResult( $key, $result ) { + if ( $result !== false ) { + return $result; + } + switch ( $this->client->getResultCode() ) { + case Memcached::RES_SUCCESS: + break; + case Memcached::RES_DATA_EXISTS: + case Memcached::RES_NOTSTORED: + case Memcached::RES_NOTFOUND: + $this->debugLog( "result: " . $this->client->getResultMessage() ); + break; + default: + $msg = $this->client->getResultMessage(); + if ( $key !== false ) { + $server = $this->client->getServerByKey( $key ); + $serverName = "{$server['host']}:{$server['port']}"; + $msg = "Memcached error for key \"$key\" on server \"$serverName\": $msg"; + } else { + $msg = "Memcached error: $msg"; + } + wfDebugLog( 'memcached-serious', $msg ); + } + return $result; + } + + /** + * @param $keys Array + * @return Array + */ + public function getMulti( array $keys ) { + $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' ); + $callback = array( $this, 'encodeKey' ); + $result = $this->client->getMulti( array_map( $callback, $keys ) ); + return $this->checkResult( false, $result ); + } + + /* NOTE: there is no cas() method here because it is currently not supported + * by the BagOStuff interface and other BagOStuff subclasses, such as + * SqlBagOStuff. + */ +} diff --git a/includes/objectcache/MemcachedPhpBagOStuff.php b/includes/objectcache/MemcachedPhpBagOStuff.php index 14016683..a46dc716 100644 --- a/includes/objectcache/MemcachedPhpBagOStuff.php +++ b/includes/objectcache/MemcachedPhpBagOStuff.php @@ -1,14 +1,32 @@ <?php +/** + * Object caching using memcached. + * + * 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 + */ /** * A wrapper class for the pure-PHP memcached client, exposing a BagOStuff interface. + * + * @ingroup Cache */ -class MemcachedPhpBagOStuff extends BagOStuff { - - /** - * @var MemCachedClientforWiki - */ - protected $client; +class MemcachedPhpBagOStuff extends MemcachedBagOStuff { /** * Constructor. @@ -24,24 +42,7 @@ class MemcachedPhpBagOStuff extends BagOStuff { * @param $params array */ function __construct( $params ) { - if ( !isset( $params['servers'] ) ) { - $params['servers'] = $GLOBALS['wgMemCachedServers']; - } - if ( !isset( $params['debug'] ) ) { - $params['debug'] = $GLOBALS['wgMemCachedDebug']; - } - if ( !isset( $params['persistent'] ) ) { - $params['persistent'] = $GLOBALS['wgMemCachedPersistent']; - } - if ( !isset( $params['compress_threshold'] ) ) { - $params['compress_threshold'] = 1500; - } - if ( !isset( $params['timeout'] ) ) { - $params['timeout'] = $GLOBALS['wgMemCachedTimeout']; - } - if ( !isset( $params['connect_timeout'] ) ) { - $params['connect_timeout'] = 0.1; - } + $params = $this->applyDefaultParams( $params ); $this->client = new MemCachedClientforWiki( $params ); $this->client->set_servers( $params['servers'] ); @@ -56,36 +57,18 @@ class MemcachedPhpBagOStuff extends BagOStuff { } /** - * @param $key string - * @return Mixed - */ - public function get( $key ) { - return $this->client->get( $this->encodeKey( $key ) ); - } - - /** - * @param $key string - * @param $value - * @param $exptime int - * @return bool + * @param $keys Array + * @return Array */ - public function set( $key, $value, $exptime = 0 ) { - return $this->client->set( $this->encodeKey( $key ), $value, $exptime ); - } - - /** - * @param $key string - * @param $time int - * @return bool - */ - public function delete( $key, $time = 0 ) { - return $this->client->delete( $this->encodeKey( $key ), $time ); + public function getMulti( array $keys ) { + $callback = array( $this, 'encodeKey' ); + return $this->client->get_multi( array_map( $callback, $keys ) ); } /** * @param $key * @param $timeout int - * @return + * @return bool */ public function lock( $key, $timeout = 0 ) { return $this->client->lock( $this->encodeKey( $key ), $timeout ); @@ -98,26 +81,7 @@ class MemcachedPhpBagOStuff extends BagOStuff { public function unlock( $key ) { return $this->client->unlock( $this->encodeKey( $key ) ); } - - /** - * @param $key string - * @param $value int - * @return Mixed - */ - public function add( $key, $value, $exptime = 0 ) { - return $this->client->add( $this->encodeKey( $key ), $value, $exptime ); - } - - /** - * @param $key string - * @param $value int - * @param $exptime - * @return Mixed - */ - public function replace( $key, $value, $exptime = 0 ) { - return $this->client->replace( $this->encodeKey( $key ), $value, $exptime ); - } - + /** * @param $key string * @param $value int @@ -135,44 +99,5 @@ class MemcachedPhpBagOStuff extends BagOStuff { public function decr( $key, $value = 1 ) { return $this->client->decr( $this->encodeKey( $key ), $value ); } - - /** - * Get the underlying client object. This is provided for debugging - * purposes. - * - * @return MemCachedClientforWiki - */ - public function getClient() { - return $this->client; - } - - /** - * Encode a key for use on the wire inside the memcached protocol. - * - * We encode spaces and line breaks to avoid protocol errors. We encode - * the other control characters for compatibility with libmemcached - * verify_key. We leave other punctuation alone, to maximise backwards - * compatibility. - */ - public function encodeKey( $key ) { - return preg_replace_callback( '/[\x00-\x20\x25\x7f]+/', - array( $this, 'encodeKeyCallback' ), $key ); - } - - protected function encodeKeyCallback( $m ) { - return rawurlencode( $m[0] ); - } - - /** - * Decode a key encoded with encodeKey(). This is provided as a convenience - * function for debugging. - * - * @param $key string - * - * @return string - */ - public function decodeKey( $key ) { - return urldecode( $key ); - } } diff --git a/includes/objectcache/MultiWriteBagOStuff.php b/includes/objectcache/MultiWriteBagOStuff.php index 0d95a846..e496ddd8 100644 --- a/includes/objectcache/MultiWriteBagOStuff.php +++ b/includes/objectcache/MultiWriteBagOStuff.php @@ -1,9 +1,32 @@ <?php +/** + * Wrapper for object caching in different caches. + * + * 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 + */ /** * A cache class that replicates all writes to multiple child caches. Reads * are implemented by reading from the caches in the order they are given in * the configuration until a cache gives a positive result. + * + * @ingroup Cache */ class MultiWriteBagOStuff extends BagOStuff { var $caches; @@ -11,11 +34,12 @@ class MultiWriteBagOStuff extends BagOStuff { /** * Constructor. Parameters are: * - * - caches: This should have a numbered array of cache parameter + * - caches: This should have a numbered array of cache parameter * structures, in the style required by $wgObjectCaches. See * the documentation of $wgObjectCaches for more detail. * * @param $params array + * @throws MWException */ public function __construct( $params ) { if ( !isset( $params['caches'] ) ) { @@ -28,10 +52,17 @@ class MultiWriteBagOStuff extends BagOStuff { } } + /** + * @param $debug bool + */ public function setDebug( $debug ) { $this->doWrite( 'setDebug', $debug ); } + /** + * @param $key string + * @return bool|mixed + */ public function get( $key ) { foreach ( $this->caches as $cache ) { $value = $cache->get( $key ); @@ -42,30 +73,68 @@ class MultiWriteBagOStuff extends BagOStuff { return false; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function set( $key, $value, $exptime = 0 ) { return $this->doWrite( 'set', $key, $value, $exptime ); } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { return $this->doWrite( 'delete', $key, $time ); } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function add( $key, $value, $exptime = 0 ) { return $this->doWrite( 'add', $key, $value, $exptime ); } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function replace( $key, $value, $exptime = 0 ) { return $this->doWrite( 'replace', $key, $value, $exptime ); } + /** + * @param $key string + * @param $value int + * @return bool|null + */ public function incr( $key, $value = 1 ) { return $this->doWrite( 'incr', $key, $value ); } + /** + * @param $key string + * @param $value int + * @return bool + */ public function decr( $key, $value = 1 ) { return $this->doWrite( 'decr', $key, $value ); - } + } + /** + * @param $key string + * @param $timeout int + * @return bool + */ public function lock( $key, $timeout = 0 ) { // Lock only the first cache, to avoid deadlocks if ( isset( $this->caches[0] ) ) { @@ -75,6 +144,10 @@ class MultiWriteBagOStuff extends BagOStuff { } } + /** + * @param $key string + * @return bool + */ public function unlock( $key ) { if ( isset( $this->caches[0] ) ) { return $this->caches[0]->unlock( $key ); @@ -83,6 +156,10 @@ class MultiWriteBagOStuff extends BagOStuff { } } + /** + * @param $method string + * @return bool + */ protected function doWrite( $method /*, ... */ ) { $ret = true; $args = func_get_args(); @@ -97,9 +174,12 @@ class MultiWriteBagOStuff extends BagOStuff { } /** - * Delete objects expiring before a certain date. + * Delete objects expiring before a certain date. * * Succeed if any of the child caches succeed. + * @param $date string + * @param $progressCallback bool|callback + * @return bool */ public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { $ret = false; diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 77ca8371..9b360f32 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -1,19 +1,40 @@ <?php /** - * Functions to get cache objects + * Functions to get cache objects. + * + * 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 */ + +/** + * Functions to get cache objects + * + * @ingroup Cache + */ class ObjectCache { static $instances = array(); /** * Get a cached instance of the specified type of cache object. * - * @param $id + * @param $id string * - * @return object + * @return ObjectCache */ static function getInstance( $id ) { if ( isset( self::$instances[$id] ) ) { @@ -35,8 +56,9 @@ class ObjectCache { /** * Create a new cache object of the specified type. * - * @param $id + * @param $id string * + * @throws MWException * @return ObjectCache */ static function newFromId( $id ) { @@ -55,6 +77,7 @@ class ObjectCache { * * @param $params array * + * @throws MWException * @return ObjectCache */ static function newFromParams( $params ) { @@ -71,6 +94,15 @@ class ObjectCache { /** * Factory function referenced from DefaultSettings.php for CACHE_ANYTHING + * + * CACHE_ANYTHING means that stuff has to be cached, not caching is not an option. + * If a caching method is configured for any of the main caches ($wgMainCacheType, + * $wgMessageCacheType, $wgParserCacheType), then CACHE_ANYTHING will effectively + * be an alias to the configured cache choice for that. + * If no cache choice is configured (by default $wgMainCacheType is CACHE_NONE), + * then CACHE_ANYTHING will forward to CACHE_DB. + * @param $params array + * @return ObjectCache */ static function newAnything( $params ) { global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; @@ -86,6 +118,8 @@ class ObjectCache { /** * Factory function referenced from DefaultSettings.php for CACHE_ACCEL. * + * @param $params array + * @throws MWException * @return ObjectCache */ static function newAccelerator( $params ) { @@ -104,8 +138,10 @@ class ObjectCache { /** * Factory function that creates a memcached client object. - * The idea of this is that it might eventually detect and automatically - * support the PECL extension, assuming someone can get it to compile. + * + * This always uses the PHP client, since the PECL client has a different + * hashing scheme and a different interpretation of the flags bitfield, so + * switching between the two clients randomly would be disasterous. * * @param $params array * diff --git a/includes/objectcache/ObjectCacheSessionHandler.php b/includes/objectcache/ObjectCacheSessionHandler.php new file mode 100644 index 00000000..f55da94d --- /dev/null +++ b/includes/objectcache/ObjectCacheSessionHandler.php @@ -0,0 +1,145 @@ +<?php +/** + * Session storage in object cache. + * + * 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 + */ + +/** + * Session storage in object cache. + * Used if $wgSessionsInObjectCache is true. + * + * @ingroup Cache + */ +class ObjectCacheSessionHandler { + /** + * Install a session handler for the current web request + */ + static function install() { + session_set_save_handler( + array( __CLASS__, 'open' ), + array( __CLASS__, 'close' ), + array( __CLASS__, 'read' ), + array( __CLASS__, 'write' ), + array( __CLASS__, 'destroy' ), + array( __CLASS__, 'gc' ) ); + + // It's necessary to register a shutdown function to call session_write_close(), + // because by the time the request shutdown function for the session module is + // called, $wgMemc has already been destroyed. Shutdown functions registered + // this way are called before object destruction. + register_shutdown_function( array( __CLASS__, 'handleShutdown' ) ); + } + + /** + * Get the cache storage object to use for session storage + */ + static function getCache() { + global $wgSessionCacheType; + return ObjectCache::getInstance( $wgSessionCacheType ); + } + + /** + * Get a cache key for the given session id. + * + * @param $id String: session id + * @return String: cache key + */ + static function getKey( $id ) { + return wfMemcKey( 'session', $id ); + } + + /** + * Callback when opening a session. + * + * @param $save_path String: path used to store session files, unused + * @param $session_name String: session name + * @return Boolean: success + */ + static function open( $save_path, $session_name ) { + return true; + } + + /** + * Callback when closing a session. + * NOP. + * + * @return Boolean: success + */ + static function close() { + return true; + } + + /** + * Callback when reading session data. + * + * @param $id String: session id + * @return Mixed: session data + */ + static function read( $id ) { + $data = self::getCache()->get( self::getKey( $id ) ); + if( $data === false ) { + return ''; + } + return $data; + } + + /** + * Callback when writing session data. + * + * @param $id String: session id + * @param $data Mixed: session data + * @return Boolean: success + */ + static function write( $id, $data ) { + global $wgObjectCacheSessionExpiry; + self::getCache()->set( self::getKey( $id ), $data, $wgObjectCacheSessionExpiry ); + return true; + } + + /** + * Callback to destroy a session when calling session_destroy(). + * + * @param $id String: session id + * @return Boolean: success + */ + static function destroy( $id ) { + self::getCache()->delete( self::getKey( $id ) ); + return true; + } + + /** + * Callback to execute garbage collection. + * NOP: Object caches perform garbage collection implicitly + * + * @param $maxlifetime Integer: maximum session life time + * @return Boolean: success + */ + static function gc( $maxlifetime ) { + return true; + } + + /** + * Shutdown function. See the comment inside ObjectCacheSessionHandler::install + * for rationale. + */ + static function handleShutdown() { + session_write_close(); + } +} diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php new file mode 100644 index 00000000..c5966cdb --- /dev/null +++ b/includes/objectcache/RedisBagOStuff.php @@ -0,0 +1,413 @@ +<?php +/** + * Object caching using Redis (http://redis.io/). + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + + +class RedisBagOStuff extends BagOStuff { + protected $connectTimeout, $persistent, $password, $automaticFailover; + + /** + * A list of server names, from $params['servers'] + */ + protected $servers; + + /** + * A cache of Redis objects, representing connections to Redis servers. + * The key is the server name. + */ + protected $conns = array(); + + /** + * An array listing "dead" servers which have had a connection error in + * the past. Servers are marked dead for a limited period of time, to + * avoid excessive overhead from repeated connection timeouts. The key in + * the array is the server name, the value is the UNIX timestamp at which + * the server is resurrected. + */ + protected $deadServers = array(); + + /** + * Construct a RedisBagOStuff object. Parameters are: + * + * - servers: An array of server names. A server name may be a hostname, + * a hostname/port combination or the absolute path of a UNIX socket. + * If a hostname is specified but no port, the standard port number + * 6379 will be used. Required. + * + * - connectTimeout: The timeout for new connections, in seconds. Optional, + * default is 1 second. + * + * - persistent: Set this to true to allow connections to persist across + * multiple web requests. False by default. + * + * - password: The authentication password, will be sent to Redis in + * clear text. Optional, if it is unspecified, no AUTH command will be + * sent. + * + * - automaticFailover: If this is false, then each key will be mapped to + * a single server, and if that server is down, any requests for that key + * will fail. If this is true, a connection failure will cause the client + * to immediately try the next server in the list (as determined by a + * consistent hashing algorithm). True by default. This has the + * potential to create consistency issues if a server is slow enough to + * flap, for example if it is in swap death. + */ + function __construct( $params ) { + if ( !extension_loaded( 'redis' ) ) { + throw new MWException( __CLASS__. ' requires the phpredis extension: ' . + 'https://github.com/nicolasff/phpredis' ); + } + + $this->servers = $params['servers']; + $this->connectTimeout = isset( $params['connectTimeout'] ) + ? $params['connectTimeout'] : 1; + $this->persistent = !empty( $params['persistent'] ); + if ( isset( $params['password'] ) ) { + $this->password = $params['password']; + } + if ( isset( $params['automaticFailover'] ) ) { + $this->automaticFailover = $params['automaticFailover']; + } else { + $this->automaticFailover = true; + } + } + + public function get( $key ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + try { + $result = $conn->get( $key ); + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + $this->logRequest( 'get', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function set( $key, $value, $expiry = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + $expiry = $this->convertToRelative( $expiry ); + try { + if ( !$expiry ) { + // No expiry, that is very different from zero expiry in Redis + $result = $conn->set( $key, $value ); + } else { + $result = $conn->setex( $key, $expiry, $value ); + } + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + + $this->logRequest( 'set', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function delete( $key, $time = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + try { + $conn->delete( $key ); + // Return true even if the key didn't exist + $result = true; + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + $this->logRequest( 'delete', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function getMulti( array $keys ) { + wfProfileIn( __METHOD__ ); + $batches = array(); + $conns = array(); + foreach ( $keys as $key ) { + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + continue; + } + $conns[$server] = $conn; + $batches[$server][] = $key; + } + $result = array(); + foreach ( $batches as $server => $batchKeys ) { + $conn = $conns[$server]; + try { + $conn->multi( Redis::PIPELINE ); + foreach ( $batchKeys as $key ) { + $conn->get( $key ); + } + $batchResult = $conn->exec(); + if ( $batchResult === false ) { + $this->debug( "multi request to $server failed" ); + continue; + } + foreach ( $batchResult as $i => $value ) { + if ( $value !== false ) { + $result[$batchKeys[$i]] = $value; + } + } + } catch ( RedisException $e ) { + $this->handleException( $server, $e ); + } + } + + $this->debug( "getMulti for " . count( $keys ) . " keys " . + "returned " . count( $result ) . " results" ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function add( $key, $value, $expiry = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + $expiry = $this->convertToRelative( $expiry ); + try { + $result = $conn->setnx( $key, $value ); + if ( $result && $expiry ) { + $conn->expire( $key, $expiry ); + } + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + $this->logRequest( 'add', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Non-atomic implementation of replace(). Could perhaps be done atomically + * with WATCH or scripting, but this function is rarely used. + */ + public function replace( $key, $value, $expiry = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + if ( !$conn->exists( $key ) ) { + wfProfileOut( __METHOD__ ); + return false; + } + + $expiry = $this->convertToRelative( $expiry ); + try { + if ( !$expiry ) { + $result = $conn->set( $key, $value ); + } else { + $result = $conn->setex( $key, $expiry, $value ); + } + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + + $this->logRequest( 'replace', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Non-atomic implementation of incr(). + * + * Probably all callers actually want incr() to atomically initialise + * values to zero if they don't exist, as provided by the Redis INCR + * command. But we are constrained by the memcached-like interface to + * return null in that case. Once the key exists, further increments are + * atomic. + */ + public function incr( $key, $value = 1 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + if ( !$conn->exists( $key ) ) { + wfProfileOut( __METHOD__ ); + return null; + } + try { + $result = $conn->incrBy( $key, $value ); + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + + $this->logRequest( 'incr', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Get a Redis object with a connection suitable for fetching the specified key + */ + protected function getConnection( $key ) { + if ( count( $this->servers ) === 1 ) { + $candidates = $this->servers; + } else { + // Use consistent hashing + $hashes = array(); + foreach ( $this->servers as $server ) { + $hashes[$server] = md5( $server . '/' . $key ); + } + asort( $hashes ); + if ( !$this->automaticFailover ) { + reset( $hashes ); + $candidates = array( key( $hashes ) ); + } else { + $candidates = array_keys( $hashes ); + } + } + + foreach ( $candidates as $server ) { + $conn = $this->getConnectionToServer( $server ); + if ( $conn ) { + return array( $server, $conn ); + } + } + return array( false, false ); + } + + /** + * Get a connection to the server with the specified name. Connections + * are cached, and failures are persistent to avoid multiple timeouts. + * + * @return Redis object, or false on failure + */ + protected function getConnectionToServer( $server ) { + if ( isset( $this->deadServers[$server] ) ) { + $now = time(); + if ( $now > $this->deadServers[$server] ) { + // Dead time expired + unset( $this->deadServers[$server] ); + } else { + // Server is dead + $this->debug( "server $server is marked down for another " . + ($this->deadServers[$server] - $now ) . + " seconds, can't get connection" ); + return false; + } + } + + if ( isset( $this->conns[$server] ) ) { + return $this->conns[$server]; + } + + if ( substr( $server, 0, 1 ) === '/' ) { + // UNIX domain socket + // These are required by the redis extension to start with a slash, but + // we still need to set the port to a special value to make it work. + $host = $server; + $port = 0; + } else { + // TCP connection + $hostPort = IP::splitHostAndPort( $server ); + if ( !$hostPort ) { + throw new MWException( __CLASS__.": invalid configured server \"$server\"" ); + } + list( $host, $port ) = $hostPort; + if ( $port === false ) { + $port = 6379; + } + } + $conn = new Redis; + try { + if ( $this->persistent ) { + $this->debug( "opening persistent connection to $host:$port" ); + $result = $conn->pconnect( $host, $port, $this->connectTimeout ); + } else { + $this->debug( "opening non-persistent connection to $host:$port" ); + $result = $conn->connect( $host, $port, $this->connectTimeout ); + } + if ( !$result ) { + $this->logError( "could not connect to server $server" ); + // Mark server down for 30s to avoid further timeouts + $this->deadServers[$server] = time() + 30; + return false; + } + if ( $this->password !== null ) { + if ( !$conn->auth( $this->password ) ) { + $this->logError( "authentication error connecting to $server" ); + } + } + } catch ( RedisException $e ) { + $this->deadServers[$server] = time() + 30; + wfDebugLog( 'redis', "Redis exception: " . $e->getMessage() . "\n" ); + return false; + } + + $conn->setOption( Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP ); + $this->conns[$server] = $conn; + return $conn; + } + + /** + * Log a fatal error + */ + protected function logError( $msg ) { + wfDebugLog( 'redis', "Redis error: $msg\n" ); + } + + /** + * The redis extension throws an exception in response to various read, write + * and protocol errors. Sometimes it also closes the connection, sometimes + * not. The safest response for us is to explicitly destroy the connection + * object and let it be reopened during the next request. + */ + protected function handleException( $server, $e ) { + wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" ); + unset( $this->conns[$server] ); + } + + /** + * Send information about a single request to the debug log + */ + public function logRequest( $method, $key, $server, $result ) { + $this->debug( "$method $key on $server: " . + ( $result === false ? "failure" : "success" ) ); + } +} + diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index 93d22f11..54051dc1 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using a SQL database. + * + * 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 + */ /** * Class to store objects in the database @@ -6,7 +27,6 @@ * @ingroup Cache */ class SqlBagOStuff extends BagOStuff { - /** * @var LoadBalancer */ @@ -22,6 +42,9 @@ class SqlBagOStuff extends BagOStuff { var $shards = 1; var $tableName = 'objectcache'; + protected $connFailureTime = 0; // UNIX timestamp + protected $connFailureError; // exception + /** * Constructor. Parameters are: * - server: A server info structure in the format required by each @@ -66,25 +89,40 @@ class SqlBagOStuff extends BagOStuff { * @return DatabaseBase */ protected function getDB() { + global $wgDebugDBTransactions; + + # Don't keep timing out trying to connect for each call if the DB is down + if ( $this->connFailureError && ( time() - $this->connFailureTime ) < 60 ) { + throw $this->connFailureError; + } + if ( !isset( $this->db ) ) { # If server connection info was given, use that if ( $this->serverInfo ) { + if ( $wgDebugDBTransactions ) { + wfDebug( sprintf( "Using provided serverInfo for SqlBagOStuff\n" ) ); + } $this->lb = new LoadBalancer( array( 'servers' => array( $this->serverInfo ) ) ); $this->db = $this->lb->getConnection( DB_MASTER ); $this->db->clearFlag( DBO_TRX ); } else { - # 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 ( wfGetDB( DB_MASTER )->getType() == 'sqlite' ) { - $this->db = wfGetDB( DB_MASTER ); - } else { + /* + * We must keep a separate connection to MySQL in order to avoid deadlocks + * However, SQLite has an opposite behaviour. And PostgreSQL needs to know + * if we are in transaction or no + */ + if ( wfGetDB( DB_MASTER )->getType() == 'mysql' ) { $this->lb = wfGetLBFactory()->newMainLB(); $this->db = $this->lb->getConnection( DB_MASTER ); - $this->db->clearFlag( DBO_TRX ); + $this->db->clearFlag( DBO_TRX ); // auto-commit mode + } else { + $this->db = wfGetDB( DB_MASTER ); } } + if ( $wgDebugDBTransactions ) { + wfDebug( sprintf( "Connection %s will be used for SqlBagOStuff\n", $this->db ) ); + } } return $this->db; @@ -92,6 +130,8 @@ class SqlBagOStuff extends BagOStuff { /** * Get the table name for a given key + * @param $key string + * @return string */ protected function getTableByKey( $key ) { if ( $this->shards > 1 ) { @@ -104,6 +144,8 @@ class SqlBagOStuff extends BagOStuff { /** * Get the table name for a given shard index + * @param $index int + * @return string */ protected function getTableByShard( $index ) { if ( $this->shards > 1 ) { @@ -115,61 +157,103 @@ class SqlBagOStuff extends BagOStuff { } } + /** + * @param $key string + * @return mixed + */ public function get( $key ) { - # expire old entries if any - $this->garbageCollect(); - $db = $this->getDB(); - $tableName = $this->getTableByKey( $key ); - $row = $db->selectRow( $tableName, array( 'value', 'exptime' ), - array( 'keyname' => $key ), __METHOD__ ); + $values = $this->getMulti( array( $key ) ); + return array_key_exists( $key, $values ) ? $values[$key] : false; + } - if ( !$row ) { - $this->debug( 'get: no matching rows' ); - return false; - } + /** + * @param $keys array + * @return Array + */ + public function getMulti( array $keys ) { + $values = array(); // array of (key => value) - $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); + try { + $db = $this->getDB(); + $keysByTableName = array(); + foreach ( $keys as $key ) { + $tableName = $this->getTableByKey( $key ); + if ( !isset( $keysByTableName[$tableName] ) ) { + $keysByTableName[$tableName] = array(); + } + $keysByTableName[$tableName][] = $key; + } - if ( $this->isExpired( $row->exptime ) ) { - $this->debug( "get: key has expired, deleting" ); - try { - $db->begin( __METHOD__ ); - # Put the expiry time in the WHERE condition to avoid deleting a - # newly-inserted value - $db->delete( $tableName, - array( - 'keyname' => $key, - 'exptime' => $row->exptime - ), __METHOD__ ); - $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); + $this->garbageCollect(); // expire old entries if any + + $dataRows = array(); + foreach ( $keysByTableName as $tableName => $tableKeys ) { + $res = $db->select( $tableName, + array( 'keyname', 'value', 'exptime' ), + array( 'keyname' => $tableKeys ), + __METHOD__ ); + foreach ( $res as $row ) { + $dataRows[$row->keyname] = $row; + } } - return false; - } + foreach ( $keys as $key ) { + if ( isset( $dataRows[$key] ) ) { // HIT? + $row = $dataRows[$key]; + $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); + if ( $this->isExpired( $row->exptime ) ) { // MISS + $this->debug( "get: key has expired, deleting" ); + try { + $db->begin( __METHOD__ ); + # Put the expiry time in the WHERE condition to avoid deleting a + # newly-inserted value + $db->delete( $this->getTableByKey( $key ), + array( 'keyname' => $key, 'exptime' => $row->exptime ), + __METHOD__ ); + $db->commit( __METHOD__ ); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); + } + $values[$key] = false; + } else { // HIT + $values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) ); + } + } else { // MISS + $values[$key] = false; + $this->debug( 'get: no matching rows' ); + } + } + } catch ( DBError $e ) { + $this->handleReadError( $e ); + }; - return $this->unserialize( $db->decodeBlob( $row->value ) ); + return $values; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function set( $key, $value, $exptime = 0 ) { - $db = $this->getDB(); - $exptime = intval( $exptime ); - - if ( $exptime < 0 ) { - $exptime = 0; - } + try { + $db = $this->getDB(); + $exptime = intval( $exptime ); - if ( $exptime == 0 ) { - $encExpiry = $this->getMaxDateTime(); - } else { - if ( $exptime < 3.16e8 ) { # ~10 years - $exptime += time(); + if ( $exptime < 0 ) { + $exptime = 0; } - $encExpiry = $db->timestamp( $exptime ); - } - try { + if ( $exptime == 0 ) { + $encExpiry = $this->getMaxDateTime(); + } else { + if ( $exptime < 3.16e8 ) { # ~10 years + $exptime += time(); + } + + $encExpiry = $db->timestamp( $exptime ); + } $db->begin( __METHOD__ ); // (bug 24425) use a replace if the db supports it instead of // delete/insert to avoid clashes with conflicting keynames @@ -182,40 +266,46 @@ class SqlBagOStuff extends BagOStuff { 'exptime' => $encExpiry ), __METHOD__ ); $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); - return false; } return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { - $db = $this->getDB(); - try { + $db = $this->getDB(); $db->begin( __METHOD__ ); $db->delete( $this->getTableByKey( $key ), array( 'keyname' => $key ), __METHOD__ ); $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); - return false; } return true; } + /** + * @param $key string + * @param $step int + * @return int|null + */ public function incr( $key, $step = 1 ) { - $db = $this->getDB(); - $tableName = $this->getTableByKey( $key ); - $step = intval( $step ); - try { + $db = $this->getDB(); + $tableName = $this->getTableByKey( $key ); + $step = intval( $step ); $db->begin( __METHOD__ ); $row = $db->selectRow( $tableName, @@ -251,34 +341,47 @@ class SqlBagOStuff extends BagOStuff { $newValue = null; } $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); - return null; } return $newValue; } + /** + * @return Array + */ public function keys() { - $db = $this->getDB(); $result = array(); - for ( $i = 0; $i < $this->shards; $i++ ) { - $res = $db->select( $this->getTableByShard( $i ), - array( 'keyname' ), false, __METHOD__ ); - foreach ( $res as $row ) { - $result[] = $row->keyname; + try { + $db = $this->getDB(); + for ( $i = 0; $i < $this->shards; $i++ ) { + $res = $db->select( $this->getTableByShard( $i ), + array( 'keyname' ), false, __METHOD__ ); + foreach ( $res as $row ) { + $result[] = $row->keyname; + } } + } catch ( DBError $e ) { + $this->handleReadError( $e ); } return $result; } + /** + * @param $exptime string + * @return bool + */ protected function isExpired( $exptime ) { return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time(); } + /** + * @return string + */ protected function getMaxDateTime() { if ( time() > 0x7fffffff ) { return $this->getDB()->timestamp( 1 << 62 ); @@ -310,14 +413,16 @@ class SqlBagOStuff extends BagOStuff { /** * Delete objects from the database which expire before a certain date. + * @param $timestamp string + * @param $progressCallback bool|callback + * @return bool */ public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) { - $db = $this->getDB(); - $dbTimestamp = $db->timestamp( $timestamp ); - $totalSeconds = false; - $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) ); - try { + $db = $this->getDB(); + $dbTimestamp = $db->timestamp( $timestamp ); + $totalSeconds = false; + $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) ); for ( $i = 0; $i < $this->shards; $i++ ) { $maxExpTime = false; while ( true ) { @@ -325,7 +430,7 @@ class SqlBagOStuff extends BagOStuff { if ( $maxExpTime !== false ) { $conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime ); } - $rows = $db->select( + $rows = $db->select( $this->getTableByShard( $i ), array( 'keyname', 'exptime' ), $conds, @@ -349,7 +454,7 @@ class SqlBagOStuff extends BagOStuff { $db->begin( __METHOD__ ); $db->delete( $this->getTableByShard( $i ), - array( + array( 'exptime >= ' . $db->addQuotes( $minExpTime ), 'exptime < ' . $db->addQuotes( $dbTimestamp ), 'keyname' => $keys @@ -361,36 +466,40 @@ class SqlBagOStuff extends BagOStuff { if ( intval( $totalSeconds ) === 0 ) { $percent = 0; } else { - $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp ) + $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp ) - wfTimestamp( TS_UNIX, $maxExpTime ); if ( $remainingSeconds > $totalSeconds ) { $totalSeconds = $remainingSeconds; } - $percent = ( $i + $remainingSeconds / $totalSeconds ) + $percent = ( $i + $remainingSeconds / $totalSeconds ) / $this->shards * 100; } call_user_func( $progressCallback, $percent ); } } } - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); + return false; } + return true; } public function deleteAll() { - $db = $this->getDB(); - try { + $db = $this->getDB(); for ( $i = 0; $i < $this->shards; $i++ ) { $db->begin( __METHOD__ ); $db->delete( $this->getTableByShard( $i ), '*', __METHOD__ ); $db->commit( __METHOD__ ); } - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); + return false; } + + return true; } /** @@ -433,23 +542,40 @@ class SqlBagOStuff extends BagOStuff { } /** - * Handle a DBQueryError which occurred during a write operation. - * Ignore errors which are due to a read-only database, rethrow others. + * Handle a DBError which occurred during a read operation. */ - protected function handleWriteError( $exception ) { - $db = $this->getDB(); - - if ( !$db->wasReadOnlyError() ) { - throw $exception; + protected function handleReadError( DBError $exception ) { + if ( $exception instanceof DBConnectionError ) { + $this->connFailureTime = time(); + $this->connFailureError = $exception; } - - try { - $db->rollback(); - } catch ( DBQueryError $e ) { + wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" ); + if ( $this->db ) { + wfDebug( __METHOD__ . ": ignoring query error\n" ); + } else { + wfDebug( __METHOD__ . ": ignoring connection error\n" ); } + } - wfDebug( __METHOD__ . ": ignoring query error\n" ); - $db->ignoreErrors( false ); + /** + * Handle a DBQueryError which occurred during a write operation. + */ + protected function handleWriteError( DBError $exception ) { + if ( $exception instanceof DBConnectionError ) { + $this->connFailureTime = time(); + $this->connFailureError = $exception; + } + if ( $this->db && $this->db->wasReadOnlyError() ) { + try { + $this->db->rollback( __METHOD__ ); + } catch ( DBError $e ) {} + } + wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" ); + if ( $this->db ) { + wfDebug( __METHOD__ . ": ignoring query error\n" ); + } else { + wfDebug( __METHOD__ . ": ignoring connection error\n" ); + } } /** diff --git a/includes/objectcache/WinCacheBagOStuff.php b/includes/objectcache/WinCacheBagOStuff.php index 7f464946..21aa39e7 100644 --- a/includes/objectcache/WinCacheBagOStuff.php +++ b/includes/objectcache/WinCacheBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using WinCache. + * + * 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 + */ /** * Wrapper for WinCache object caching functions; identical interface @@ -53,6 +74,9 @@ class WinCacheBagOStuff extends BagOStuff { return true; } + /** + * @return Array + */ public function keys() { $info = wincache_ucache_info(); $list = $info['ucache_entries']; diff --git a/includes/objectcache/XCacheBagOStuff.php b/includes/objectcache/XCacheBagOStuff.php index 0ddf1245..bc68b596 100644 --- a/includes/objectcache/XCacheBagOStuff.php +++ b/includes/objectcache/XCacheBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using XCache. + * + * 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 + */ /** * Wrapper for XCache object caching functions; identical interface @@ -17,7 +38,13 @@ class XCacheBagOStuff extends BagOStuff { $val = xcache_get( $key ); if ( is_string( $val ) ) { - $val = unserialize( $val ); + if ( $this->isInteger( $val ) ) { + $val = intval( $val ); + } else { + $val = unserialize( $val ); + } + } elseif ( is_null( $val ) ) { + return false; } return $val; @@ -32,7 +59,11 @@ class XCacheBagOStuff extends BagOStuff { * @return bool */ public function set( $key, $value, $expire = 0 ) { - xcache_set( $key, serialize( $value ), $expire ); + if ( !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + + xcache_set( $key, $value, $expire ); return true; } @@ -47,5 +78,12 @@ class XCacheBagOStuff extends BagOStuff { xcache_unset( $key ); return true; } -} + public function incr( $key, $value = 1 ) { + return xcache_inc( $key, $value ); + } + + public function decr( $key, $value = 1 ) { + return xcache_dec( $key, $value ); + } +} diff --git a/includes/parser/CacheTime.php b/includes/parser/CacheTime.php new file mode 100644 index 00000000..881dded7 --- /dev/null +++ b/includes/parser/CacheTime.php @@ -0,0 +1,132 @@ +<?php +/** + * Parser cache specific expiry check. + * + * 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 Parser + */ + +/** + * Parser cache specific expiry check. + * + * @ingroup Parser + */ +class CacheTime { + + var $mVersion = Parser::VERSION, # Compatibility check + $mCacheTime = '', # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. + $mCacheExpiry = null, # Seconds after which the object should expire, use 0 for uncachable. Used in ParserCache. + $mContainsOldMagic; # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} + + function getCacheTime() { return $this->mCacheTime; } + + function containsOldMagic() { return $this->mContainsOldMagic; } + function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); } + + /** + * setCacheTime() sets the timestamp expressing when the page has been rendered. + * This doesn not control expiry, see updateCacheExpiry() for that! + * @param $t string + * @return string + */ + function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } + + /** + * Sets the number of seconds after which this object should expire. + * This value is used with the ParserCache. + * If called with a value greater than the value provided at any previous call, + * the new call has no effect. The value returned by getCacheExpiry is smaller + * or equal to the smallest number that was provided as an argument to + * updateCacheExpiry(). + * + * @param $seconds number + */ + function updateCacheExpiry( $seconds ) { + $seconds = (int)$seconds; + + if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) { + $this->mCacheExpiry = $seconds; + } + + // hack: set old-style marker for uncacheable entries. + if ( $this->mCacheExpiry !== null && $this->mCacheExpiry <= 0 ) { + $this->mCacheTime = -1; + } + } + + /** + * Returns the number of seconds after which this object should expire. + * This method is used by ParserCache to determine how long the ParserOutput can be cached. + * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime(). + * The value returned by getCacheExpiry is smaller or equal to the smallest number + * that was provided to a call of updateCacheExpiry(), and smaller or equal to the + * value of $wgParserCacheExpireTime. + * @return int|mixed|null + */ + function getCacheExpiry() { + global $wgParserCacheExpireTime; + + if ( $this->mCacheTime < 0 ) { + return 0; + } // old-style marker for "not cachable" + + $expire = $this->mCacheExpiry; + + if ( $expire === null ) { + $expire = $wgParserCacheExpireTime; + } else { + $expire = min( $expire, $wgParserCacheExpireTime ); + } + + if( $this->containsOldMagic() ) { //compatibility hack + $expire = min( $expire, 3600 ); # 1 hour + } + + if ( $expire <= 0 ) { + return 0; // not cachable + } else { + return $expire; + } + } + + /** + * @return bool + */ + function isCacheable() { + return $this->getCacheExpiry() > 0; + } + + /** + * Return true if this cached output object predates the global or + * per-article cache invalidation timestamps, or if it comes from + * an incompatible older version. + * + * @param $touched String: the affected article's last touched timestamp + * @return Boolean + */ + public function expired( $touched ) { + global $wgCacheEpoch; + return !$this->isCacheable() || // parser says it's uncacheable + $this->getCacheTime() < $touched || + $this->getCacheTime() <= $wgCacheEpoch || + $this->getCacheTime() < wfTimestamp( TS_MW, time() - $this->getCacheExpiry() ) || // expiry period has passed + !isset( $this->mVersion ) || + version_compare( $this->mVersion, Parser::VERSION, "lt" ); + } + +} diff --git a/includes/parser/CoreLinkFunctions.php b/includes/parser/CoreLinkFunctions.php index 8de13278..4bfa9d35 100644 --- a/includes/parser/CoreLinkFunctions.php +++ b/includes/parser/CoreLinkFunctions.php @@ -2,7 +2,23 @@ /** * Link functions provided by MediaWiki core; experimental * + * 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 Parser */ /** diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php index 0e5702b7..8917b6d0 100644 --- a/includes/parser/CoreParserFunctions.php +++ b/includes/parser/CoreParserFunctions.php @@ -2,7 +2,23 @@ /** * Parser functions provided by MediaWiki core * + * 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 Parser */ /** @@ -55,6 +71,7 @@ class CoreParserFunctions { $parser->setFunctionHook( 'padright', array( __CLASS__, 'padright' ), SFH_NO_HASH ); $parser->setFunctionHook( 'anchorencode', array( __CLASS__, 'anchorencode' ), SFH_NO_HASH ); $parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) ); + $parser->setFunctionHook( 'speciale', array( __CLASS__, 'speciale' ) ); $parser->setFunctionHook( 'defaultsort', array( __CLASS__, 'defaultsort' ), SFH_NO_HASH ); $parser->setFunctionHook( 'filepath', array( __CLASS__, 'filepath' ), SFH_NO_HASH ); $parser->setFunctionHook( 'pagesincategory', array( __CLASS__, 'pagesincategory' ), SFH_NO_HASH ); @@ -62,6 +79,7 @@ class CoreParserFunctions { $parser->setFunctionHook( 'protectionlevel', array( __CLASS__, 'protectionlevel' ), SFH_NO_HASH ); $parser->setFunctionHook( 'namespace', array( __CLASS__, 'mwnamespace' ), SFH_NO_HASH ); $parser->setFunctionHook( 'namespacee', array( __CLASS__, 'namespacee' ), SFH_NO_HASH ); + $parser->setFunctionHook( 'namespacenumber', array( __CLASS__, 'namespacenumber' ), SFH_NO_HASH ); $parser->setFunctionHook( 'talkspace', array( __CLASS__, 'talkspace' ), SFH_NO_HASH ); $parser->setFunctionHook( 'talkspacee', array( __CLASS__, 'talkspacee' ), SFH_NO_HASH ); $parser->setFunctionHook( 'subjectspace', array( __CLASS__, 'subjectspace' ), SFH_NO_HASH ); @@ -111,7 +129,8 @@ class CoreParserFunctions { * @return mixed|string */ static function formatDate( $parser, $date, $defaultPref = null ) { - $df = DateFormatter::getInstance(); + $lang = $parser->getFunctionLang(); + $df = DateFormatter::getInstance( $lang ); $date = trim( $date ); @@ -158,6 +177,7 @@ class CoreParserFunctions { * @param $parser Parser object * @param $s String: The text to encode. * @param $arg String (optional): The type of encoding. + * @return string */ static function urlencode( $parser, $s = '', $arg = null ) { static $magicWords = null; @@ -283,8 +303,10 @@ class CoreParserFunctions { // Some shortcuts to avoid loading user data unnecessarily if ( count( $forms ) === 0 ) { + wfProfileOut( __METHOD__ ); return ''; } elseif ( count( $forms ) === 1 ) { + wfProfileOut( __METHOD__ ); return $forms[0]; } @@ -303,9 +325,9 @@ class CoreParserFunctions { // check parameter, or use the ParserOptions if in interface message $user = User::newFromName( $username ); if ( $user ) { - $gender = $user->getOption( 'gender' ); + $gender = GenderCache::singleton()->getGenderOf( $user, __METHOD__ ); } elseif ( $username === '' && $parser->getOptions()->getInterfaceMessage() ) { - $gender = $parser->getOptions()->getUser()->getOption( 'gender' ); + $gender = GenderCache::singleton()->getGenderOf( $parser->getOptions()->getUser(), __METHOD__ ); } $ret = $parser->getFunctionLang()->gender( $gender, $forms ); wfProfileOut( __METHOD__ ); @@ -320,6 +342,7 @@ class CoreParserFunctions { static function plural( $parser, $text = '' ) { $forms = array_slice( func_get_args(), 2 ); $text = $parser->getFunctionLang()->parseFormattedNumber( $text ); + settype( $text, ctype_digit( $text ) ? 'int' : 'float' ); return $parser->getFunctionLang()->convertPlural( $text, $forms ); } @@ -420,6 +443,7 @@ class CoreParserFunctions { * corresponding magic word * Note: function name changed to "mwnamespace" rather than "namespace" * to not break PHP 5.3 + * @return mixed|string */ static function mwnamespace( $parser, $title = null ) { $t = Title::newFromText( $title ); @@ -433,6 +457,12 @@ class CoreParserFunctions { return ''; return wfUrlencode( $t->getNsText() ); } + static function namespacenumber( $parser, $title = null ) { + $t = Title::newFromText( $title ); + if ( is_null( $t ) ) + return ''; + return $t->getNamespace(); + } static function talkspace( $parser, $title = null ) { $t = Title::newFromText( $title ); if ( is_null( $t ) || !$t->canTalk() ) @@ -461,6 +491,7 @@ class CoreParserFunctions { /** * Functions to get and normalize pagenames, corresponding to the magic words * of the same names + * @return String */ static function pagename( $parser, $title = null ) { $t = Title::newFromText( $title ); @@ -536,28 +567,64 @@ class CoreParserFunctions { } /** - * Return the number of pages in the given category, or 0 if it's nonexis- - * tent. This is an expensive parser function and can't be called too many - * times per page. + * Return the number of pages, files or subcats in the given category, + * or 0 if it's nonexistent. This is an expensive parser function and + * can't be called too many times per page. + * @return string */ - static function pagesincategory( $parser, $name = '', $raw = null ) { + static function pagesincategory( $parser, $name = '', $arg1 = null, $arg2 = null ) { + static $magicWords = null; + if ( is_null( $magicWords ) ) { + $magicWords = new MagicWordArray( array( + 'pagesincategory_all', + 'pagesincategory_pages', + 'pagesincategory_subcats', + 'pagesincategory_files' + ) ); + } static $cache = array(); - $category = Category::newFromName( $name ); - if( !is_object( $category ) ) { - $cache[$name] = 0; + // split the given option to its variable + if( self::isRaw( $arg1 ) ) { + //{{pagesincategory:|raw[|type]}} + $raw = $arg1; + $type = $magicWords->matchStartToEnd( $arg2 ); + } else { + //{{pagesincategory:[|type[|raw]]}} + $type = $magicWords->matchStartToEnd( $arg1 ); + $raw = $arg2; + } + if( !$type ) { //backward compatibility + $type = 'pagesincategory_all'; + } + + $title = Title::makeTitleSafe( NS_CATEGORY, $name ); + if( !$title ) { # invalid title return self::formatRaw( 0, $raw ); } - # Normalize name for cache - $name = $category->getName(); + // Normalize name for cache + $name = $title->getDBkey(); - $count = 0; - if( isset( $cache[$name] ) ) { - $count = $cache[$name]; - } elseif( $parser->incrementExpensiveFunctionCount() ) { - $count = $cache[$name] = (int)$category->getPageCount(); + if( !isset( $cache[$name] ) ) { + $category = Category::newFromTitle( $title ); + + $allCount = $subcatCount = $fileCount = $pagesCount = 0; + if( $parser->incrementExpensiveFunctionCount() ) { + // $allCount is the total number of cat members, + // not the count of how many members are normal pages. + $allCount = (int)$category->getPageCount(); + $subcatCount = (int)$category->getSubcatCount(); + $fileCount = (int)$category->getFileCount(); + $pagesCount = $allCount - $subcatCount - $fileCount; + } + $cache[$name]['pagesincategory_all'] = $allCount; + $cache[$name]['pagesincategory_pages'] = $pagesCount; + $cache[$name]['pagesincategory_subcats'] = $subcatCount; + $cache[$name]['pagesincategory_files'] = $fileCount; } + + $count = $cache[$name][$type]; return self::formatRaw( $count, $raw ); } @@ -576,6 +643,7 @@ class CoreParserFunctions { * @param $parser Parser * @param $page String TODO DOCUMENT (Default: empty string) * @param $raw TODO DOCUMENT (Default: null) + * @return string */ static function pagesize( $parser, $page = '', $raw = null ) { static $cache = array(); @@ -593,7 +661,7 @@ class CoreParserFunctions { if( isset( $cache[$page] ) ) { $length = $cache[$page]; } elseif( $parser->incrementExpensiveFunctionCount() ) { - $rev = Revision::newFromTitle( $title ); + $rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); $id = $rev ? $rev->getPage() : 0; $length = $cache[$page] = $rev ? $rev->getSize() : 0; @@ -605,7 +673,8 @@ class CoreParserFunctions { /** * Returns the requested protection level for the current page - */ + * @return string + */ static function protectionlevel( $parser, $type = '' ) { $restrictions = $parser->mTitle->getRestrictions( strtolower( $type ) ); # Title::getRestrictions returns an array, its possible it may have @@ -616,26 +685,20 @@ class CoreParserFunctions { /** * Gives language names. * @param $parser Parser - * @param $code String Language code - * @param $language String Language code + * @param $code String Language code (of which to get name) + * @param $inLanguage String Language code (in which to get name) * @return String */ - static function language( $parser, $code = '', $language = '' ) { - global $wgContLang; + static function language( $parser, $code = '', $inLanguage = '' ) { $code = strtolower( $code ); - $language = strtolower( $language ); - - if ( $language !== '' ) { - $names = Language::getTranslatedLanguageNames( $language ); - return isset( $names[$code] ) ? $names[$code] : wfBCP47( $code ); - } - - $lang = $wgContLang->getLanguageName( $code ); + $inLanguage = strtolower( $inLanguage ); + $lang = Language::fetchLanguageName( $code, $inLanguage ); return $lang !== '' ? $lang : wfBCP47( $code ); } /** * Unicode-safe str_pad with the restriction that $length is forced to be <= 500 + * @return string */ static function pad( $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT ) { $padding = $parser->killMarkers( $padding ); @@ -683,12 +746,16 @@ class CoreParserFunctions { list( $page, $subpage ) = SpecialPageFactory::resolveAlias( $text ); if ( $page ) { $title = SpecialPage::getTitleFor( $page, $subpage ); - return $title; + return $title->getPrefixedText(); } else { - return wfMsgForContent( 'nosuchspecialpage' ); + return wfMessage( 'nosuchspecialpage' )->inContentLanguage()->text(); } } + static function speciale( $parser, $text ) { + return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) ); + } + /** * @param $parser Parser * @param $text String The sortkey to use @@ -716,48 +783,39 @@ class CoreParserFunctions { return ''; } else { return( '<span class="error">' . - wfMsgForContent( 'duplicate-defaultsort', - htmlspecialchars( $old ), - htmlspecialchars( $text ) ) . + wfMessage( 'duplicate-defaultsort', $old, $text )->inContentLanguage()->escaped() . '</span>' ); } } // Usage {{filepath|300}}, {{filepath|nowiki}}, {{filepath|nowiki|300}} or {{filepath|300|nowiki}} + // or {{filepath|300px}}, {{filepath|200x300px}}, {{filepath|nowiki|200x300px}}, {{filepath|200x300px|nowiki}} public static function filepath( $parser, $name='', $argA='', $argB='' ) { $file = wfFindFile( $name ); - $size = ''; - $argA_int = intval( $argA ); - $argB_int = intval( $argB ); - - if ( $argB_int > 0 ) { - // {{filepath: | option | size }} - $size = $argB_int; - $option = $argA; - - } elseif ( $argA_int > 0 ) { - // {{filepath: | size [|option] }} - $size = $argA_int; - $option = $argB; + if( $argA == 'nowiki' ) { + // {{filepath: | option [| size] }} + $isNowiki = true; + $parsedWidthParam = $parser->parseWidthParam( $argB ); } else { - // {{filepath: [|option] }} - $option = $argA; + // {{filepath: [| size [|option]] }} + $parsedWidthParam = $parser->parseWidthParam( $argA ); + $isNowiki = ($argB == 'nowiki'); } if ( $file ) { $url = $file->getFullUrl(); // If a size is requested... - if ( is_integer( $size ) ) { - $mto = $file->transform( array( 'width' => $size ) ); + if ( count( $parsedWidthParam ) ) { + $mto = $file->transform( $parsedWidthParam ); // ... and we can if ( $mto && !$mto->isError() ) { // ... change the URL to point to a thumbnail. $url = wfExpandUrl( $mto->getUrl(), PROTO_RELATIVE ); } } - if ( $option == 'nowiki' ) { + if ( $isNowiki ) { return array( $url, 'nowiki' => true ); } return $url; @@ -768,6 +826,7 @@ class CoreParserFunctions { /** * Parser function to extension tag adaptor + * @return string */ public static function tagObj( $parser, $frame, $args ) { if ( !count( $args ) ) { @@ -784,7 +843,7 @@ class CoreParserFunctions { $stripList = $parser->getStripList(); if ( !in_array( $tagName, $stripList ) ) { return '<span class="error">' . - wfMsgForContent( 'unknown_extension_tag', $tagName ) . + wfMessage( 'unknown_extension_tag', $tagName )->inContentLanguage()->text() . '</span>'; } diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php index 7d488c4b..296be66f 100644 --- a/includes/parser/CoreTagHooks.php +++ b/includes/parser/CoreTagHooks.php @@ -2,7 +2,23 @@ /** * Tag hooks provided by MediaWiki core * + * 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 Parser */ /** diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php index 6559e886..2917b4a7 100644 --- a/includes/parser/DateFormatter.php +++ b/includes/parser/DateFormatter.php @@ -2,7 +2,23 @@ /** * Date formatter * + * 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 Parser */ /** @@ -10,14 +26,15 @@ * @todo preferences, OutputPage * @ingroup Parser */ -class DateFormatter -{ +class DateFormatter { var $mSource, $mTarget; var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD; var $regexes, $pDays, $pMonths, $pYears; var $rules, $xMonths, $preferences; + protected $lang; + const ALL = -1; const NONE = 0; const MDY = 1; @@ -32,15 +49,15 @@ class DateFormatter const LAST = 8; /** - * @todo document + * @param $lang Language In which language to format the date */ - function __construct() { - global $wgContLang; + function __construct( Language $lang ) { + $this->lang = $lang; $this->monthNames = $this->getMonthRegex(); for ( $i=1; $i<=12; $i++ ) { - $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i; - $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i; + $this->xMonths[$this->lang->lc( $this->lang->getMonthName( $i ) )] = $i; + $this->xMonths[$this->lang->lc( $this->lang->getMonthAbbreviation( $i ) )] = $i; } $this->regexTrail = '(?![a-z])/iu'; @@ -103,16 +120,20 @@ class DateFormatter /** * Get a DateFormatter object * + * @param $lang Language|string|null In which language to format the date + * Defaults to the site content language * @return DateFormatter object */ - public static function &getInstance() { - global $wgMemc; + public static function &getInstance( $lang = null ) { + global $wgMemc, $wgContLang; static $dateFormatter = false; + $lang = $lang ? wfGetLangObj( $lang ) : $wgContLang; + $key = wfMemcKey( 'dateformatter', $lang->getCode() ); if ( !$dateFormatter ) { - $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) ); + $dateFormatter = $wgMemc->get( $key ); if ( !$dateFormatter ) { - $dateFormatter = new DateFormatter; - $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 ); + $dateFormatter = new DateFormatter( $lang ); + $wgMemc->set( $key, $dateFormatter, 3600 ); } } return $dateFormatter; @@ -122,12 +143,12 @@ class DateFormatter * @param $preference String: User preference * @param $text String: Text to reformat * @param $options Array: can contain 'linked' and/or 'match-whole' + * @return mixed|String */ function reformat( $preference, $text, $options = array('linked') ) { - $linked = in_array( 'linked', $options ); $match_whole = in_array( 'match-whole', $options ); - + if ( isset( $this->preferences[$preference] ) ) { $preference = $this->preferences[$preference]; } else { @@ -149,19 +170,19 @@ class DateFormatter $this->mTarget = $i; } $regex = $this->regexes[$i]; - + // Horrible hack if (!$linked) { $regex = str_replace( array( '\[\[', '\]\]' ), '', $regex ); } - + if ($match_whole) { // Let's hope this works $regex = preg_replace( '!^/!', '/^', $regex ); $regex = str_replace( $this->regexTrail, '$'.$this->regexTrail, $regex ); } - + // Another horrible hack $this->mLinked = $linked; $text = preg_replace_callback( $regex, array( &$this, 'replace' ), $text ); @@ -172,6 +193,7 @@ class DateFormatter /** * @param $matches + * @return string */ function replace( $matches ) { # Extract information from $matches @@ -186,10 +208,15 @@ class DateFormatter $bits[$key[$p]] = $matches[$p+1]; } } - + return $this->formatDate( $bits, $linked ); } - + + /** + * @param $bits array + * @param $link bool + * @return string + */ function formatDate( $bits, $link = true ) { $format = $this->targets[$this->mTarget]; @@ -203,13 +230,13 @@ class DateFormatter # Construct new date $text = ''; $fail = false; - + // Pre-generate y/Y stuff because we need the year for the <span> title. if ( !isset( $bits['y'] ) && isset( $bits['Y'] ) ) $bits['y'] = $this->makeIsoYear( $bits['Y'] ); if ( !isset( $bits['Y'] ) && isset( $bits['y'] ) ) $bits['Y'] = $this->makeNormalYear( $bits['y'] ); - + if ( !isset( $bits['m'] ) ) { $m = $this->makeIsoMonth( $bits['F'] ); if ( !$m || $m == '00' ) { @@ -218,7 +245,7 @@ class DateFormatter $bits['m'] = $m; } } - + if ( !isset($bits['d']) ) { $bits['d'] = sprintf( '%02d', $bits['j'] ); } @@ -248,8 +275,7 @@ class DateFormatter if ( $m > 12 || $m < 1 ) { $fail = true; } else { - global $wgContLang; - $text .= $wgContLang->getMonthName( $m ); + $text .= $this->lang->getMonthName( $m ); } } else { $text .= ucfirst( $bits['F'] ); @@ -265,30 +291,30 @@ class DateFormatter if ( $fail ) { $text = $matches[0]; } - + $isoBits = array(); if ( isset($bits['y']) ) $isoBits[] = $bits['y']; $isoBits[] = $bits['m']; $isoBits[] = $bits['d']; $isoDate = implode( '-', $isoBits ); - + // Output is not strictly HTML (it's wikitext), but <span> is whitelisted. $text = Html::rawElement( 'span', array( 'class' => 'mw-formatted-date', 'title' => $isoDate ), $text ); - + return $text; } /** * @todo document + * @return string */ function getMonthRegex() { - global $wgContLang; $names = array(); for( $i = 1; $i <= 12; $i++ ) { - $names[] = $wgContLang->getMonthName( $i ); - $names[] = $wgContLang->getMonthAbbreviation( $i ); + $names[] = $this->lang->getMonthName( $i ); + $names[] = $this->lang->getMonthAbbreviation( $i ); } return implode( '|', $names ); } @@ -299,9 +325,7 @@ class DateFormatter * @return string ISO month name */ function makeIsoMonth( $monthName ) { - global $wgContLang; - - $n = $this->xMonths[$wgContLang->lc( $monthName )]; + $n = $this->xMonths[$this->lang->lc( $monthName )]; return sprintf( '%02d', $n ); } @@ -325,6 +349,7 @@ class DateFormatter /** * @todo document + * @return int|string */ function makeNormalYear( $iso ) { if ( $iso[0] == '-' ) { diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php index fb013047..d9356b48 100644 --- a/includes/parser/LinkHolderArray.php +++ b/includes/parser/LinkHolderArray.php @@ -2,7 +2,23 @@ /** * Holder of replacement pairs for wiki links * + * 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 Parser */ /** @@ -33,7 +49,8 @@ class LinkHolderArray { * serializing at present. * * Compact the titles, only serialize the text form. - */ + * @return array + */ function __sleep() { foreach ( $this->internals as &$nsLinks ) { foreach ( $nsLinks as &$entry ) { @@ -134,6 +151,7 @@ class LinkHolderArray { /** * Get a subset of the current LinkHolderArray which is sufficient to * interpret the given text. + * @return LinkHolderArray */ function getSubArray( $text ) { $sub = new LinkHolderArray( $this->parent ); @@ -167,6 +185,7 @@ class LinkHolderArray { /** * Returns true if the memory requirements of this object are getting large + * @return bool */ function isBig() { global $wgLinkHolderBatchSize; @@ -190,6 +209,11 @@ class LinkHolderArray { * article length checks (for stub links) to be bundled into a single query. * * @param $nt Title + * @param $text String + * @param $query Array [optional] + * @param $trail String [optional] + * @param $prefix String [optional] + * @return string */ function makeHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) { wfProfileIn( __METHOD__ ); @@ -433,7 +457,7 @@ class LinkHolderArray { foreach ( $entries as $index => $entry ) { $pdbk = $entry['pdbk']; // we only deal with new links (in its first query) - if ( !isset( $colours[$pdbk] ) ) { + if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) { $title = $entry['title']; $titleText = $title->getText(); $titlesAttrs[] = array( @@ -449,7 +473,7 @@ class LinkHolderArray { } // Now do the conversion and explode string to text of titles - $titlesAllVariants = $wgContLang->autoConvertToAllVariants( $titlesToBeConverted ); + $titlesAllVariants = $wgContLang->autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) ); $allVariantsName = array_keys( $titlesAllVariants ); foreach ( $titlesAllVariants as &$titlesVariant ) { $titlesVariant = explode( "\0", $titlesVariant ); @@ -517,7 +541,7 @@ class LinkHolderArray { $entry =& $this->internals[$ns][$index]; $pdbk = $entry['pdbk']; - if(!isset($colours[$pdbk])){ + if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) { // found link in some of the variants, replace the link holder data $entry['title'] = $variantTitle; $entry['pdbk'] = $varPdbk; diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index d4f167c9..2a24bee7 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -1,12 +1,29 @@ <?php /** - * @defgroup Parser Parser + * PHP parser that converts wiki markup to HTML. + * + * 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 Parser - * File for Parser and related classes */ +/** + * @defgroup Parser Parser + */ /** * PHP Parser - Processes wiki markup (which uses a more user-friendly @@ -14,36 +31,38 @@ * transformation of that wiki markup it into XHTML output / markup * (which in turn the browser understands, and can display). * - * <pre> - * There are five main entry points into the Parser class: - * parse() + * There are seven main entry points into the Parser class: + * + * - Parser::parse() * produces HTML output - * preSaveTransform(). + * - Parser::preSaveTransform(). * produces altered wiki markup. - * preprocess() + * - Parser::preprocess() * removes HTML comments and expands templates - * cleanSig() / cleanSigInSig() + * - Parser::cleanSig() and Parser::cleanSigInSig() * Cleans a signature before saving it to preferences - * getSection() + * - Parser::getSection() * Return the content of a section from an article for section editing - * replaceSection() + * - Parser::replaceSection() * Replaces a section by number inside an article - * getPreloadText() + * - Parser::getPreloadText() * Removes <noinclude> sections, and <includeonly> tags. * * Globals used: * object: $wgContLang * - * NOT $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away! + * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away! * - * settings: - * $wgUseDynamicDates*, $wgInterwikiMagic*, - * $wgNamespacesWithSubpages, $wgAllowExternalImages*, - * $wgLocaltimezone, $wgAllowSpecialInclusion*, - * $wgMaxArticleSize* + * @par Settings: + * $wgLocaltimezone + * $wgNamespacesWithSubpages * - * * only within ParserOptions - * </pre> + * @par Settings only within ParserOptions: + * $wgAllowExternalImages + * $wgAllowSpecialInclusion + * $wgInterwikiMagic + * $wgMaxArticleSize + * $wgUseDynamicDates * * @ingroup Parser */ @@ -144,7 +163,8 @@ class Parser { var $mLinkHolders; var $mLinkID; - var $mIncludeSizes, $mPPNodeCount, $mDefaultSort; + var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth; + var $mDefaultSort; var $mTplExpandCache; # empty-frame expansion cache var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores; var $mExpensiveFunctionCount; # number of expensive parser function calls @@ -188,7 +208,7 @@ class Parser { public function __construct( $conf = array() ) { $this->mConf = $conf; $this->mUrlProtocols = wfUrlProtocols(); - $this->mExtLinkBracketedRegex = '/\[((' . wfUrlProtocols() . ')'. + $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')'. self::EXT_LINK_URL_CLASS.'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su'; if ( isset( $conf['preprocessorClass'] ) ) { $this->mPreprocessorClass = $conf['preprocessorClass']; @@ -273,8 +293,6 @@ class Parser { * Must not consist of all title characters, or else it will change * the behaviour of <nowiki> in a link. */ - # $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString(); - # Changed to \x7f to allow XML double-parsing -- TS $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString(); $this->mStripState = new StripState( $this->mUniqPrefix ); @@ -289,6 +307,8 @@ class Parser { 'arg' => 0, ); $this->mPPNodeCount = 0; + $this->mGeneratedPPNodeCount = 0; + $this->mHighestExpansionDepth = 0; $this->mDefaultSort = false; $this->mHeadings = array(); $this->mDoubleUnderscores = array(); @@ -321,13 +341,18 @@ class Parser { * to internalParse() which does all the real work. */ - global $wgUseTidy, $wgAlwaysUseTidy, $wgDisableLangConversion, $wgDisableTitleConversion; + global $wgUseTidy, $wgAlwaysUseTidy; $fname = __METHOD__.'-' . wfGetCaller(); wfProfileIn( __METHOD__ ); wfProfileIn( $fname ); $this->startParse( $title, $options, self::OT_HTML, $clearState ); + # Remove the strip marker tag prefix from the input, if present. + if ( $clearState ) { + $text = str_replace( $this->mUniqPrefix, '', $text ); + } + $oldRevisionId = $this->mRevisionId; $oldRevisionObject = $this->mRevisionObject; $oldRevisionTimestamp = $this->mRevisionTimestamp; @@ -343,6 +368,7 @@ class Parser { # No more strip! wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); $text = $this->internalParse( $text ); + wfRunHooks( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) ); $text = $this->mStripState->unstripGeneral( $text ); @@ -368,9 +394,8 @@ class Parser { * c) It's a conversion table * d) it is an interface message (which is in the user language) */ - if ( !( $wgDisableLangConversion - || isset( $this->mDoubleUnderscores['nocontentconvert'] ) - || $this->mTitle->isConversionTable() ) ) + if ( !( $options->getDisableContentConversion() + || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) ) { # Run convert unconditionally in 1.18-compatible mode global $wgBug34832TransitionalRollback; @@ -389,8 +414,7 @@ class Parser { * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over * automatic link conversion. */ - if ( !( $wgDisableLangConversion - || $wgDisableTitleConversion + if ( !( $options->getDisableTitleConversion() || isset( $this->mDoubleUnderscores['nocontentconvert'] ) || isset( $this->mDoubleUnderscores['notitleconvert'] ) || $this->mOutput->getDisplayTitle() !== false ) ) @@ -442,9 +466,12 @@ class Parser { array_values( $tidyregs ), $text ); } - global $wgExpensiveParserFunctionLimit; - if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) { - $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit ); + + if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) { + $this->limitationWarn( 'expensive-parserfunction', + $this->mExpensiveFunctionCount, + $this->mOptions->getExpensiveParserFunctionLimit() + ); } wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); @@ -452,12 +479,15 @@ class Parser { # Information on include size limits, for the benefit of users who try to skirt them if ( $this->mOptions->getEnableLimitReport() ) { $max = $this->mOptions->getMaxIncludeSize(); - $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n"; + $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/{$this->mOptions->getExpensiveParserFunctionLimit()}\n"; $limitReport = "NewPP limit report\n" . - "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" . + "Preprocessor visited node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" . + "Preprocessor generated node count: " . + "{$this->mGeneratedPPNodeCount}/{$this->mOptions->getMaxGeneratedPPNodeCount()}\n" . "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" . "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n". + "Highest expansion depth: {$this->mHighestExpansionDepth}/{$this->mOptions->getMaxPPExpandDepth()}\n". $PFreport; wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) ); $text .= "\n<!-- \n$limitReport-->\n"; @@ -497,6 +527,7 @@ class Parser { /** * Expand templates and variables in the text, producing valid, static wikitext. * Also removes comments. + * @return mixed|string */ function preprocess( $text, Title $title, ParserOptions $options, $revid = null ) { wfProfileIn( __METHOD__ ); @@ -530,10 +561,11 @@ class Parser { } /** - * Process the wikitext for the ?preload= feature. (bug 5210) + * Process the wikitext for the "?preload=" feature. (bug 5210) * - * <noinclude>, <includeonly> etc. are parsed as for template transclusion, - * comments, templates, arguments, tags hooks and parser functions are untouched. + * "<noinclude>", "<includeonly>" etc. are parsed as for template + * transclusion, comments, templates, arguments, tags hooks and parser + * functions are untouched. * * @param $text String * @param $title Title @@ -557,7 +589,7 @@ class Parser { * @return string */ static public function getRandomString() { - return dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) ); + return wfRandomString( 16 ); } /** @@ -619,7 +651,7 @@ class Parser { /** * Accessor/mutator for the Title object * - * @param $x New Title object or null to just get the current one + * @param $x Title object or null to just get the current one * @return Title object */ function Title( $x = null ) { @@ -645,7 +677,7 @@ class Parser { /** * Accessor/mutator for the output type * - * @param $x New value or null to just get the current one + * @param $x int|null New value or null to just get the current one * @return Integer */ function OutputType( $x = null ) { @@ -673,8 +705,8 @@ class Parser { /** * Accessor/mutator for the ParserOptions object * - * @param $x New value or null to just get the current one - * @return Current ParserOptions object + * @param $x ParserOptions New value or null to just get the current one + * @return ParserOptions Current ParserOptions object */ function Options( $x = null ) { return wfSetVar( $this->mOptions, $x ); @@ -703,18 +735,24 @@ class Parser { } /** - * Get the target language for the content being parsed. This is usually the - * language that the content is in. + * Get the target language for the content being parsed. This is usually the + * language that the content is in. + * + * @since 1.19 + * + * @return Language|null */ - function getTargetLanguage() { + public function getTargetLanguage() { $target = $this->mOptions->getTargetLanguage(); + if ( $target !== null ) { return $target; } elseif( $this->mOptions->getInterfaceMessage() ) { return $this->mOptions->getUserLangObj(); } elseif( is_null( $this->mTitle ) ) { - throw new MWException( __METHOD__.': $this->mTitle is null' ); + throw new MWException( __METHOD__ . ': $this->mTitle is null' ); } + return $this->mTitle->getPageLanguage(); } @@ -761,11 +799,14 @@ class Parser { * in the text with a random marker and returns the next text. The output * parameter $matches will be an associative array filled with data in * the form: + * + * @code * 'UNIQ-xxxxx' => array( * 'element', * 'tag content', * array( 'param' => 'x' ), * '<element param="x">tag content</element>' ) ) + * @endcode * * @param $elements array list of element names. Comments are always extracted. * @param $text string Source text string. @@ -864,6 +905,7 @@ class Parser { * parse the wiki syntax used to render tables * * @private + * @return string */ function doTableStuff( $text ) { wfProfileIn( __METHOD__ ); @@ -1094,6 +1136,7 @@ class Parser { $text = $this->replaceVariables( $text ); } + wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) ); $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) ); wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); @@ -1146,7 +1189,7 @@ class Parser { '!(?: # Start cases (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text (<.*?>) | # m[2]: Skip stuff inside HTML elements' . " - (\\b(?:$prots)$urlChar+) | # m[3]: Free external links" . ' + (\\b(?i:$prots)$urlChar+) | # m[3]: Free external links" . ' (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number ISBN\s+(\b # m[5]: ISBN, capture number (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix @@ -1189,7 +1232,7 @@ class Parser { throw new MWException( __METHOD__.': unrecognised match type "' . substr( $m[0], 0, 20 ) . '"' ); } - $url = wfMsgForContent( $urlmsg, $id ); + $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text(); return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $CssClass ); } elseif ( isset( $m[5] ) && $m[5] !== '' ) { # ISBN @@ -1249,7 +1292,7 @@ class Parser { $text = $this->maybeMakeExternalImage( $url ); if ( $text === false ) { # Not an image, make a link - $text = Linker::makeExternalLink( $url, + $text = Linker::makeExternalLink( $url, $this->getConverterLanguage()->markNoConversion($url), true, 'free', $this->getExternalLinkAttribs( $url ) ); # Register it in the output object... @@ -1646,7 +1689,7 @@ class Parser { } if ( !$text && $this->mOptions->getEnableImageWhitelist() && preg_match( self::EXT_IMAGE_REGEX, $url ) ) { - $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) ); + $whitelist = explode( "\n", wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() ); foreach ( $whitelist as $entry ) { # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments if ( strpos( $entry, '#' ) === 0 || $entry === '' ) { @@ -1698,7 +1741,7 @@ class Parser { $holders = new LinkHolderArray( $this ); - # split the entire text string on occurences of [[ + # split the entire text string on occurrences of [[ $a = StringUtils::explode( '[[', ' ' . $s ); # get the first element (all text up to first [[), and remove the space we added $s = $a->current(); @@ -1711,7 +1754,7 @@ class Parser { if ( $useLinkPrefixExtension ) { # Match the end of a line for a word that's not followed by whitespace, # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched - $e2 = wfMsgForContent( 'linkprefix' ); + $e2 = wfMessage( 'linkprefix' )->inContentLanguage()->text(); } if ( is_null( $this->mTitle ) ) { @@ -1733,7 +1776,7 @@ class Parser { } if ( $this->getConverterLanguage()->hasVariants() ) { - $selflink = $this->getConverterLanguage()->autoConvertToAllVariants( + $selflink = $this->getConverterLanguage()->autoConvertToAllVariants( $this->mTitle->getPrefixedText() ); } else { $selflink = array( $this->mTitle->getPrefixedText() ); @@ -1812,7 +1855,7 @@ class Parser { # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these # should be external links. - if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $m[1] ) ) { + if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $m[1] ) ) { $s .= $prefix . '[[' . $line ; wfProfileOut( __METHOD__."-misc" ); continue; @@ -1902,11 +1945,9 @@ class Parser { # Link not escaped by : , create the various objects if ( $noforce ) { - global $wgContLang; - # Interwikis wfProfileIn( __METHOD__."-interwiki" ); - if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { + if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) { $this->mOutput->addLanguageLink( $nt->getFullText() ); $s = rtrim( $s . $prefix ); $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail; @@ -2051,7 +2092,7 @@ class Parser { * @return String: less-or-more HTML with NOPARSE bits */ function armorLinks( $text ) { - return preg_replace( '/\b(' . wfUrlProtocols() . ')/', + return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/', "{$this->mUniqPrefix}NOPARSE$1", $text ); } @@ -2122,7 +2163,7 @@ class Parser { * element appropriate to the prefix character passed into them. * @private * - * @param $char char + * @param $char string * * @return string */ @@ -2384,7 +2425,7 @@ class Parser { } /** - * Split up a string on ':', ignoring any occurences inside tags + * Split up a string on ':', ignoring any occurrences inside tags * to prevent illegal overlapping. * * @param $str String the string to split @@ -2698,6 +2739,18 @@ class Parser { $subjPage = $this->mTitle->getSubjectPage(); $value = wfEscapeWikiText( $subjPage->getPrefixedUrl() ); break; + case 'pageid': // requested in bug 23427 + $pageid = $this->getTitle()->getArticleId(); + if( $pageid == 0 ) { + # 0 means the page doesn't exist in the database, + # which means the user is previewing a new page. + # The vary-revision flag must be set, because the magic word + # will have a different value once the page is saved. + $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" ); + } + $value = $pageid ? $pageid : null; + break; case 'revisionid': # Let the edit saving system know we should parse the page # *after* a revision ID has been assigned. @@ -2760,6 +2813,9 @@ class Parser { case 'namespacee': $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); break; + case 'namespacenumber': + $value = $this->mTitle->getNamespace(); + break; case 'talkspace': $value = $this->mTitle->canTalk() ? str_replace( '_',' ',$this->mTitle->getTalkNsText() ) : ''; break; @@ -2834,7 +2890,8 @@ class Parser { $value = $pageLang->formatNum( SiteStats::edits() ); break; case 'numberofviews': - $value = $pageLang->formatNum( SiteStats::views() ); + global $wgDisableCounters; + $value = !$wgDisableCounters ? $pageLang->formatNum( SiteStats::views() ) : ''; break; case 'currenttimestamp': $value = wfTimestamp( TS_MW, $ts ); @@ -2900,7 +2957,7 @@ class Parser { * * @param $text String: The text to parse * @param $flags Integer: bitwise combination of: - * self::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being + * self::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" as if the text is being * included. Default is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. @@ -3027,13 +3084,14 @@ class Parser { * 'post-expand-template-inclusion' (corresponding messages: * 'post-expand-template-inclusion-warning', * 'post-expand-template-inclusion-category') - * @param $current Current value - * @param $max Maximum allowed, when an explicit limit has been + * @param $current int|null Current value + * @param $max int|null Maximum allowed, when an explicit limit has been * exceeded, provide the values (optional) */ - function limitationWarn( $limitationType, $current=null, $max=null) { + function limitationWarn( $limitationType, $current = '', $max = '' ) { # does no harm if $current and $max are present but are unnecessary for the message - $warning = wfMsgExt( "$limitationType-warning", array( 'parsemag', 'escape' ), $current, $max ); + $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max ) + ->inContentLanguage()->escaped(); $this->mOutput->addWarning( $warning ); $this->addTrackingCategory( "$limitationType-category" ); } @@ -3051,7 +3109,7 @@ class Parser { * @private */ function braceSubstitution( $piece, $frame ) { - global $wgNonincludableNamespaces, $wgContLang; + global $wgContLang; wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__.'-setup' ); @@ -3238,7 +3296,8 @@ class Parser { if ( $frame->depth >= $limit ) { $found = true; $text = '<span class="error">' - . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) + . wfMessage( 'parser-template-recursion-depth-warning' ) + ->numParams( $limit )->inContentLanguage()->text() . '</span>'; } } @@ -3246,8 +3305,11 @@ class Parser { # Load from database if ( !$found && $title ) { - $titleProfileIn = __METHOD__ . "-title-" . $title->getDBKey(); - wfProfileIn( $titleProfileIn ); // template in + if ( !Profiler::instance()->isPersistent() ) { + # Too many unique items can kill profiling DBs/collectors + $titleProfileIn = __METHOD__ . "-title-" . $title->getDBKey(); + wfProfileIn( $titleProfileIn ); // template in + } wfProfileIn( __METHOD__ . '-loadtpl' ); if ( !$title->isExternal() ) { if ( $title->isSpecialPage() @@ -3281,7 +3343,7 @@ class Parser { $isHTML = true; $this->disableCache(); } - } elseif ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { + } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) { $found = false; # access denied wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() ); } else { @@ -3315,7 +3377,9 @@ class Parser { # This has to be done after redirect resolution to avoid infinite loops via redirects if ( !$frame->loopCheck( $title ) ) { $found = true; - $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>'; + $text = '<span class="error">' + . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text() + . '</span>'; wfDebug( __METHOD__.": template loop broken at '$titleText'\n" ); } wfProfileOut( __METHOD__ . '-loadtpl' ); @@ -3362,10 +3426,8 @@ class Parser { } # Replace raw HTML by a placeholder - # Add a blank line preceding, to prevent it from mucking up - # immediately preceding headings if ( $isHTML ) { - $text = "\n\n" . $this->insertStripItem( $text ); + $text = $this->insertStripItem( $text ); } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) { # Escape nowiki-style return values $text = wfEscapeWikiText( $text ); @@ -3476,7 +3538,7 @@ class Parser { * Static function to get a template * Can be overridden via ParserOptions::setTemplateCallback(). * - * @parma $title Title + * @param $title Title * @param $parser Parser * * @return array @@ -3505,7 +3567,7 @@ class Parser { # Get the revision $rev = $id ? Revision::newFromId( $id ) - : Revision::newFromTitle( $title ); + : Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); $rev_id = $rev ? $rev->getId() : 0; # If there is no current revision, there is no page if ( $id === false && !$rev ) { @@ -3556,7 +3618,7 @@ class Parser { * If 'broken' is a key in $options then the file will appear as a broken thumbnail. * @param Title $title * @param Array $options Array of options to RepoGroup::findFile - * @return File|false + * @return File|bool */ function fetchFile( $title, $options = array() ) { $res = $this->fetchFileAndTitle( $title, $options ); @@ -3607,13 +3669,13 @@ class Parser { global $wgEnableScaryTranscluding; if ( !$wgEnableScaryTranscluding ) { - return wfMsgForContent('scarytranscludedisabled'); + return wfMessage('scarytranscludedisabled')->inContentLanguage()->text(); } $url = $title->getFullUrl( "action=$action" ); if ( strlen( $url ) > 255 ) { - return wfMsgForContent( 'scarytranscludetoolong' ); + return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text(); } return $this->fetchScaryTemplateMaybeFromCache( $url ); } @@ -3634,7 +3696,7 @@ class Parser { $text = Http::get( $url ); if ( !$text ) { - return wfMsgForContent( 'scarytranscludefailed', $url ); + return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text(); } $dbw = wfGetDB( DB_MASTER ); @@ -3650,7 +3712,7 @@ class Parser { * Triple brace replacement -- used for template arguments * @private * - * @param $peice array + * @param $piece array * @param $frame PPFrame * * @return array @@ -3700,7 +3762,7 @@ class Parser { * Return the text to be used for a given extension tag. * This is the ghost of strip(). * - * @param $params Associative array of parameters: + * @param $params array Associative array of parameters: * name PPNode for the tag name * attr PPNode for unparsed text where tag attributes are thought to be * attributes Optional associative array of parsed attributes @@ -3808,12 +3870,8 @@ class Parser { * @return Boolean: false if the limit has been exceeded */ function incrementExpensiveFunctionCount() { - global $wgExpensiveParserFunctionLimit; $this->mExpensiveFunctionCount++; - if ( $this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit ) { - return true; - } - return false; + return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit(); } /** @@ -3921,6 +3979,7 @@ class Parser { * @param $text String * @param $origText String: original, untouched wikitext * @param $isMain Boolean + * @return mixed|string * @private */ function formatHeadings( $text, $origText, $isMain=true ) { @@ -4147,7 +4206,7 @@ class Parser { # Don't number the heading if it is the only one (looks silly) if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) { # the two are different if the line contains a link - $headline = $numbering . ' ' . $headline; + $headline = Html::element( 'span', array( 'class' => 'mw-headline-number' ), $numbering ) . ' ' . $headline; } # Create the anchor for linking from the TOC to the section @@ -4286,7 +4345,7 @@ class Parser { } /** - * Transform wiki markup when saving a page by doing \r\n -> \n + * Transform wiki markup when saving a page by doing "\r\n" -> "\n" * conversion, substitting signatures, {{subst:}} templates, etc. * * @param $text String: the text to transform @@ -4362,7 +4421,7 @@ class Parser { $text = $this->replaceVariables( $text ); # This works almost by chance, as the replaceVariables are done before the getUserSig(), - # which may corrupt this parser instance via its wfMsgExt( parsemag ) call- + # which may corrupt this parser instance via its wfMessage()->text() call- # Signatures $sigText = $this->getUserSig( $user ); @@ -4373,13 +4432,12 @@ class Parser { ) ); # Context links: [[|name]] and [[name (context)|]] - global $wgLegalTitleChars; - $tc = "[$wgLegalTitleChars]"; + $tc = '[' . Title::legalChars() . ']'; $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii! $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:page (context)|]] $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]] - $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]] + $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]] $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" @@ -4583,7 +4641,7 @@ class Parser { } /** - * Create an HTML-style tag, e.g. <yourtag>special text</yourtag> + * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>" * The callback should have the following form: * function myParserHook( $text, $params, $parser, $frame ) { ... } * @@ -4601,13 +4659,15 @@ class Parser { * this interface, as it is not documented and injudicious use could smash * private variables.** * - * @param $tag Mixed: the tag to use, e.g. 'hook' for <hook> + * @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>" * @param $callback Mixed: the callback function (and object) to use for the tag - * @return The old value of the mTagHooks array associated with the hook + * @return Mixed|null The old value of the mTagHooks array associated with the hook */ public function setHook( $tag, $callback ) { $tag = strtolower( $tag ); - if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" ); + if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { + throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" ); + } $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; $this->mTagHooks[$tag] = $callback; if ( !in_array( $tag, $this->mStripList ) ) { @@ -4629,13 +4689,15 @@ class Parser { * @since 1.10 * @todo better document or deprecate this * - * @param $tag Mixed: the tag to use, e.g. 'hook' for <hook> + * @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>" * @param $callback Mixed: the callback function (and object) to use for the tag - * @return The old value of the mTagHooks array associated with the hook + * @return Mixed|null The old value of the mTagHooks array associated with the hook */ function setTransparentTagHook( $tag, $callback ) { $tag = strtolower( $tag ); - if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" ); + if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { + throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" ); + } $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null; $this->mTransparentTagHooks[$tag] = $callback; @@ -4691,7 +4753,7 @@ class Parser { * Please read the documentation in includes/parser/Preprocessor.php for more information * about the methods available in PPFrame and PPNode. * - * @return The old callback function for this name, if any + * @return string|callback The old callback function for this name, if any */ public function setFunctionHook( $id, $callback, $flags = 0 ) { global $wgContLang; @@ -4735,9 +4797,10 @@ class Parser { } /** - * Create a tag function, e.g. <test>some stuff</test>. + * Create a tag function, e.g. "<test>some stuff</test>". * Unlike tag hooks, tag functions are parsed at preprocessor level. * Unlike parser functions, their content is not preprocessed. + * @return null */ function setFunctionTagHook( $tag, $callback, $flags ) { $tag = strtolower( $tag ); @@ -4755,7 +4818,7 @@ class Parser { /** * @todo FIXME: Update documentation. makeLinkObj() is deprecated. - * Replace <!--LINK--> link placeholders with actual links, in the buffer + * Replace "<!--LINK-->" link placeholders with actual links, in the buffer * Placeholders created in Skin::makeLinkObj() * * @param $text string @@ -4768,7 +4831,7 @@ class Parser { } /** - * Replace <!--LINK--> link placeholders with plain text of links + * Replace "<!--LINK-->" link placeholders with plain text of links * (not HTML-formatted). * * @param $text String @@ -4845,30 +4908,41 @@ class Parser { $label = ''; $alt = ''; + $link = ''; if ( isset( $matches[3] ) ) { // look for an |alt= definition while trying not to break existing // captions with multiple pipes (|) in it, until a more sensible grammar // is defined for images in galleries $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) ); - $altmatches = StringUtils::explode('|', $matches[3]); + $parameterMatches = StringUtils::explode('|', $matches[3]); $magicWordAlt = MagicWord::get( 'img_alt' ); + $magicWordLink = MagicWord::get( 'img_link' ); - foreach ( $altmatches as $altmatch ) { - $match = $magicWordAlt->matchVariableStartToEnd( $altmatch ); - if ( $match ) { + foreach ( $parameterMatches as $parameterMatch ) { + if ( $match = $magicWordAlt->matchVariableStartToEnd( $parameterMatch ) ) { $alt = $this->stripAltText( $match, false ); } + elseif( $match = $magicWordLink->matchVariableStartToEnd( $parameterMatch ) ){ + $link = strip_tags($this->replaceLinkHoldersText($match)); + $chars = self::EXT_LINK_URL_CLASS; + $prots = $this->mUrlProtocols; + //check to see if link matches an absolute url, if not then it must be a wiki link. + if(!preg_match( "/^($prots)$chars+$/u", $link)){ + $localLinkTitle = Title::newFromText($link); + $link = $localLinkTitle->getLocalURL(); + } + } else { // concatenate all other pipes - $label .= '|' . $altmatch; + $label .= '|' . $parameterMatch; } } // remove the first pipe $label = substr( $label, 1 ); } - $ig->add( $title, $label, $alt ); + $ig->add( $title, $label, $alt ,$link); } return $ig->toHTML(); } @@ -4890,7 +4964,7 @@ class Parser { 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom' ), 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless', - 'upright', 'border', 'link', 'alt' ), + 'upright', 'border', 'link', 'alt', 'class' ), ); static $internalParamMap; if ( !$internalParamMap ) { @@ -4922,7 +4996,7 @@ class Parser { * * @param $title Title * @param $options String - * @param $holders LinkHolderArray|false + * @param $holders LinkHolderArray|bool * @return string HTML */ function makeImage( $title, $options, $holders = false ) { @@ -4940,6 +5014,7 @@ class Parser { # * upright reduce width for upright images, rounded to full __0 px # * border draw a 1px border around the image # * alt Text for HTML alt attribute (defaults to empty) + # * class Set a class for img node # * link Set the target of the image link. Can be external, interwiki, or local # vertical-align values (no % or length right now): # * baseline @@ -4983,27 +5058,22 @@ class Parser { # Special case; width and height come in one variable together if ( $type === 'handler' && $paramName === 'width' ) { - $m = array(); - # (bug 13500) In both cases (width/height and width only), - # permit trailing "px" for backward compatibility. - if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) { - $width = intval( $m[1] ); - $height = intval( $m[2] ); + $parsedWidthParam = $this->parseWidthParam( $value ); + if( isset( $parsedWidthParam['width'] ) ) { + $width = $parsedWidthParam['width']; if ( $handler->validateParam( 'width', $width ) ) { $params[$type]['width'] = $width; $validated = true; } + } + if( isset( $parsedWidthParam['height'] ) ) { + $height = $parsedWidthParam['height']; if ( $handler->validateParam( 'height', $height ) ) { $params[$type]['height'] = $height; $validated = true; } - } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) { - $width = intval( $value ); - if ( $handler->validateParam( 'width', $width ) ) { - $params[$type]['width'] = $width; - $validated = true; - } - } # else no validation -- bug 13436 + } + # else no validation -- bug 13436 } else { if ( $type === 'handler' ) { # Validate handler parameter @@ -5013,6 +5083,7 @@ class Parser { switch( $paramName ) { case 'manualthumb': case 'alt': + case 'class': # @todo FIXME: Possibly check validity here for # manualthumb? downstream behavior seems odd with # missing manual thumbs. @@ -5026,8 +5097,8 @@ class Parser { $paramName = 'no-link'; $value = true; $validated = true; - } elseif ( preg_match( "/^$prots/", $value ) ) { - if ( preg_match( "/^($prots)$chars+$/u", $value, $m ) ) { + } elseif ( preg_match( "/^(?i)$prots/", $value ) ) { + if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) { $paramName = 'link-url'; $this->mOutput->addExternalLink( $value ); if ( $this->mOptions->getExternalLinkTarget() ) { @@ -5116,11 +5187,11 @@ class Parser { $params['frame']['title'] = $this->stripAltText( $caption, $holders ); } - wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) ); + wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params, $this ) ); # Linker does the rest $time = isset( $options['time'] ) ? $options['time'] : false; - $ret = Linker::makeImageLink2( $title, $file, $params['frame'], $params['handler'], + $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'], $time, $descQuery, $this->mOptions->getThumbSize() ); # Give the handler a chance to modify the parser object @@ -5229,13 +5300,13 @@ class Parser { * * @param $text String: Page wikitext * @param $section String: a section identifier string of the form: - * <flag1> - <flag2> - ... - <section number> + * "<flag1> - <flag2> - ... - <section number>" * * Currently the only recognised flag is "T", which means the target section number * was derived during a template inclusion parse, in other words this is a template * section edit link. If no flags are given, it was an ordinary section edit link. * This flag is required to avoid a section numbering mismatch when a section is - * enclosed by <includeonly> (bug 6563). + * enclosed by "<includeonly>" (bug 6563). * * The section number 0 pulls the text before the first heading; other numbers will * pull the given section along with its lower-level subsections. If the section is @@ -5381,7 +5452,7 @@ class Parser { * section does not exist, $oldtext is returned unchanged. * * @param $oldtext String: former text of the article - * @param $section Numeric: section identifier + * @param $section int section identifier * @param $text String: replacing text * @return String: modified text */ @@ -5464,7 +5535,7 @@ class Parser { /** * Mutator for $mDefaultSort * - * @param $sort New value + * @param $sort string New value */ public function setDefaultSort( $sort ) { $this->mDefaultSort = $sort; @@ -5542,7 +5613,7 @@ class Parser { * * @param $text String: text string to be stripped of wikitext * for use in a Section anchor - * @return Filtered text string + * @return string Filtered text string */ public function stripSectionName( $text ) { # Strip internal link markup @@ -5553,7 +5624,7 @@ class Parser { # @todo FIXME: Not tolerant to blank link text # I.E. [http://www.mediawiki.org] will render as [1] or something depending # on how many empty links there are on the page - need to figure that out. - $text = preg_replace( '/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text ); + $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text ); # Parse wikitext quotes (italics & bold) $text = $this->doQuotes( $text ); @@ -5691,7 +5762,7 @@ class Parser { * If the $data array has been stored persistently, the caller should first * check whether it is still valid, by calling isValidHalfParsedText(). * - * @param $data Serialized data + * @param $data array Serialized data * @return String */ function unserializeHalfParsedText( $data ) { @@ -5722,4 +5793,32 @@ class Parser { function isValidHalfParsedText( $data ) { return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION; } + + /** + * Parsed a width param of imagelink like 300px or 200x300px + * + * @param $value String + * + * @return array + * @since 1.20 + */ + public function parseWidthParam( $value ) { + $parsedWidthParam = array(); + if( $value === '' ) { + return $parsedWidthParam; + } + $m = array(); + # (bug 13500) In both cases (width/height and width only), + # permit trailing "px" for backward compatibility. + if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) { + $width = intval( $m[1] ); + $height = intval( $m[2] ); + $parsedWidthParam['width'] = $width; + $parsedWidthParam['height'] = $height; + } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) { + $width = intval( $value ); + $parsedWidthParam['width'] = $width; + } + return $parsedWidthParam; + } } diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index 8b043290..6a4ef0c5 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -2,7 +2,23 @@ /** * Cache for outputs of the PHP parser * + * 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 Parser */ /** @@ -77,6 +93,7 @@ class ParserCache { * * @param $article Article * @param $popts ParserOptions + * @return string */ function getETag( $article, $popts ) { return 'W/"' . $this->getParserOutputKey( $article, @@ -88,7 +105,7 @@ class ParserCache { * Retrieve the ParserOutput from ParserCache, even if it's outdated. * @param $article Article * @param $popts ParserOptions - * @return ParserOutput|false + * @return ParserOutput|bool False on failure */ public function getDirty( $article, $popts ) { $value = $this->get( $article, $popts, true ); @@ -102,8 +119,10 @@ class ParserCache { * * @todo Document parameter $useOutdated * - * @param $article Article - * @param $popts ParserOptions + * @param $article Article + * @param $popts ParserOptions + * @param $useOutdated Boolean (default true) + * @return bool|mixed|string */ public function getKey( $article, $popts, $useOutdated = true ) { global $wgCacheEpoch; @@ -139,11 +158,11 @@ class ParserCache { * Retrieve the ParserOutput from ParserCache. * false if not found or outdated. * - * @param $article Article - * @param $popts ParserOptions - * @param $useOutdated + * @param $article Article + * @param $popts ParserOptions + * @param $useOutdated Boolean (default false) * - * @return ParserOutput|false + * @return ParserOutput|bool False on failure */ public function get( $article, $popts, $useOutdated = false ) { global $wgCacheEpoch; diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index 57d3a7eb..009b18a1 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -1,6 +1,21 @@ <?php /** - * \brief Options for the PHP parser + * Options for the PHP parser + * + * 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 Parser @@ -79,6 +94,11 @@ class ParserOptions { * Maximum number of nodes touched by PPFrame::expand() */ var $mMaxPPNodeCount; + + /** + * Maximum number of nodes generated by Preprocessor::preprocessToObj() + */ + var $mMaxGeneratedPPNodeCount; /** * Maximum recursion depth in PPFrame::expand() @@ -91,6 +111,11 @@ class ParserOptions { var $mMaxTemplateDepth; /** + * Maximum number of calls per parse to expensive parser functions + */ + var $mExpensiveParserFunctionLimit; + + /** * Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS */ var $mRemoveComments = true; @@ -130,6 +155,16 @@ class ParserOptions { var $mPreSaveTransform = true; /** + * Whether content conversion should be disabled + */ + var $mDisableContentConversion; + + /** + * Whether title conversion should be disabled + */ + var $mDisableTitleConversion; + + /** * Automatically number headings? */ var $mNumberHeadings; @@ -199,13 +234,18 @@ class ParserOptions { function getTargetLanguage() { return $this->mTargetLanguage; } function getMaxIncludeSize() { return $this->mMaxIncludeSize; } function getMaxPPNodeCount() { return $this->mMaxPPNodeCount; } + function getMaxGeneratedPPNodeCount() { return $this->mMaxGeneratedPPNodeCount; } function getMaxPPExpandDepth() { return $this->mMaxPPExpandDepth; } function getMaxTemplateDepth() { return $this->mMaxTemplateDepth; } + /* @since 1.20 */ + function getExpensiveParserFunctionLimit() { return $this->mExpensiveParserFunctionLimit; } function getRemoveComments() { return $this->mRemoveComments; } function getTemplateCallback() { return $this->mTemplateCallback; } function getEnableLimitReport() { return $this->mEnableLimitReport; } function getCleanSignatures() { return $this->mCleanSignatures; } function getExternalLinkTarget() { return $this->mExternalLinkTarget; } + function getDisableContentConversion() { return $this->mDisableContentConversion; } + function getDisableTitleConversion() { return $this->mDisableTitleConversion; } function getMath() { $this->optionUsed( 'math' ); return $this->mMath; } function getThumbSize() { $this->optionUsed( 'thumbsize' ); @@ -285,13 +325,18 @@ class ParserOptions { function setTargetLanguage( $x ) { return wfSetVar( $this->mTargetLanguage, $x, true ); } function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); } function setMaxPPNodeCount( $x ) { return wfSetVar( $this->mMaxPPNodeCount, $x ); } + function setMaxGeneratedPPNodeCount( $x ) { return wfSetVar( $this->mMaxGeneratedPPNodeCount, $x ); } function setMaxTemplateDepth( $x ) { return wfSetVar( $this->mMaxTemplateDepth, $x ); } + /* @since 1.20 */ + function setExpensiveParserFunctionLimit( $x ) { return wfSetVar( $this->mExpensiveParserFunctionLimit, $x ); } function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); } function setTemplateCallback( $x ) { return wfSetVar( $this->mTemplateCallback, $x ); } function enableLimitReport( $x = true ) { return wfSetVar( $this->mEnableLimitReport, $x ); } function setTimestamp( $x ) { return wfSetVar( $this->mTimestamp, $x ); } function setCleanSignatures( $x ) { return wfSetVar( $this->mCleanSignatures, $x ); } function setExternalLinkTarget( $x ) { return wfSetVar( $this->mExternalLinkTarget, $x ); } + function disableContentConversion( $x = true ) { return wfSetVar( $this->mDisableContentConversion, $x ); } + function disableTitleConversion( $x = true ) { return wfSetVar( $this->mDisableTitleConversion, $x ); } function setMath( $x ) { return wfSetVar( $this->mMath, $x ); } function setUserLang( $x ) { if ( is_string( $x ) ) { @@ -380,7 +425,8 @@ class ParserOptions { global $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages, $wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion, $wgMaxArticleSize, $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth, - $wgCleanSignatures, $wgExternalLinkTarget; + $wgCleanSignatures, $wgExternalLinkTarget, $wgExpensiveParserFunctionLimit, + $wgMaxGeneratedPPNodeCount, $wgDisableLangConversion, $wgDisableTitleConversion; wfProfileIn( __METHOD__ ); @@ -392,10 +438,14 @@ class ParserOptions { $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion; $this->mMaxIncludeSize = $wgMaxArticleSize * 1024; $this->mMaxPPNodeCount = $wgMaxPPNodeCount; + $this->mMaxGeneratedPPNodeCount = $wgMaxGeneratedPPNodeCount; $this->mMaxPPExpandDepth = $wgMaxPPExpandDepth; $this->mMaxTemplateDepth = $wgMaxTemplateDepth; + $this->mExpensiveParserFunctionLimit = $wgExpensiveParserFunctionLimit; $this->mCleanSignatures = $wgCleanSignatures; $this->mExternalLinkTarget = $wgExternalLinkTarget; + $this->mDisableContentConversion = $wgDisableLangConversion; + $this->mDisableTitleConversion = $wgDisableLangConversion || $wgDisableTitleConversion; $this->mUser = $user; $this->mNumberHeadings = $user->getOption( 'numberheadings' ); @@ -428,6 +478,7 @@ class ParserOptions { * Returns the full array of options that would have been used by * in 1.16. * Used to get the old parser cache entries when available. + * @return array */ public static function legacyOptions() { global $wgUseDynamicDates; diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 2d99a3b5..41b4a385 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -1,118 +1,26 @@ <?php + /** - * Output of the PHP parser + * Output of the PHP parser. + * + * 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 Parser */ - -/** - * @todo document - * @ingroup Parser - */ - -class CacheTime { - var $mVersion = Parser::VERSION, # Compatibility check - $mCacheTime = '', # Time when this object was generated, or -1 for uncacheable. Used in ParserCache. - $mCacheExpiry = null, # Seconds after which the object should expire, use 0 for uncachable. Used in ParserCache. - $mContainsOldMagic; # Boolean variable indicating if the input contained variables like {{CURRENTDAY}} - - function getCacheTime() { return $this->mCacheTime; } - - function containsOldMagic() { return $this->mContainsOldMagic; } - function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); } - - /** - * setCacheTime() sets the timestamp expressing when the page has been rendered. - * This doesn not control expiry, see updateCacheExpiry() for that! - * @param $t string - * @return string - */ - function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } - - /** - * Sets the number of seconds after which this object should expire. - * This value is used with the ParserCache. - * If called with a value greater than the value provided at any previous call, - * the new call has no effect. The value returned by getCacheExpiry is smaller - * or equal to the smallest number that was provided as an argument to - * updateCacheExpiry(). - * - * @param $seconds number - */ - function updateCacheExpiry( $seconds ) { - $seconds = (int)$seconds; - - if ( $this->mCacheExpiry === null || $this->mCacheExpiry > $seconds ) { - $this->mCacheExpiry = $seconds; - } - - // hack: set old-style marker for uncacheable entries. - if ( $this->mCacheExpiry !== null && $this->mCacheExpiry <= 0 ) { - $this->mCacheTime = -1; - } - } - - /** - * Returns the number of seconds after which this object should expire. - * This method is used by ParserCache to determine how long the ParserOutput can be cached. - * The timestamp of expiry can be calculated by adding getCacheExpiry() to getCacheTime(). - * The value returned by getCacheExpiry is smaller or equal to the smallest number - * that was provided to a call of updateCacheExpiry(), and smaller or equal to the - * value of $wgParserCacheExpireTime. - */ - function getCacheExpiry() { - global $wgParserCacheExpireTime; - - if ( $this->mCacheTime < 0 ) { - return 0; - } // old-style marker for "not cachable" - - $expire = $this->mCacheExpiry; - - if ( $expire === null ) { - $expire = $wgParserCacheExpireTime; - } else { - $expire = min( $expire, $wgParserCacheExpireTime ); - } - - if( $this->containsOldMagic() ) { //compatibility hack - $expire = min( $expire, 3600 ); # 1 hour - } - - if ( $expire <= 0 ) { - return 0; // not cachable - } else { - return $expire; - } - } - - /** - * @return bool - */ - function isCacheable() { - return $this->getCacheExpiry() > 0; - } - - /** - * Return true if this cached output object predates the global or - * per-article cache invalidation timestamps, or if it comes from - * an incompatible older version. - * - * @param $touched String: the affected article's last touched timestamp - * @return Boolean - */ - public function expired( $touched ) { - global $wgCacheEpoch; - return !$this->isCacheable() || // parser says it's uncacheable - $this->getCacheTime() < $touched || - $this->getCacheTime() <= $wgCacheEpoch || - $this->getCacheTime() < wfTimestamp( TS_MW, time() - $this->getCacheExpiry() ) || // expiry period has passed - !isset( $this->mVersion ) || - version_compare( $this->mVersion, Parser::VERSION, "lt" ); - } -} - class ParserOutput extends CacheTime { var $mText, # The output text $mLanguageLinks, # List of the full text of language links, in the order they appear @@ -140,8 +48,9 @@ class ParserOutput extends CacheTime { $mProperties = array(), # Name/value pairs to be cached in the DB $mTOCHTML = '', # HTML of the TOC $mTimestamp; # Timestamp of the revision - private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. - private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys) + private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. + private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys) + private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place. const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#'; @@ -166,6 +75,7 @@ class ParserOutput extends CacheTime { /** * callback used by getText to replace editsection tokens * @private + * @return mixed */ function replaceEditSectionLinksCallback( $m ) { global $wgOut, $wgLang; @@ -331,7 +241,7 @@ class ParserOutput extends CacheTime { } /** - * Add some text to the <head>. + * Add some text to the "<head>". * If $tag is set, the section with that tag will only be included once * in a given page. */ @@ -447,4 +357,45 @@ class ParserOutput extends CacheTime { function recordOption( $option ) { $this->mAccessedOptions[$option] = true; } + + /** + * Adds an update job to the output. Any update jobs added to the output will eventually bexecuted in order to + * store any secondary information extracted from the page's content. + * + * @since 1.20 + * + * @param DataUpdate $update + */ + public function addSecondaryDataUpdate( DataUpdate $update ) { + $this->mSecondaryDataUpdates[] = $update; + } + + /** + * Returns any DataUpdate jobs to be executed in order to store secondary information + * extracted from the page's content, including a LinksUpdate object for all links stored in + * this ParserOutput object. + * + * @since 1.20 + * + * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText() + * @param $recursive Boolean: queue jobs for recursive updates? + * + * @return Array. An array of instances of DataUpdate + */ + public function getSecondaryDataUpdates( Title $title = null, $recursive = true ) { + if ( is_null( $title ) ) { + $title = Title::newFromText( $this->getTitleText() ); + } + + $linksUpdate = new LinksUpdate( $title, $this, $recursive ); + + if ( $this->mSecondaryDataUpdates === array() ) { + return array( $linksUpdate ); + } else { + $updates = array_merge( $this->mSecondaryDataUpdates, array( $linksUpdate ) ); + } + + return $updates; + } + } diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php index efad33f9..f25340fa 100644 --- a/includes/parser/Parser_DiffTest.php +++ b/includes/parser/Parser_DiffTest.php @@ -2,7 +2,23 @@ /** * Fake parser that output the difference of two different parsers * + * 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 Parser */ /** diff --git a/includes/parser/Parser_LinkHooks.php b/includes/parser/Parser_LinkHooks.php index 90e44943..6bcc324d 100644 --- a/includes/parser/Parser_LinkHooks.php +++ b/includes/parser/Parser_LinkHooks.php @@ -2,7 +2,23 @@ /** * Modified version of the PHP parser with hooks for wiki links; experimental * + * 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 Parser */ /** @@ -84,7 +100,7 @@ class Parser_LinkHooks extends Parser { * @param $flags Integer: a combination of the following flags: * SLH_PATTERN Use a regex link pattern rather than a namespace * - * @return The old callback function for this name, if any + * @return callback|null The old callback function for this name, if any */ public function setLinkHook( $ns, $callback, $flags = 0 ) { if( $flags & SLH_PATTERN && !is_string($ns) ) @@ -210,7 +226,7 @@ class Parser_LinkHooks extends Parser { # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these # should be external links. - if( preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $titleText) ) { + if( preg_match('/^\b(?i:' . wfUrlProtocols() . ')/', $titleText) ) { wfProfileOut( __METHOD__ ); return $wt; } diff --git a/includes/parser/Preprocessor.php b/includes/parser/Preprocessor.php index ae088fdb..bd13f9ae 100644 --- a/includes/parser/Preprocessor.php +++ b/includes/parser/Preprocessor.php @@ -2,7 +2,23 @@ /** * Interfaces for preprocessors * + * 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 Parser */ /** @@ -62,15 +78,19 @@ interface PPFrame { const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet + /** This constant exists when $indexOffset is supported in newChild() */ + const SUPPORTS_INDEX_OFFSET = 1; + /** * Create a child frame * * @param $args array * @param $title Title + * @param $indexOffset A number subtracted from the index attributes of the arguments * * @return PPFrame */ - function newChild( $args = false, $title = false ); + function newChild( $args = false, $title = false, $indexOffset = 0 ); /** * Expand a document tree node @@ -211,7 +231,7 @@ interface PPNode { function getName(); /** - * Split a <part> node into an associative array containing: + * Split a "<part>" node into an associative array containing: * name PPNode name * index String index * value PPNode value @@ -219,13 +239,13 @@ interface PPNode { function splitArg(); /** - * Split an <ext> node into an associative array containing name, attr, inner and close + * Split an "<ext>" node into an associative array containing name, attr, inner and close * All values in the resulting array are PPNodes. Inner and close are optional. */ function splitExt(); /** - * Split an <h> node + * Split an "<h>" node */ function splitHeading(); } diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php index 066589f6..34de0ba5 100644 --- a/includes/parser/Preprocessor_DOM.php +++ b/includes/parser/Preprocessor_DOM.php @@ -2,6 +2,21 @@ /** * Preprocessor using PHP's dom extension * + * 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 Parser */ @@ -41,7 +56,7 @@ class Preprocessor_DOM implements Preprocessor { } /** - * @param $args + * @param $args array * @return PPCustomFrame_DOM */ function newCustomFrame( $args ) { @@ -97,7 +112,7 @@ class Preprocessor_DOM implements Preprocessor { * * @param $text String: the text to parse * @param $flags Integer: bitwise combination of: - * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being + * Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" as if the text is being * included. Default is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. @@ -147,6 +162,15 @@ class Preprocessor_DOM implements Preprocessor { } } + + // Fail if the number of elements exceeds acceptable limits + // Do not attempt to generate the DOM + $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' ); + $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount(); + if ( $this->parser->mGeneratedPPNodeCount > $max ) { + throw new MWException( __METHOD__.': generated node count limit exceeded' ); + } + wfProfileIn( __METHOD__.'-loadXML' ); $dom = new DOMDocument; wfSuppressWarnings(); @@ -220,6 +244,7 @@ class Preprocessor_DOM implements Preprocessor { $searchBase = "[{<\n"; #} $revText = strrev( $text ); // For fast reverse searches + $lengthText = strlen( $text ); $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start $accum =& $stack->getAccum(); # Current accumulator @@ -275,7 +300,7 @@ class Preprocessor_DOM implements Preprocessor { $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) ); $i += $literalLength; } - if ( $i >= strlen( $text ) ) { + if ( $i >= $lengthText ) { if ( $currentClosing == "\n" ) { // Do a past-the-end run to finish off the heading $curChar = ''; @@ -339,10 +364,10 @@ class Preprocessor_DOM implements Preprocessor { // Unclosed comment in input, runs to end $inner = substr( $text, $i ); $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; - $i = strlen( $text ); + $i = $lengthText; } else { // Search backwards for leading whitespace - $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0; + $wsStart = $i ? ( $i - strspn( $revText, ' ', $lengthText - $i ) ) : 0; // Search forwards for trailing whitespace // $wsEnd will be the position of the last space (or the '>' if there's none) $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 ); @@ -423,7 +448,7 @@ class Preprocessor_DOM implements Preprocessor { } else { // No end tag -- let it run out to the end of the text. $inner = substr( $text, $tagEndPos + 1 ); - $i = strlen( $text ); + $i = $lengthText; $close = ''; } } @@ -479,20 +504,20 @@ class Preprocessor_DOM implements Preprocessor { } elseif ( $found == 'line-end' ) { $piece = $stack->top; // A heading must be open, otherwise \n wouldn't have been in the search list - assert( $piece->open == "\n" ); + assert( '$piece->open == "\n"' ); $part = $piece->getCurrentPart(); // Search back through the input to see if it has a proper close // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient - $wsLength = strspn( $revText, " \t", strlen( $text ) - $i ); + $wsLength = strspn( $revText, " \t", $lengthText - $i ); $searchStart = $i - $wsLength; if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { // Comment found at line end // Search for equals signs before the comment $searchStart = $part->visualEnd; - $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart ); + $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); } $count = $piece->count; - $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart ); + $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); if ( $equalsLength > 0 ) { if ( $searchStart - $equalsLength == $piece->startPos ) { // This is just a single string of equals signs on its own line @@ -911,7 +936,7 @@ class PPFrame_DOM implements PPFrame { * * @return PPTemplateFrame_DOM */ - function newChild( $args = false, $title = false ) { + function newChild( $args = false, $title = false, $indexOffset = 0 ) { $namedArgs = array(); $numberedArgs = array(); if ( $title === false ) { @@ -923,6 +948,9 @@ class PPFrame_DOM implements PPFrame { $args = $args->node; } foreach ( $args as $arg ) { + if ( $arg instanceof PPNode ) { + $arg = $arg->node; + } if ( !$xpath ) { $xpath = new DOMXPath( $arg->ownerDocument ); } @@ -932,6 +960,7 @@ class PPFrame_DOM implements PPFrame { if ( $nameNodes->item( 0 )->hasAttributes() ) { // Numbered parameter $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent; + $index = $index - $indexOffset; $numberedArgs[$index] = $value->item( 0 ); unset( $namedArgs[$index] ); } else { @@ -958,14 +987,25 @@ class PPFrame_DOM implements PPFrame { } if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { + $this->parser->limitationWarn( 'node-count-exceeded', + $this->parser->mPPNodeCount, + $this->parser->mOptions->getMaxPPNodeCount() + ); return '<span class="error">Node-count limit exceeded</span>'; } if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { + $this->parser->limitationWarn( 'expansion-depth-exceeded', + $expansionDepth, + $this->parser->mOptions->getMaxPPExpandDepth() + ); return '<span class="error">Expansion depth limit exceeded</span>'; } wfProfileIn( __METHOD__ ); ++$expansionDepth; + if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { + $this->parser->mHighestExpansionDepth = $expansionDepth; + } if ( $root instanceof PPNode_DOM ) { $root = $root->node; @@ -1250,6 +1290,7 @@ class PPFrame_DOM implements PPFrame { /** * Virtual implode with brackets + * @return array */ function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { $args = array_slice( func_get_args(), 3 ); @@ -1522,6 +1563,10 @@ class PPCustomFrame_DOM extends PPFrame_DOM { } return $this->args[$index]; } + + function getArguments() { + return $this->args; + } } /** @@ -1623,10 +1668,10 @@ class PPNode_DOM implements PPNode { } /** - * Split a <part> node into an associative array containing: - * name PPNode name - * index String index - * value PPNode value + * Split a "<part>" node into an associative array containing: + * - name PPNode name + * - index String index + * - value PPNode value * * @return array */ @@ -1646,7 +1691,7 @@ class PPNode_DOM implements PPNode { } /** - * Split an <ext> node into an associative array containing name, attr, inner and close + * Split an "<ext>" node into an associative array containing name, attr, inner and close * All values in the resulting array are PPNodes. Inner and close are optional. * * @return array @@ -1673,7 +1718,8 @@ class PPNode_DOM implements PPNode { } /** - * Split a <h> node + * Split a "<h>" node + * @return array */ function splitHeading() { if ( $this->getName() !== 'h' ) { diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php index 2934181a..4f04c865 100644 --- a/includes/parser/Preprocessor_Hash.php +++ b/includes/parser/Preprocessor_Hash.php @@ -2,6 +2,21 @@ /** * Preprocessor using PHP arrays * + * 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 Parser */ @@ -9,7 +24,7 @@ /** * Differences from DOM schema: * * attribute nodes are children - * * <h> nodes that aren't at the top are replaced with <possible-h> + * * "<h>" nodes that aren't at the top are replaced with <possible-h> * @ingroup Parser */ class Preprocessor_Hash implements Preprocessor { @@ -32,7 +47,7 @@ class Preprocessor_Hash implements Preprocessor { } /** - * @param $args + * @param $args array * @return PPCustomFrame_Hash */ function newCustomFrame( $args ) { @@ -76,7 +91,7 @@ class Preprocessor_Hash implements Preprocessor { * * @param $text String: the text to parse * @param $flags Integer: bitwise combination of: - * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being + * Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" as if the text is being * included. Default is to assume a direct page view. * * The generated DOM tree must depend only on the input text and the flags. @@ -162,6 +177,7 @@ class Preprocessor_Hash implements Preprocessor { $searchBase = "[{<\n"; $revText = strrev( $text ); // For fast reverse searches + $lengthText = strlen( $text ); $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start $accum =& $stack->getAccum(); # Current accumulator @@ -216,7 +232,7 @@ class Preprocessor_Hash implements Preprocessor { $accum->addLiteral( substr( $text, $i, $literalLength ) ); $i += $literalLength; } - if ( $i >= strlen( $text ) ) { + if ( $i >= $lengthText ) { if ( $currentClosing == "\n" ) { // Do a past-the-end run to finish off the heading $curChar = ''; @@ -280,10 +296,10 @@ class Preprocessor_Hash implements Preprocessor { // Unclosed comment in input, runs to end $inner = substr( $text, $i ); $accum->addNodeWithText( 'comment', $inner ); - $i = strlen( $text ); + $i = $lengthText; } else { // Search backwards for leading whitespace - $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0; + $wsStart = $i ? ( $i - strspn( $revText, ' ', $lengthText - $i ) ) : 0; // Search forwards for trailing whitespace // $wsEnd will be the position of the last space (or the '>' if there's none) $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 ); @@ -368,7 +384,7 @@ class Preprocessor_Hash implements Preprocessor { } else { // No end tag -- let it run out to the end of the text. $inner = substr( $text, $tagEndPos + 1 ); - $i = strlen( $text ); + $i = $lengthText; $close = null; } } @@ -428,20 +444,20 @@ class Preprocessor_Hash implements Preprocessor { } elseif ( $found == 'line-end' ) { $piece = $stack->top; // A heading must be open, otherwise \n wouldn't have been in the search list - assert( $piece->open == "\n" ); + assert( '$piece->open == "\n"' ); $part = $piece->getCurrentPart(); // Search back through the input to see if it has a proper close // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient - $wsLength = strspn( $revText, " \t", strlen( $text ) - $i ); + $wsLength = strspn( $revText, " \t", $lengthText - $i ); $searchStart = $i - $wsLength; if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { // Comment found at line end // Search for equals signs before the comment $searchStart = $part->visualEnd; - $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart ); + $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); } $count = $piece->count; - $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart ); + $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); if ( $equalsLength > 0 ) { if ( $searchStart - $equalsLength == $piece->startPos ) { // This is just a single string of equals signs on its own line @@ -869,11 +885,11 @@ class PPFrame_Hash implements PPFrame { * $args is optionally a multi-root PPNode or array containing the template arguments * * @param $args PPNode_Hash_Array|array - * @param $title Title|false + * @param $title Title|bool * * @return PPTemplateFrame_Hash */ - function newChild( $args = false, $title = false ) { + function newChild( $args = false, $title = false, $indexOffset = 0 ) { $namedArgs = array(); $numberedArgs = array(); if ( $title === false ) { @@ -889,8 +905,9 @@ class PPFrame_Hash implements PPFrame { $bits = $arg->splitArg(); if ( $bits['index'] !== '' ) { // Numbered parameter - $numberedArgs[$bits['index']] = $bits['value']; - unset( $namedArgs[$bits['index']] ); + $index = $bits['index'] - $indexOffset; + $numberedArgs[$index] = $bits['value']; + unset( $namedArgs[$index] ); } else { // Named parameter $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); @@ -915,12 +932,23 @@ class PPFrame_Hash implements PPFrame { } if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { + $this->parser->limitationWarn( 'node-count-exceeded', + $this->parser->mPPNodeCount, + $this->parser->mOptions->getMaxPPNodeCount() + ); return '<span class="error">Node-count limit exceeded</span>'; } if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { + $this->parser->limitationWarn( 'expansion-depth-exceeded', + $expansionDepth, + $this->parser->mOptions->getMaxPPExpandDepth() + ); return '<span class="error">Expansion depth limit exceeded</span>'; } ++$expansionDepth; + if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { + $this->parser->mHighestExpansionDepth = $expansionDepth; + } $outStack = array( '', '' ); $iteratorStack = array( false, $root ); @@ -1470,6 +1498,10 @@ class PPCustomFrame_Hash extends PPFrame_Hash { } return $this->args[$index]; } + + function getArguments() { + return $this->args; + } } /** @@ -1543,7 +1575,7 @@ class PPNode_Hash_Tree implements PPNode { $children = array(); for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { if ( isset( $child->name ) && $child->name === $name ) { - $children[] = $name; + $children[] = $child; } } return $children; @@ -1572,10 +1604,10 @@ class PPNode_Hash_Tree implements PPNode { } /** - * Split a <part> node into an associative array containing: - * name PPNode name - * index String index - * value PPNode value + * Split a "<part>" node into an associative array containing: + * - name PPNode name + * - index String index + * - value PPNode value * * @return array */ @@ -1607,7 +1639,7 @@ class PPNode_Hash_Tree implements PPNode { } /** - * Split an <ext> node into an associative array containing name, attr, inner and close + * Split an "<ext>" node into an associative array containing name, attr, inner and close * All values in the resulting array are PPNodes. Inner and close are optional. * * @return array @@ -1635,7 +1667,7 @@ class PPNode_Hash_Tree implements PPNode { } /** - * Split an <h> node + * Split an "<h>" node * * @return array */ @@ -1661,7 +1693,7 @@ class PPNode_Hash_Tree implements PPNode { } /** - * Split a <template> or <tplarg> node + * Split a "<template>" or "<tplarg>" node * * @return array */ diff --git a/includes/parser/Preprocessor_HipHop.hphp b/includes/parser/Preprocessor_HipHop.hphp index f5af0154..8b71a1b5 100644 --- a/includes/parser/Preprocessor_HipHop.hphp +++ b/includes/parser/Preprocessor_HipHop.hphp @@ -3,6 +3,21 @@ * A preprocessor optimised for HipHop, using HipHop-specific syntax. * vim: ft=php * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Parser */ @@ -18,6 +33,9 @@ class Preprocessor_HipHop implements Preprocessor { const CACHE_VERSION = 1; + /** + * @param $parser Parser + */ function __construct( $parser ) { $this->parser = $parser; } @@ -30,10 +48,10 @@ class Preprocessor_HipHop implements Preprocessor { } /** - * @param $args + * @param $args array * @return PPCustomFrame_HipHop */ - function newCustomFrame( array $args ) { + function newCustomFrame( $args ) { return new PPCustomFrame_HipHop( $this, $args ); } @@ -88,15 +106,18 @@ class Preprocessor_HipHop implements Preprocessor { * cache may be implemented at a later date which takes further advantage of these strict * dependency requirements. * + * @throws MWException * @return PPNode_HipHop_Tree */ - function preprocessToObj( string $text, int $flags = 0 ) { + function preprocessToObj( $text, $flags = 0 ) { wfProfileIn( __METHOD__ ); // Check cache. global $wgMemc, $wgPreprocessorCacheThreshold; - $cacheable = ($wgPreprocessorCacheThreshold !== false && strlen( $text ) > $wgPreprocessorCacheThreshold); + $lengthText = strlen( $text ); + + $cacheable = ($wgPreprocessorCacheThreshold !== false && $lengthText > $wgPreprocessorCacheThreshold); if ( $cacheable ) { wfProfileIn( __METHOD__.'-cacheable' ); @@ -220,7 +241,7 @@ class Preprocessor_HipHop implements Preprocessor { $accum->addLiteral( strval( substr( $text, $i, $literalLength ) ) ); $i += $literalLength; } - if ( $i >= strlen( $text ) ) { + if ( $i >= $lengthText ) { if ( $currentClosing === "\n" ) { // Do a past-the-end run to finish off the heading $curChar = ''; @@ -286,12 +307,12 @@ class Preprocessor_HipHop implements Preprocessor { // Unclosed comment in input, runs to end $inner = strval( substr( $text, $i ) ); $accum->addNodeWithText( 'comment', $inner ); - $i = strlen( $text ); + $i = $lengthText; } else { $endPos = intval( $variantEndPos ); // Search backwards for leading whitespace if ( $i ) { - $wsStart = $i - intval( strspn( $revText, ' ', strlen( $text ) - $i ) ); + $wsStart = $i - intval( strspn( $revText, ' ', $lengthText - $i ) ); } else { $wsStart = 0; } @@ -384,7 +405,7 @@ class Preprocessor_HipHop implements Preprocessor { } else { // No end tag -- let it run out to the end of the text. $inner = strval( substr( $text, $tagEndPos + 1 ) ); - $i = strlen( $text ); + $i = $lengthText; $haveClose = false; } } @@ -444,20 +465,21 @@ class Preprocessor_HipHop implements Preprocessor { } elseif ( $found === 'line-end' ) { $piece = $stack->getTop(); // A heading must be open, otherwise \n wouldn't have been in the search list - assert( $piece->open === "\n" ); + assert( $piece->open === "\n" ); // Passing the assert condition directly instead of string, as + // HPHP /compiler/ chokes on strings when ASSERT_ACTIVE != 0. $part = $piece->getCurrentPart(); // Search back through the input to see if it has a proper close // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient - $wsLength = intval( strspn( $revText, " \t", strlen( $text ) - $i ) ); + $wsLength = intval( strspn( $revText, " \t", $lengthText - $i ) ); $searchStart = $i - $wsLength; if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { // Comment found at line end // Search for equals signs before the comment $searchStart = intval( $part->visualEnd ); - $searchStart -= intval( strspn( $revText, " \t", strlen( $text ) - $searchStart ) ); + $searchStart -= intval( strspn( $revText, " \t", $lengthText - $searchStart ) ); } $count = intval( $piece->count ); - $equalsLength = intval( strspn( $revText, '=', strlen( $text ) - $searchStart ) ); + $equalsLength = intval( strspn( $revText, '=', $lengthText - $searchStart ) ); $isTreeNode = false; $resultAccum = $accum; if ( $equalsLength > 0 ) { @@ -814,16 +836,23 @@ class PPDStack_HipHop { * @ingroup Parser */ class PPDStackElement_HipHop { - var $open, // Opening character (\n for heading) - $close, // Matching closing character + var $open, // Opening character (\n for heading) + $close, // Matching closing character $count, // Number of opening characters found (number of "=" for heading) $parts, // Array of PPDPart objects describing pipe-separated parts. $lineStart; // True if the open char appeared at the start of the input line. Not set for headings. + /** + * @param $obj PPDStackElement_HipHop + * @return PPDStackElement_HipHop + */ static function cast( PPDStackElement_HipHop $obj ) { return $obj; } + /** + * @param $data array + */ function __construct( $data = array() ) { $this->parts = array( new PPDPart_HipHop ); @@ -832,14 +861,23 @@ class PPDStackElement_HipHop { } } + /** + * @return PPDAccum_HipHop + */ function getAccum() { return PPDAccum_HipHop::cast( $this->parts[count($this->parts) - 1]->out ); } + /** + * @param $s string + */ function addPart( $s = '' ) { $this->parts[] = new PPDPart_HipHop( $s ); } + /** + * @return PPDPart_HipHop + */ function getCurrentPart() { return PPDPart_HipHop::cast( $this->parts[count($this->parts) - 1] ); } @@ -860,6 +898,7 @@ class PPDStackElement_HipHop { /** * Get the accumulator that would result if the close is not found. * + * @param $openingCount bool * @return PPDAccum_HipHop */ function breakSyntax( $openingCount = false ) { @@ -1025,12 +1064,14 @@ class PPFrame_HipHop implements PPFrame { * Create a new child frame * $args is optionally a multi-root PPNode or array containing the template arguments * - * @param $args PPNode_HipHop_Array|array - * @param $title Title|false + * @param $args PPNode_HipHop_Array|array|bool + * @param $title Title|bool + * @param $indexOffset A number subtracted from the index attributes of the arguments * + * @throws MWException * @return PPTemplateFrame_HipHop */ - function newChild( $args = false, $title = false ) { + function newChild( $args = false, $title = false, $indexOffset = 0 ) { $namedArgs = array(); $numberedArgs = array(); if ( $title === false ) { @@ -1072,12 +1113,23 @@ class PPFrame_HipHop implements PPFrame { } if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { + $this->parser->limitationWarn( 'node-count-exceeded', + $this->parser->mPPNodeCount, + $this->parser->mOptions->getMaxPPNodeCount() + ); return '<span class="error">Node-count limit exceeded</span>'; } if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { + $this->parser->limitationWarn( 'expansion-depth-exceeded', + $expansionDepth, + $this->parser->mOptions->getMaxPPExpandDepth() + ); return '<span class="error">Expansion depth limit exceeded</span>'; } ++$expansionDepth; + if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { + $this->parser->mHighestExpansionDepth = $expansionDepth; + } $outStack = array( '', '' ); $iteratorStack = array( false, $root ); @@ -1266,6 +1318,7 @@ class PPFrame_HipHop implements PPFrame { /** * Implode with no flags specified * This previously called implodeWithFlags but has now been inlined to reduce stack depth + * @param $sep * @return string */ function implode( $sep /*, ... */ ) { @@ -1296,6 +1349,7 @@ class PPFrame_HipHop implements PPFrame { * Makes an object that, when expand()ed, will be the same as one obtained * with implode() * + * @param $sep * @return PPNode_HipHop_Array */ function virtualImplode( $sep /*, ... */ ) { @@ -1325,6 +1379,9 @@ class PPFrame_HipHop implements PPFrame { /** * Virtual implode with brackets * + * @param $start + * @param $sep + * @param $end * @return PPNode_HipHop_Array */ function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { @@ -1445,11 +1502,11 @@ class PPTemplateFrame_HipHop extends PPFrame_HipHop { var $numberedExpansionCache, $namedExpansionCache; /** - * @param $preprocessor - * @param $parent + * @param $preprocessor Preprocessor_HipHop + * @param $parent bool * @param $numberedArgs array * @param $namedArgs array - * @param $title Title + * @param $title Title|bool */ function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { parent::__construct( $preprocessor ); @@ -1696,11 +1753,15 @@ class PPNode_HipHop_Tree implements PPNode { return $this->nextSibling; } + /** + * @param $name string + * @return array + */ function getChildrenOfType( $name ) { $children = array(); for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { if ( isset( $child->name ) && $child->name === $name ) { - $children[] = $name; + $children[] = $child; } } return $children; @@ -1734,6 +1795,7 @@ class PPNode_HipHop_Tree implements PPNode { * index String index * value PPNode value * + * @throws MWException * @return array */ function splitArg() { @@ -1767,6 +1829,7 @@ class PPNode_HipHop_Tree implements PPNode { * Split an <ext> node into an associative array containing name, attr, inner and close * All values in the resulting array are PPNodes. Inner and close are optional. * + * @throws MWException * @return array */ function splitExt() { @@ -1794,6 +1857,7 @@ class PPNode_HipHop_Tree implements PPNode { /** * Split an <h> node * + * @throws MWException * @return array */ function splitHeading() { diff --git a/includes/parser/StripState.php b/includes/parser/StripState.php index 7ad80fa1..ad95d5f7 100644 --- a/includes/parser/StripState.php +++ b/includes/parser/StripState.php @@ -1,4 +1,25 @@ <?php +/** + * Holder for stripped items when parsing wiki markup. + * + * 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 Parser + */ /** * @todo document, briefly. @@ -10,6 +31,10 @@ class StripState { protected $regex; protected $tempType, $tempMergePrefix; + protected $circularRefGuard; + protected $recursionLevel = 0; + + const UNSTRIP_RECURSION_LIMIT = 20; /** * @param $prefix string @@ -21,6 +46,7 @@ class StripState { 'general' => array() ); $this->regex = "/{$this->prefix}([^\x7f]+)" . Parser::MARKER_SUFFIX . '/'; + $this->circularRefGuard = array(); } /** @@ -92,12 +118,10 @@ class StripState { } wfProfileIn( __METHOD__ ); + $oldType = $this->tempType; $this->tempType = $type; - do { - $oldText = $text; - $text = preg_replace_callback( $this->regex, array( $this, 'unstripCallback' ), $text ); - } while ( $text !== $oldText ); - $this->tempType = null; + $text = preg_replace_callback( $this->regex, array( $this, 'unstripCallback' ), $text ); + $this->tempType = $oldType; wfProfileOut( __METHOD__ ); return $text; } @@ -107,8 +131,25 @@ class StripState { * @return array */ protected function unstripCallback( $m ) { - if ( isset( $this->data[$this->tempType][$m[1]] ) ) { - return $this->data[$this->tempType][$m[1]]; + $marker = $m[1]; + if ( isset( $this->data[$this->tempType][$marker] ) ) { + if ( isset( $this->circularRefGuard[$marker] ) ) { + return '<span class="error">' + . wfMessage( 'parser-unstrip-loop-warning' )->inContentLanguage()->text() + . '</span>'; + } + if ( $this->recursionLevel >= self::UNSTRIP_RECURSION_LIMIT ) { + return '<span class="error">' . + wfMessage( 'parser-unstrip-recursion-limit' ) + ->numParams( self::UNSTRIP_RECURSION_LIMIT )->inContentLanguage()->text() . + '</span>'; + } + $this->circularRefGuard[$marker] = true; + $this->recursionLevel++; + $ret = $this->unstripType( $this->tempType, $this->data[$this->tempType][$marker] ); + $this->recursionLevel--; + unset( $this->circularRefGuard[$marker] ); + return $ret; } else { return $m[0]; } diff --git a/includes/parser/Tidy.php b/includes/parser/Tidy.php index 2b98f01d..ed2d436d 100644 --- a/includes/parser/Tidy.php +++ b/includes/parser/Tidy.php @@ -2,7 +2,23 @@ /** * HTML validation and correction * + * 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 Parser */ /** @@ -14,6 +30,8 @@ * * This re-uses some of the parser's UNIQ tricks, though some of it is private so it's * duplicated. Perhaps we should create an abstract marker hiding class. + * + * @ingroup Parser */ class MWTidyWrapper { @@ -143,7 +161,7 @@ class MWTidy { * * @param $text String: HTML to check * @param $stderr Boolean: Whether to read result from STDERR rather than STDOUT - * @param &$retval Exit code (-1 on internal error) + * @param &$retval int Exit code (-1 on internal error) * @return mixed String or null */ private static function execExternalTidy( $text, $stderr = false, &$retval = null ) { @@ -207,7 +225,7 @@ class MWTidy { * * @param $text String: HTML to check * @param $stderr Boolean: Whether to read result from error status instead of output - * @param &$retval Exit code (-1 on internal error) + * @param &$retval int Exit code (-1 on internal error) * @return mixed String or null */ private static function execInternalTidy( $text, $stderr = false, &$retval = null ) { diff --git a/includes/profiler/Profiler.php b/includes/profiler/Profiler.php index 0fe18c25..62be39e4 100644 --- a/includes/profiler/Profiler.php +++ b/includes/profiler/Profiler.php @@ -1,6 +1,21 @@ <?php /** - * @defgroup Profiler Profiler + * Base class and functions for profiling. + * + * 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 Profiler @@ -8,6 +23,10 @@ */ /** + * @defgroup Profiler Profiler + */ + +/** * Begin profiling of a function * @param $functionname String: name of the function we will profile */ @@ -48,14 +67,7 @@ class Profiler { $this->mProfileID = $params['profileID']; } - // Push an entry for the pre-profile setup time onto the stack - $initial = $this->getInitialTime(); - if ( $initial !== null ) { - $this->mWorkStack[] = array( '-total', 0, $initial, 0 ); - $this->mStack[] = array( '-setup', 1, $initial, 0, $this->getTime(), 0 ); - } else { - $this->profileIn( '-total' ); - } + $this->addInitialStack(); } /** @@ -102,6 +114,16 @@ class Profiler { return false; } + /** + * Return whether this profiler stores data + * + * @see Profiler::logData() + * @return Boolean + */ + public function isPersistent() { + return true; + } + public function setProfileID( $id ) { $this->mProfileID = $id; } @@ -115,6 +137,20 @@ class Profiler { } /** + * Add the inital item in the stack. + */ + protected function addInitialStack() { + // Push an entry for the pre-profile setup time onto the stack + $initial = $this->getInitialTime(); + if ( $initial !== null ) { + $this->mWorkStack[] = array( '-total', 0, $initial, 0 ); + $this->mStack[] = array( '-setup', 1, $initial, 0, $this->getTime(), 0 ); + } else { + $this->profileIn( '-total' ); + } + } + + /** * Called by wfProfieIn() * * @param $functionname String @@ -205,6 +241,7 @@ class Profiler { /** * Returns a tree of function call instead of a list of functions + * @return string */ function getCallTree() { return implode( '', array_map( array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack ) ) ); @@ -213,7 +250,8 @@ class Profiler { /** * Recursive function the format the current profiling array into a tree * - * @param $stack profiling array + * @param $stack array profiling array + * @return array */ function remapCallTree( $stack ) { if( count( $stack ) < 2 ){ @@ -252,6 +290,7 @@ class Profiler { /** * Callback to get a formatted line for the call tree + * @return string */ function getCallTreeLine( $entry ) { list( $fname, $level, $start, /* $x */, $end) = $entry; @@ -262,28 +301,69 @@ class Profiler { return sprintf( "%10s %s %s\n", trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname ); } - function getTime() { - if ( $this->mTimeMetric === 'user' ) { - return $this->getUserTime(); + /** + * Get the initial time of the request, based either on $wgRequestTime or + * $wgRUstart. Will return null if not able to find data. + * + * @param $metric string|false: metric to use, with the following possibilities: + * - user: User CPU time (without system calls) + * - cpu: Total CPU time (user and system calls) + * - wall (or any other string): elapsed time + * - false (default): will fall back to default metric + * @return float|null + */ + function getTime( $metric = false ) { + if ( $metric === false ) { + $metric = $this->mTimeMetric; + } + + if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) { + if ( !function_exists( 'getrusage' ) ) { + return 0; + } + $ru = getrusage(); + $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6; + if ( $metric === 'cpu' ) { + # This is the time of system calls, added to the user time + # it gives the total CPU time + $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6; + } + return $time; } else { return microtime( true ); } } - function getUserTime() { - $ru = getrusage(); - return $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6; - } - - private function getInitialTime() { + /** + * Get the initial time of the request, based either on $wgRequestTime or + * $wgRUstart. Will return null if not able to find data. + * + * @param $metric string|false: metric to use, with the following possibilities: + * - user: User CPU time (without system calls) + * - cpu: Total CPU time (user and system calls) + * - wall (or any other string): elapsed time + * - false (default): will fall back to default metric + * @return float|null + */ + protected function getInitialTime( $metric = false ) { global $wgRequestTime, $wgRUstart; - if ( $this->mTimeMetric === 'user' ) { - if ( count( $wgRUstart ) ) { - return $wgRUstart['ru_utime.tv_sec'] + $wgRUstart['ru_utime.tv_usec'] / 1e6; - } else { + if ( $metric === false ) { + $metric = $this->mTimeMetric; + } + + if ( $metric === 'cpu' || $this->mTimeMetric === 'user' ) { + if ( !count( $wgRUstart ) ) { return null; } + + $time = $wgRUstart['ru_utime.tv_sec'] + $wgRUstart['ru_utime.tv_usec'] / 1e6; + if ( $metric === 'cpu' ) { + # This is the time of system calls, added to the user time + # it gives the total CPU time + $time += $wgRUstart['ru_stime.tv_sec'] + $wgRUstart['ru_stime.tv_usec'] / 1e6; + } + return $time; } else { if ( empty( $wgRequestTime ) ) { return null; @@ -409,7 +489,7 @@ class Profiler { } wfProfileOut( '-overhead-total' ); } - + /** * Counts the number of profiled function calls sitting under * the given point in the call graph. Not the most efficient algo. @@ -479,7 +559,7 @@ class Profiler { $rc = $dbw->affectedRows(); if ( $rc == 0 ) { $dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount, - 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ), + 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ), __METHOD__, array ('IGNORE')); } // When we upgrade to mysql 4.1, the insert+update @@ -494,6 +574,7 @@ class Profiler { /** * Get the function name of the current profiling section + * @return */ function getCurrentSection() { $elt = end( $this->mWorkStack ); diff --git a/includes/profiler/ProfilerSimple.php b/includes/profiler/ProfilerSimple.php index 055a0ea0..d1d1c5d9 100644 --- a/includes/profiler/ProfilerSimple.php +++ b/includes/profiler/ProfilerSimple.php @@ -1,5 +1,22 @@ <?php /** + * Base class for simple profiling. + * + * 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 Profiler */ @@ -15,32 +32,24 @@ class ProfilerSimple extends Profiler { var $zeroEntry = array('cpu'=> 0.0, 'cpu_sq' => 0.0, 'real' => 0.0, 'real_sq' => 0.0, 'count' => 0); var $errorEntry; - function __construct( $params ) { - global $wgRequestTime, $wgRUstart; - parent::__construct( $params ); + public function isPersistent() { + /* Implement in output subclasses */ + return false; + } + protected function addInitialStack() { $this->errorEntry = $this->zeroEntry; $this->errorEntry['count'] = 1; - if (!empty($wgRequestTime) && !empty($wgRUstart)) { - # Remove the -total entry from parent::__construct - $this->mWorkStack = array(); - - $this->mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart)); - - $elapsedcpu = $this->getCpuTime() - $this->getCpuTime($wgRUstart); - $elapsedreal = microtime(true) - $wgRequestTime; + $initialTime = $this->getInitialTime(); + $initialCpu = $this->getInitialTime( 'cpu' ); + if ( $initialTime !== null && $initialCpu !== null ) { + $this->mWorkStack[] = array( '-total', 0, $initialTime, $initialCpu ); + $this->mWorkStack[] = array( '-setup', 1, $initialTime, $initialCpu ); - $entry =& $this->mCollated["-setup"]; - if (!is_array($entry)) { - $entry = $this->zeroEntry; - $this->mCollated["-setup"] =& $entry; - } - $entry['cpu'] += $elapsedcpu; - $entry['cpu_sq'] += $elapsedcpu*$elapsedcpu; - $entry['real'] += $elapsedreal; - $entry['real_sq'] += $elapsedreal*$elapsedreal; - $entry['count']++; + $this->profileOut( '-setup' ); + } else { + $this->profileIn( '-total' ); } } @@ -53,7 +62,7 @@ class ProfilerSimple extends Profiler { if ($wgDebugFunctionEntry) { $this->debug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n"); } - $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime()); + $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), $this->getTime( 'cpu' ) ); } function profileOut($functionname) { @@ -80,8 +89,8 @@ class ProfilerSimple extends Profiler { $this->mCollated[$message] = $this->errorEntry; } $entry =& $this->mCollated[$functionname]; - $elapsedcpu = $this->getCpuTime() - $octime; - $elapsedreal = microtime(true) - $ortime; + $elapsedcpu = $this->getTime( 'cpu' ) - $octime; + $elapsedreal = $this->getTime() - $ortime; if (!is_array($entry)) { $entry = $this->zeroEntry; $this->mCollated[$functionname] =& $entry; @@ -104,15 +113,20 @@ class ProfilerSimple extends Profiler { /* Implement in subclasses */ } - function getCpuTime($ru=null) { - if ( function_exists( 'getrusage' ) ) { - if ( $ru == null ) { - $ru = getrusage(); - } - return ($ru['ru_utime.tv_sec'] + $ru['ru_stime.tv_sec'] + ($ru['ru_utime.tv_usec'] + - $ru['ru_stime.tv_usec']) * 1e-6); + /** + * Get the actual CPU time or the initial one if $ru is set. + * + * @deprecated in 1.20 + * @return float|null + */ + function getCpuTime( $ru = null ) { + wfDeprecated( __METHOD__, '1.20' ); + + if ( $ru === null ) { + return $this->getTime( 'cpu' ); } else { - return 0; + # It theory we should use $ru here, but it always $wgRUstart that is passed here + return $this->getInitialTime( 'cpu' ); } } } diff --git a/includes/profiler/ProfilerSimpleText.php b/includes/profiler/ProfilerSimpleText.php index 3621a41f..3e7d6fa4 100644 --- a/includes/profiler/ProfilerSimpleText.php +++ b/includes/profiler/ProfilerSimpleText.php @@ -1,5 +1,22 @@ <?php /** + * Profiler showing output in page source. + * + * 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 Profiler */ diff --git a/includes/profiler/ProfilerSimpleTrace.php b/includes/profiler/ProfilerSimpleTrace.php index 784609f5..822e9fe4 100644 --- a/includes/profiler/ProfilerSimpleTrace.php +++ b/includes/profiler/ProfilerSimpleTrace.php @@ -1,5 +1,22 @@ <?php /** + * Profiler showing execution trace. + * + * 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 Profiler */ @@ -10,20 +27,11 @@ * @ingroup Profiler */ class ProfilerSimpleTrace extends ProfilerSimple { - var $trace = ""; + var $trace = "Beginning trace: \n"; var $memory = 0; - function __construct( $params ) { - global $wgRequestTime, $wgRUstart; - parent::__construct( $params ); - if ( !empty( $wgRequestTime ) && !empty( $wgRUstart ) ) { - $this->mWorkStack[] = array( '-total', 0, $wgRequestTime, $this->getCpuTime( $wgRUstart ) ); - } - $this->trace .= "Beginning trace: \n"; - } - - function profileIn($functionname) { - $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime()); + function profileIn( $functionname ) { + parent::profileIn( $functionname ); $this->trace .= " " . sprintf("%6.1f",$this->memoryDiff()) . str_repeat( " ", count($this->mWorkStack)) . " > " . $functionname . "\n"; } @@ -48,12 +56,12 @@ class ProfilerSimpleTrace extends ProfilerSimple { elseif ( $ofname != $functionname ) { $this->trace .= "Profiling error: in({$ofname}), out($functionname)"; } - $elapsedreal = microtime( true ) - $ortime; + $elapsedreal = $this->getTime() - $ortime; $this->trace .= sprintf( "%03.6f %6.1f", $elapsedreal, $this->memoryDiff() ) . str_repeat(" ", count( $this->mWorkStack ) + 1 ) . " < " . $functionname . "\n"; } } - + function memoryDiff() { $diff = memory_get_usage() - $this->memory; $this->memory = memory_get_usage(); diff --git a/includes/profiler/ProfilerSimpleUDP.php b/includes/profiler/ProfilerSimpleUDP.php index ae607aa6..a95ccb0d 100644 --- a/includes/profiler/ProfilerSimpleUDP.php +++ b/includes/profiler/ProfilerSimpleUDP.php @@ -1,5 +1,22 @@ <?php /** + * Profiler sending messages over UDP. + * + * 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 Profiler */ @@ -10,6 +27,10 @@ * @ingroup Profiler */ class ProfilerSimpleUDP extends ProfilerSimple { + public function isPersistent() { + return true; + } + public function logData() { global $wgUDPProfilerHost, $wgUDPProfilerPort; diff --git a/includes/profiler/ProfilerStub.php b/includes/profiler/ProfilerStub.php index 1a0933c4..c0eb0fb4 100644 --- a/includes/profiler/ProfilerStub.php +++ b/includes/profiler/ProfilerStub.php @@ -1,13 +1,38 @@ <?php /** - * Stub profiling functions + * Stub profiling functions. + * + * 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 Profiler */ + +/** + * Stub profiler that does nothing + * + * @ingroup Profiler + */ class ProfilerStub extends Profiler { public function isStub() { return true; } + public function isPersistent() { + return false; + } public function profileIn( $fn ) {} public function profileOut( $fn ) {} public function getOutput() {} diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 9175b10d..3b48a266 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -1,5 +1,7 @@ <?php /** + * Base class for resource loading system. + * * 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 @@ -161,11 +163,11 @@ class ResourceLoader { $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength ); - $result .= "\n\n/* cache key: $key */\n"; + $result .= "\n/* cache key: $key */"; break; case 'minify-css': $result = CSSMin::minify( $data ); - $result .= "\n\n/* cache key: $key */\n"; + $result .= "\n/* cache key: $key */"; break; } @@ -215,7 +217,7 @@ class ResourceLoader { * Registers a module with the ResourceLoader system. * * @param $name Mixed: Name of module as a string or List of name/object pairs as an array - * @param $info Module info array. For backwards compatibility with 1.17alpha, + * @param $info array Module info array. For backwards compatibility with 1.17alpha, * this may also be a ResourceLoaderModule object. Optional when using * multiple-registration calling style. * @throws MWException: If a duplicate module registration is attempted @@ -239,9 +241,9 @@ class ResourceLoader { ); } - // Check $name for illegal characters - if ( preg_match( '/[|,!]/', $name ) ) { - throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|), commas (,) or exclamation marks (!)" ); + // Check $name for validity + if ( !self::isValidModuleName( $name ) ) { + throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" ); } // Attach module @@ -354,6 +356,7 @@ class ResourceLoader { * @return Array */ public function getTestModuleNames( $framework = 'all' ) { + /// @TODO: api siteinfo prop testmodulenames modulenames if ( $framework == 'all' ) { return $this->testModuleNames; } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) { @@ -499,10 +502,6 @@ class ResourceLoader { $response = $this->makeComment( $warnings ) . $response; } - // Remove the output buffer and output the response - ob_end_clean(); - echo $response; - // Save response to file cache unless there are errors if ( isset( $fileCache ) && !$errors && !$missing ) { // Cache single modules...and other requests if there are enough hits @@ -515,6 +514,10 @@ class ResourceLoader { } } + // Remove the output buffer and output the response + ob_end_clean(); + echo $response; + wfProfileOut( __METHOD__ ); } @@ -598,7 +601,7 @@ class ResourceLoader { /** * Send out code for a response from file cache if possible * - * @param $fileCache ObjectFileCache: Cache object for this request URL + * @param $fileCache ResourceFileCache: Cache object for this request URL * @param $context ResourceLoaderContext: Context in which to generate a response * @return bool If this found a cache file and handled the response */ @@ -682,6 +685,7 @@ class ResourceLoader { } // Generate output + $isRaw = false; foreach ( $modules as $name => $module ) { /** * @var $module ResourceLoaderModule @@ -699,7 +703,7 @@ class ResourceLoader { $scripts = $module->getScriptURLsForDebug( $context ); } else { $scripts = $module->getScript( $context ); - if ( is_string( $scripts ) ) { + if ( is_string( $scripts ) && strlen( $scripts ) && substr( $scripts, -1 ) !== ';' ) { // bug 27054: Append semicolon to prevent weird bugs // caused by files not terminating their statements right $scripts .= ";\n"; @@ -709,12 +713,39 @@ class ResourceLoader { // Styles $styles = array(); if ( $context->shouldIncludeStyles() ) { - // If we are in debug mode, we'll want to return an array of URLs - // See comment near shouldIncludeScripts() for more details - if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) { - $styles = $module->getStyleURLsForDebug( $context ); - } else { - $styles = $module->getStyles( $context ); + // Don't create empty stylesheets like array( '' => '' ) for modules + // that don't *have* any stylesheets (bug 38024). + $stylePairs = $module->getStyles( $context ); + if ( count ( $stylePairs ) ) { + // If we are in debug mode without &only= set, we'll want to return an array of URLs + // See comment near shouldIncludeScripts() for more details + if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) { + $styles = array( + 'url' => $module->getStyleURLsForDebug( $context ) + ); + } else { + // Minify CSS before embedding in mw.loader.implement call + // (unless in debug mode) + if ( !$context->getDebug() ) { + foreach ( $stylePairs as $media => $style ) { + // Can be either a string or an array of strings. + if ( is_array( $style ) ) { + $stylePairs[$media] = array(); + foreach ( $style as $cssText ) { + if ( is_string( $cssText ) ) { + $stylePairs[$media][] = $this->filter( 'minify-css', $cssText ); + } + } + } elseif ( is_string( $style ) ) { + $stylePairs[$media] = $this->filter( 'minify-css', $style ); + } + } + } + // Wrap styles into @media groups as needed and flatten into a numerical array + $styles = array( + 'css' => self::makeCombinedStyles( $stylePairs ) + ); + } } } @@ -733,23 +764,21 @@ class ResourceLoader { } break; case 'styles': - $out .= self::makeCombinedStyles( $styles ); + // We no longer seperate into media, they are all combined now with + // custom media type groups into @media .. {} sections as part of the css string. + // Module returns either an empty array or a numerical array with css strings. + $out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : ''; break; case 'messages': $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) ); break; default: - // Minify CSS before embedding in mw.loader.implement call - // (unless in debug mode) - if ( !$context->getDebug() ) { - foreach ( $styles as $media => $style ) { - if ( is_string( $style ) ) { - $styles[$media] = $this->filter( 'minify-css', $style ); - } - } - } - $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, - new XmlJsCode( $messagesBlob ) ); + $out .= self::makeLoaderImplementScript( + $name, + $scripts, + $styles, + new XmlJsCode( $messagesBlob ) + ); break; } } catch ( Exception $e ) { @@ -760,15 +789,14 @@ class ResourceLoader { $missing[] = $name; unset( $modules[$name] ); } + $isRaw |= $module->isRaw(); wfProfileOut( __METHOD__ . '-' . $name ); } // Update module states - if ( $context->shouldIncludeScripts() ) { + if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) { // Set the state of modules loaded as only scripts to ready - if ( count( $modules ) && $context->getOnly() === 'scripts' - && !isset( $modules['startup'] ) ) - { + if ( count( $modules ) && $context->getOnly() === 'scripts' ) { $out .= self::makeLoaderStateScript( array_fill_keys( array_keys( $modules ), 'ready' ) ); } @@ -796,9 +824,9 @@ class ResourceLoader { * Returns JS code to call to mw.loader.implement for a module with * given properties. * - * @param $name Module name + * @param $name string Module name * @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code - * @param $styles Mixed: List of CSS strings keyed by media type, or list of lists of URLs to + * @param $styles Mixed: Array of CSS strings keyed by media type, or an array of lists of URLs to * CSS files keyed by media type * @param $messages Mixed: List of messages associated with this module. May either be an * associative array mapping message key to value, or a JSON-encoded message blob containing @@ -808,7 +836,7 @@ class ResourceLoader { */ public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { if ( is_string( $scripts ) ) { - $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" ); + $scripts = new XmlJsCode( "function () {\n{$scripts}\n}" ); } elseif ( !is_array( $scripts ) ) { throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' ); } @@ -817,6 +845,11 @@ class ResourceLoader { array( $name, $scripts, + // Force objects. mw.loader.implement requires them to be javascript objects. + // Although these variables are associative arrays, which become javascript + // objects through json_encode. In many cases they will be empty arrays, and + // PHP/json_encode() consider empty arrays to be numerical arrays and + // output javascript "[]" instead of "{}". This fixes that. (object)$styles, (object)$messages ) ); @@ -836,26 +869,34 @@ class ResourceLoader { /** * Combines an associative array mapping media type to CSS into a - * single stylesheet with @media blocks. + * single stylesheet with "@media" blocks. * - * @param $styles Array: List of CSS strings keyed by media type + * @param $styles Array: Array keyed by media type containing (arrays of) CSS strings. * - * @return string + * @return Array */ - public static function makeCombinedStyles( array $styles ) { - $out = ''; - foreach ( $styles as $media => $style ) { - // Transform the media type based on request params and config - // The way that this relies on $wgRequest to propagate request params is slightly evil - $media = OutputPage::transformCssMedia( $media ); - - if ( $media === null ) { - // Skip - } elseif ( $media === '' || $media == 'all' ) { - // Don't output invalid or frivolous @media statements - $out .= "$style\n"; - } else { - $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n"; + private static function makeCombinedStyles( array $stylePairs ) { + $out = array(); + foreach ( $stylePairs as $media => $styles ) { + // ResourceLoaderFileModule::getStyle can return the styles + // as a string or an array of strings. This is to allow separation in + // the front-end. + $styles = (array) $styles; + foreach ( $styles as $style ) { + $style = trim( $style ); + // Don't output an empty "@media print { }" block (bug 40498) + if ( $style !== '' ) { + // Transform the media type based on request params and config + // The way that this relies on $wgRequest to propagate request params is slightly evil + $media = OutputPage::transformCssMedia( $media ); + + if ( $media === '' || $media == 'all' ) { + $out[] = $style; + } else if ( is_string( $media ) ) { + $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}"; + } + // else: skip + } } } return $out; @@ -902,7 +943,7 @@ class ResourceLoader { public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) { $script = str_replace( "\n", "\n\t", trim( $script ) ); return Xml::encodeJsCall( - "( function( name, version, dependencies, group, source ) {\n\t$script\n} )", + "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )", array( $name, $version, $dependencies, $group, $source ) ); } @@ -975,7 +1016,7 @@ class ResourceLoader { * @return string */ public static function makeLoaderConditionalScript( $script ) { - return "if(window.mw){\n".trim( $script )."\n}"; + return "if(window.mw){\n" . trim( $script ) . "\n}"; } /** @@ -1091,4 +1132,17 @@ class ResourceLoader { ksort( $query ); return $query; } + + /** + * Check a module name for validity. + * + * Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be + * at most 255 bytes. + * + * @param $moduleName string Module name to check + * @return bool Whether $moduleName is a valid module name + */ + public static function isValidModuleName( $moduleName ) { + return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255; + } } diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index dd69bb01..0e96c6c8 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -1,5 +1,7 @@ <?php /** + * Context for resource loader modules. + * * 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 @@ -39,6 +41,7 @@ class ResourceLoaderContext { protected $only; protected $version; protected $hash; + protected $raw; /* Methods */ @@ -62,6 +65,7 @@ class ResourceLoaderContext { $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug ); $this->only = $request->getVal( 'only' ); $this->version = $request->getVal( 'version' ); + $this->raw = $request->getFuzzyBool( 'raw' ); $skinnames = Skin::getSkinNames(); // If no skin is specified, or we don't recognize the skin, use the default skin @@ -157,7 +161,7 @@ class ResourceLoaderContext { $this->direction = $this->request->getVal( 'dir' ); if ( !$this->direction ) { # directionality based on user language (see bug 6100) - $this->direction = Language::factory( $this->language )->getDir(); + $this->direction = Language::factory( $this->getLanguage() )->getDir(); } } return $this->direction; @@ -201,6 +205,13 @@ class ResourceLoaderContext { /** * @return bool */ + public function getRaw() { + return $this->raw; + } + + /** + * @return bool + */ public function shouldIncludeScripts() { return is_null( $this->only ) || $this->only === 'scripts'; } diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index 3d657e1c..d0c56ae8 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader module based on local JavaScript/CSS files. + * * 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 @@ -109,6 +111,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected $position = 'bottom'; /** Boolean: Link to raw files in debug mode */ protected $debugRaw = true; + /** Boolean: Whether mw.loader.state() call should be omitted */ + protected $raw = false; /** * Array: Cache for mtime * @par Usage: @@ -238,6 +242,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { break; // Single booleans case 'debugRaw': + case 'raw': $this->{$member} = (bool) $option; break; } @@ -366,6 +371,13 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } /** + * @return bool + */ + public function isRaw() { + return $this->raw; + } + + /** * Get the last modified timestamp of this module. * * Last modified timestamps are calculated from the highest last modified @@ -622,7 +634,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { // Get and register local file references $this->localFileRefs = array_merge( $this->localFileRefs, - CSSMin::getLocalFileReferences( $style, $dir ) ); + CSSMin::getLocalFileReferences( $style, $dir ) + ); return CSSMin::remap( $style, $dir, $remoteDir, true ); diff --git a/includes/resourceloader/ResourceLoaderFilePageModule.php b/includes/resourceloader/ResourceLoaderFilePageModule.php index e3b719bb..61ed5206 100644 --- a/includes/resourceloader/ResourceLoaderFilePageModule.php +++ b/includes/resourceloader/ResourceLoaderFilePageModule.php @@ -1,5 +1,26 @@ <?php /** + * Resource loader module for MediaWiki:Filepage.css + * + * 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 + */ + +/** * ResourceLoader definition for MediaWiki:Filepage.css */ class ResourceLoaderFilePageModule extends ResourceLoaderWikiModule { diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php new file mode 100644 index 00000000..c916c4a5 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php @@ -0,0 +1,122 @@ +<?php +/** + * Resource loader module for populating language specific data. + * + * 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 + * @author Santhosh Thottingal + * @author Timo Tijhof + */ + +/** + * ResourceLoader module for populating language specific data. + */ +class ResourceLoaderLanguageDataModule extends ResourceLoaderModule { + + protected $language; + /** + * Get the grammar forms for the site content language. + * + * @return array + */ + protected function getSiteLangGrammarForms() { + return $this->language->getGrammarForms(); + } + + /** + * Get the plural forms for the site content language. + * + * @return array + */ + protected function getPluralRules() { + return $this->language->getPluralRules(); + } + + /** + * Get the digit transform table for the content language + * Seperator transform table also required here to convert + * the . and , sign to appropriate forms in content language. + * + * @return array + */ + protected function getDigitTransformTable() { + $digitTransformTable = $this->language->digitTransformTable(); + $separatorTransformTable = $this->language->separatorTransformTable(); + if ( $digitTransformTable ) { + array_merge( $digitTransformTable, (array)$separatorTransformTable ); + } else { + return $separatorTransformTable; + } + return $digitTransformTable; + } + + /** + * Get all the dynamic data for the content language to an array + * + * @return array + */ + protected function getData() { + return array( + 'digitTransformTable' => $this->getDigitTransformTable(), + 'grammarForms' => $this->getSiteLangGrammarForms(), + 'pluralRules' => $this->getPluralRules(), + ); + } + + /** + * @param $context ResourceLoaderContext + * @return string: JavaScript code + */ + public function getScript( ResourceLoaderContext $context ) { + $this->language = Language::factory( $context->getLanguage() ); + return Xml::encodeJsCall( 'mw.language.setData', array( + $this->language->getCode(), + $this->getData() + ) ); + } + + /** + * @param $context ResourceLoaderContext + * @return array|int|Mixed + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + $this->language = Language::factory( $context ->getLanguage() ); + $cache = wfGetCache( CACHE_ANYTHING ); + $key = wfMemcKey( 'resourceloader', 'langdatamodule', 'changeinfo' ); + + $data = $this->getData(); + $hash = md5( serialize( $data ) ); + + $result = $cache->get( $key ); + if ( is_array( $result ) && $result['hash'] === $hash ) { + return $result['timestamp']; + } + $timestamp = wfTimestamp(); + $cache->set( $key, array( + 'hash' => $hash, + 'timestamp' => $timestamp, + ) ); + return $timestamp; + } + + /** + * @return array + */ + public function getDependencies() { + return array( 'mediawiki.language.init' ); + } +} diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index 1a232ec2..9c49c45f 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -1,5 +1,7 @@ <?php /** + * Abstraction for resource loader modules. + * * 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 @@ -170,7 +172,9 @@ abstract class ResourceLoaderModule { * Get all CSS for this module for a given skin. * * @param $context ResourceLoaderContext: Context object - * @return Array: List of CSS strings keyed by media type + * @return Array: List of CSS strings or array of CSS strings keyed by media type. + * like array( 'screen' => '.foo { width: 0 }' ); + * or array( 'screen' => array( '.foo { width: 0 }' ) ); */ public function getStyles( ResourceLoaderContext $context ) { // Stub, override expected @@ -235,8 +239,8 @@ abstract class ResourceLoaderModule { /** * Where on the HTML page should this module's JS be loaded? - * 'top': in the <head> - * 'bottom': at the bottom of the <body> + * - 'top': in the "<head>" + * - 'bottom': at the bottom of the "<body>" * * @return string */ @@ -245,6 +249,17 @@ abstract class ResourceLoaderModule { } /** + * Whether this module's JS expects to work without the client-side ResourceLoader module. + * Returning true from this function will prevent mw.loader.state() call from being + * appended to the bottom of the script. + * + * @return bool + */ + public function isRaw() { + return false; + } + + /** * Get the loader JS for this module, if set. * * @return Mixed: JavaScript loader code as a string or boolean false if no custom loader set diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderNoscriptModule.php index 28f629a2..8e81c8d9 100644 --- a/includes/resourceloader/ResourceLoaderNoscriptModule.php +++ b/includes/resourceloader/ResourceLoaderNoscriptModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader for site customizations for users without JavaScript enabled. + * * 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 diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php index 2527a0a3..03fe1fe5 100644 --- a/includes/resourceloader/ResourceLoaderSiteModule.php +++ b/includes/resourceloader/ResourceLoaderSiteModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader module for site customizations. + * * 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 diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 5dbce439..20ee83f9 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -1,5 +1,7 @@ <?php /** + * Module for resource loader initialization. + * * 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 @@ -35,8 +37,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { protected function getConfig( $context ) { global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, - $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion, - $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest, + $wgVariantArticlePath, $wgActionPaths, $wgVersion, + $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgSitename, $wgFileExtensions, $wgExtensionAssetsPath, $wgCookiePrefix, $wgResourceLoaderMaxQueryLength; @@ -77,10 +79,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgVersion' => $wgVersion, 'wgEnableAPI' => $wgEnableAPI, 'wgEnableWriteAPI' => $wgEnableWriteAPI, - 'wgDefaultDateFormat' => $wgContLang->getDefaultDateFormat(), - 'wgMonthNames' => $wgContLang->getMonthNamesArray(), - 'wgMonthNamesShort' => $wgContLang->getMonthAbbreviationsArray(), - 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null, + 'wgMainPageTitle' => $mainPage->getPrefixedText(), 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(), 'wgNamespaceIds' => $namespaceIds, 'wgSiteName' => $wgSitename, @@ -96,9 +95,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength, 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces, ); - if ( $wgUseAjax && $wgEnableMWSuggest ) { - $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate(); - } wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) ); @@ -125,12 +121,12 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // Register modules foreach ( $resourceLoader->getModuleNames() as $name ) { $module = $resourceLoader->getModule( $name ); + $deps = $module->getDependencies(); + $group = $module->getGroup(); + $source = $module->getSource(); // Support module loader scripts $loader = $module->getLoaderScript(); if ( $loader !== false ) { - $deps = $module->getDependencies(); - $group = $module->getGroup(); - $source = $module->getSource(); $version = wfTimestamp( TS_ISO_8601_BASIC, $module->getModifiedTime( $context ) ); $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $source, $loader ); @@ -143,26 +139,23 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) ); // Modules without dependencies, a group or a foreign source pass two arguments (name, timestamp) to // mw.loader.register() - if ( !count( $module->getDependencies() && $module->getGroup() === null && $module->getSource() === 'local' ) ) { + if ( !count( $deps ) && $group === null && $source === 'local' ) { $registrations[] = array( $name, $mtime ); } // Modules with dependencies but no group or foreign source pass three arguments // (name, timestamp, dependencies) to mw.loader.register() - elseif ( $module->getGroup() === null && $module->getSource() === 'local' ) { - $registrations[] = array( - $name, $mtime, $module->getDependencies() ); + elseif ( $group === null && $source === 'local' ) { + $registrations[] = array( $name, $mtime, $deps ); } // Modules with a group but no foreign source pass four arguments (name, timestamp, dependencies, group) // to mw.loader.register() - elseif ( $module->getSource() === 'local' ) { - $registrations[] = array( - $name, $mtime, $module->getDependencies(), $module->getGroup() ); + elseif ( $source === 'local' ) { + $registrations[] = array( $name, $mtime, $deps, $group ); } // Modules with a foreign source pass five arguments (name, timestamp, dependencies, group, source) // to mw.loader.register() else { - $registrations[] = array( - $name, $mtime, $module->getDependencies(), $module->getGroup(), $module->getSource() ); + $registrations[] = array( $name, $mtime, $deps, $group, $source ); } } } @@ -175,6 +168,13 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { /* Methods */ /** + * @return bool + */ + public function isRaw() { + return true; + } + + /** * @param $context ResourceLoaderContext * @return string */ @@ -185,19 +185,20 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { if ( $context->getOnly() === 'scripts' ) { // The core modules: - $modules = array( 'jquery', 'mediawiki' ); - wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$modules ) ); + $moduleNames = array( 'jquery', 'mediawiki' ); + wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$moduleNames ) ); // Get the latest version + $loader = $context->getResourceLoader(); $version = 0; - foreach ( $modules as $moduleName ) { + foreach ( $moduleNames as $moduleName ) { $version = max( $version, - $context->getResourceLoader()->getModule( $moduleName )->getModifiedTime( $context ) + $loader->getModule( $moduleName )->getModifiedTime( $context ) ); } // Build load query for StartupModules $query = array( - 'modules' => ResourceLoader::makePackedModulesString( $modules ), + 'modules' => ResourceLoader::makePackedModulesString( $moduleNames ), 'only' => 'scripts', 'lang' => $context->getLanguage(), 'skin' => $context->getSkin(), @@ -210,6 +211,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // Startup function $configuration = $this->getConfig( $context ); $registrations = self::getModuleRegistrations( $context ); + $registrations = str_replace( "\n", "\n\t", trim( $registrations ) ); // fix indentation $out .= "var startUp = function() {\n" . "\tmw.config = new " . Xml::encodeJsCall( 'mw.Map', array( $wgLegacyJavaScriptGlobals ) ) . "\n" . "\t$registrations\n" . diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php index 02693d3e..0e95d964 100644 --- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php +++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader module for user preference customizations. + * * 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 @@ -69,13 +71,6 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { $rules[] = 'a:lang(ar), a:lang(ckb), a:lang(fa),a:lang(kk-arab), ' . 'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }'; } - if ( $options['highlightbroken'] ) { - $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n"; - } else { - $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }"; - $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }"; - $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }"; - } if ( $options['justify'] ) { $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n"; } @@ -86,7 +81,10 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { $rules[] = ".editsection { display: none; }\n"; } if ( $options['editfont'] !== 'default' ) { - $rules[] = "textarea { font-family: {$options['editfont']}; }\n"; + // Double-check that $options['editfont'] consists of safe characters only + if ( preg_match( '/^[a-zA-Z0-9_, -]+$/', $options['editfont'] ) ) { + $rules[] = "textarea { font-family: {$options['editfont']}; }\n"; + } } $style = implode( "\n", $rules ); if ( $this->getFlip( $context ) ) { diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php index 733dfa04..1316f423 100644 --- a/includes/resourceloader/ResourceLoaderUserGroupsModule.php +++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader module for user customizations. + * * 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 @@ -31,21 +33,32 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule { * @return array */ protected function getPages( ResourceLoaderContext $context ) { - if ( $context->getUser() ) { - $user = User::newFromName( $context->getUser() ); - if ( $user instanceof User ) { - $pages = array(); - foreach( $user->getEffectiveGroups() as $group ) { - if ( in_array( $group, array( '*', 'user' ) ) ) { - continue; - } - $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' ); - $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' ); - } - return $pages; + global $wgUser; + + $userName = $context->getUser(); + if ( $userName === null ) { + return array(); + } + + // Use $wgUser is possible; allows to skip a lot of code + if ( is_object( $wgUser ) && $wgUser->getName() == $userName ) { + $user = $wgUser; + } else { + $user = User::newFromName( $userName ); + if ( !$user instanceof User ) { + return array(); + } + } + + $pages = array(); + foreach( $user->getEffectiveGroups() as $group ) { + if ( in_array( $group, array( '*', 'user' ) ) ) { + continue; } + $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' ); + $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' ); } - return array(); + return $pages; } /* Methods */ diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php index 338b6322..177302c5 100644 --- a/includes/resourceloader/ResourceLoaderUserModule.php +++ b/includes/resourceloader/ResourceLoaderUserModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader module for user customizations. + * * 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 @@ -33,33 +35,40 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { * @return array */ protected function getPages( ResourceLoaderContext $context ) { - if ( $context->getUser() ) { - // Get the normalized title of the user's user page - $username = $context->getUser(); - $userpageTitle = Title::makeTitleSafe( NS_USER, $username ); - $userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works + $username = $context->getUser(); + + if ( $username === null ) { + return array(); + } + + // Get the normalized title of the user's user page + $userpageTitle = Title::makeTitleSafe( NS_USER, $username ); + + if ( !$userpageTitle instanceof Title ) { + return array(); + } + + $userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works - $pages = array( - "$userpage/common.js" => array( 'type' => 'script' ), - "$userpage/" . $context->getSkin() . '.js' => - array( 'type' => 'script' ), - "$userpage/common.css" => array( 'type' => 'style' ), - "$userpage/" . $context->getSkin() . '.css' => - array( 'type' => 'style' ), - ); + $pages = array( + "$userpage/common.js" => array( 'type' => 'script' ), + "$userpage/" . $context->getSkin() . '.js' => + array( 'type' => 'script' ), + "$userpage/common.css" => array( 'type' => 'style' ), + "$userpage/" . $context->getSkin() . '.css' => + array( 'type' => 'style' ), + ); - // Hack for bug 26283: if we're on a preview page for a CSS/JS page, - // we need to exclude that page from this module. In that case, the excludepage - // parameter will be set to the name of the page we need to exclude. - $excludepage = $context->getRequest()->getVal( 'excludepage' ); - if ( isset( $pages[$excludepage] ) ) { - // This works because $excludepage is generated with getPrefixedDBkey(), - // just like the keys in $pages[] above - unset( $pages[$excludepage] ); - } - return $pages; + // Hack for bug 26283: if we're on a preview page for a CSS/JS page, + // we need to exclude that page from this module. In that case, the excludepage + // parameter will be set to the name of the page we need to exclude. + $excludepage = $context->getRequest()->getVal( 'excludepage' ); + if ( isset( $pages[$excludepage] ) ) { + // This works because $excludepage is generated with getPrefixedDBkey(), + // just like the keys in $pages[] above + unset( $pages[$excludepage] ); } - return array(); + return $pages; } /* Methods */ diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index 7b162205..4624cbce 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader module for user preference customizations. + * * 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 @@ -42,7 +44,7 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { if ( isset( $this->modifiedTime[$hash] ) ) { return $this->modifiedTime[$hash]; } - + global $wgUser; return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() ); } @@ -58,6 +60,13 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { } /** + * @return bool + */ + public function supportsURLLoading() { + return false; + } + + /** * @return string */ public function getGroup() { diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php index e1a52388..62d096a6 100644 --- a/includes/resourceloader/ResourceLoaderUserTokensModule.php +++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php @@ -1,5 +1,7 @@ <?php /** + * Resource loader module for user tokens. + * * 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 @@ -55,6 +57,13 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule { } /** + * @return bool + */ + public function supportsURLLoading() { + return false; + } + + /** * @return string */ public function getGroup() { diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 91a51f89..ee8dd1e5 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -1,5 +1,7 @@ <?php /** + * Abstraction for resource loader modules which pull from wiki pages. + * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -20,8 +22,6 @@ * @author Roan Kattouw */ -defined( 'MEDIAWIKI' ) || die( 1 ); - /** * Abstraction for resource loader modules which pull from wiki pages * @@ -42,7 +42,6 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { /* Abstract Protected Methods */ /** - * @abstract * @param $context ResourceLoaderContext */ abstract protected function getPages( ResourceLoaderContext $context ); @@ -69,14 +68,10 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { * @return null|string */ protected function getContent( $title ) { - if ( $title->getNamespace() === NS_MEDIAWIKI ) { - $message = wfMessage( $title->getDBkey() )->inContentLanguage(); - return $message->exists() ? $message->plain() : ''; - } if ( !$title->isCssJsSubpage() && !$title->isCssOrJsPage() ) { return null; } - $revision = Revision::newFromTitle( $title ); + $revision = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); if ( !$revision ) { return null; } @@ -137,12 +132,12 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { } $style = CSSMin::remap( $style, false, $wgScriptPath, true ); if ( !isset( $styles[$media] ) ) { - $styles[$media] = ''; + $styles[$media] = array(); } if ( strpos( $titleText, '*/' ) === false ) { - $styles[$media] .= "/* $titleText */\n"; + $style = "/* $titleText */\n" . $style; } - $styles[$media] .= $style . "\n"; + $styles[$media][] = $style; } return $styles; } @@ -181,7 +176,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { // We're dealing with a subclass that doesn't have a DB return array(); } - + $hash = $context->getHash(); if ( isset( $this->titleMtimes[$hash] ) ) { return $this->titleMtimes[$hash]; diff --git a/includes/revisiondelete/RevisionDelete.php b/includes/revisiondelete/RevisionDelete.php index 6cee6246..6ceadff4 100644 --- a/includes/revisiondelete/RevisionDelete.php +++ b/includes/revisiondelete/RevisionDelete.php @@ -1,5 +1,27 @@ <?php /** + * Base implementations for deletable items. + * + * 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 RevisionDelete + */ + +/** * List for revision table items * * This will check both the 'revision' table for live revisions and the @@ -191,13 +213,16 @@ class RevDel_RevisionItem extends RevDel_Item { /** * Get the HTML link to the revision text. * Overridden by RevDel_ArchiveItem. + * @return string */ protected function getRevisionLink() { - $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true ); + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->revision->getTimestamp(), $this->list->getUser() ) ); + if ( $this->isDeleted() && !$this->canViewContent() ) { return $date; } - return Linker::link( + return Linker::linkKnown( $this->list->title, $date, array(), @@ -211,38 +236,36 @@ class RevDel_RevisionItem extends RevDel_Item { /** * Get the HTML link to the diff. * Overridden by RevDel_ArchiveItem + * @return string */ protected function getDiffLink() { if ( $this->isDeleted() && !$this->canViewContent() ) { - return wfMsgHtml('diff'); + return $this->list->msg( 'diff' )->escaped(); } else { return - Linker::link( + Linker::linkKnown( $this->list->title, - wfMsgHtml('diff'), + $this->list->msg( 'diff' )->escaped(), array(), array( 'diff' => $this->revision->getId(), 'oldid' => 'prev', 'unhide' => 1 - ), - array( - 'known', - 'noclasses' ) ); } } public function getHTML() { - $difflink = $this->getDiffLink(); + $difflink = $this->list->msg( 'parentheses' ) + ->rawParams( $this->getDiffLink() )->escaped(); $revlink = $this->getRevisionLink(); $userlink = Linker::revUserLink( $this->revision ); $comment = Linker::revComment( $this->revision ); if ( $this->isDeleted() ) { $revlink = "<span class=\"history-deleted\">$revlink</span>"; } - return "<li>($difflink) $revlink $userlink $comment</li>"; + return "<li>$difflink $revlink $userlink $comment</li>"; } } @@ -298,7 +321,7 @@ class RevDel_ArchiveItem extends RevDel_RevisionItem { public function __construct( $list, $row ) { RevDel_Item::__construct( $list, $row ); $this->revision = Revision::newFromArchiveRow( $row, - array( 'page' => $this->list->title->getArticleId() ) ); + array( 'page' => $this->list->title->getArticleID() ) ); } public function getIdField() { @@ -339,29 +362,39 @@ class RevDel_ArchiveItem extends RevDel_RevisionItem { } protected function getRevisionLink() { - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true ); + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->revision->getTimestamp(), $this->list->getUser() ) ); + if ( $this->isDeleted() && !$this->canViewContent() ) { return $date; } - return Linker::link( $undelete, $date, array(), + + return Linker::link( + SpecialPage::getTitleFor( 'Undelete' ), + $date, + array(), array( 'target' => $this->list->title->getPrefixedText(), 'timestamp' => $this->revision->getTimestamp() - ) ); + ) + ); } protected function getDiffLink() { if ( $this->isDeleted() && !$this->canViewContent() ) { - return wfMsgHtml( 'diff' ); + return $this->list->msg( 'diff' )->escaped(); } - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - return Linker::link( $undelete, wfMsgHtml('diff'), array(), + + return Linker::link( + SpecialPage::getTitleFor( 'Undelete' ), + $this->list->msg( 'diff' )->escaped(), + array(), array( 'target' => $this->list->title->getPrefixedText(), 'diff' => 'prev', 'timestamp' => $this->revision->getTimestamp() - ) ); + ) + ); } } @@ -375,7 +408,7 @@ class RevDel_ArchivedRevisionItem extends RevDel_ArchiveItem { RevDel_Item::__construct( $list, $row ); $this->revision = Revision::newFromArchiveRow( $row, - array( 'page' => $this->list->title->getArticleId() ) ); + array( 'page' => $this->list->title->getArticleID() ) ); } public function getIdField() { @@ -569,31 +602,34 @@ class RevDel_FileItem extends RevDel_Item { /** * Get the link to the file. * Overridden by RevDel_ArchivedFileItem. + * @return string */ protected function getLink() { - $date = $this->list->getLanguage()->timeanddate( $this->file->getTimestamp(), true ); - if ( $this->isDeleted() ) { - # Hidden files... - if ( !$this->canViewContent() ) { - $link = $date; - } else { - $revdelete = SpecialPage::getTitleFor( 'Revisiondelete' ); - $link = Linker::link( - $revdelete, - $date, array(), - array( - 'target' => $this->list->title->getPrefixedText(), - 'file' => $this->file->getArchiveName(), - 'token' => $this->list->getUser()->getEditToken( - $this->file->getArchiveName() ) - ) - ); - } - return '<span class="history-deleted">' . $link . '</span>'; - } else { + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->file->getTimestamp(), $this->list->getUser() ) ); + + if ( !$this->isDeleted() ) { # Regular files... - return Xml::element( 'a', array( 'href' => $this->file->getUrl() ), $date ); + return Html::rawElement( 'a', array( 'href' => $this->file->getUrl() ), $date ); } + + # Hidden files... + if ( !$this->canViewContent() ) { + $link = $date; + } else { + $link = Linker::link( + SpecialPage::getTitleFor( 'Revisiondelete' ), + $date, + array(), + array( + 'target' => $this->list->title->getPrefixedText(), + 'file' => $this->file->getArchiveName(), + 'token' => $this->list->getUser()->getEditToken( + $this->file->getArchiveName() ) + ) + ); + } + return '<span class="history-deleted">' . $link . '</span>'; } /** * Generate a user tool link cluster if the current user is allowed to view it @@ -604,7 +640,7 @@ class RevDel_FileItem extends RevDel_Item { $link = Linker::userLink( $this->file->user, $this->file->user_text ) . Linker::userToolLinks( $this->file->user, $this->file->user_text ); } else { - $link = wfMsgHtml( 'rev-deleted-user' ); + $link = $this->list->msg( 'rev-deleted-user' )->escaped(); } if( $this->file->isDeleted( Revision::DELETED_USER ) ) { return '<span class="history-deleted">' . $link . '</span>'; @@ -622,7 +658,7 @@ class RevDel_FileItem extends RevDel_Item { if( $this->file->userCan( File::DELETED_COMMENT, $this->list->getUser() ) ) { $block = Linker::commentBlock( $this->file->description ); } else { - $block = ' ' . wfMsgHtml( 'rev-deleted-comment' ); + $block = ' ' . $this->list->msg( 'rev-deleted-comment' )->escaped(); } if( $this->file->isDeleted( File::DELETED_COMMENT ) ) { return "<span class=\"history-deleted\">$block</span>"; @@ -632,14 +668,9 @@ class RevDel_FileItem extends RevDel_Item { public function getHTML() { $data = - wfMsg( - 'widthheight', - $this->list->getLanguage()->formatNum( $this->file->getWidth() ), - $this->list->getLanguage()->formatNum( $this->file->getHeight() ) - ) . - ' (' . - wfMsgExt( 'nbytes', 'parsemag', $this->list->getLanguage()->formatNum( $this->file->getSize() ) ) . - ')'; + $this->list->msg( 'widthheight' )->numParams( + $this->file->getWidth(), $this->file->getHeight() )->text() . + ' (' . $this->list->msg( 'nbytes' )->numParams( $this->file->getSize() )->text() . ')'; return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' . $data . ' ' . $this->getComment(). '</li>'; @@ -722,13 +753,15 @@ class RevDel_ArchivedFileItem extends RevDel_FileItem { } protected function getLink() { - $date = $this->list->getLanguage()->timeanddate( $this->file->getTimestamp(), true ); - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - $key = $this->file->getKey(); + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->file->getTimestamp(), $this->list->getUser() ) ); + # Hidden files... if( !$this->canViewContent() ) { $link = $date; } else { + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + $key = $this->file->getKey(); $link = Linker::link( $undelete, $date, array(), array( 'target' => $this->list->title->getPrefixedText(), @@ -847,18 +880,21 @@ class RevDel_LogItem extends RevDel_Item { } public function getHTML() { - $date = htmlspecialchars( $this->list->getLanguage()->timeanddate( $this->row->log_timestamp ) ); + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->row->log_timestamp, $this->list->getUser() ) ); $title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title ); $formatter = LogFormatter::newFromRow( $this->row ); + $formatter->setContext( $this->list->getContext() ); $formatter->setAudience( LogFormatter::FOR_THIS_USER ); // Log link for this page $loglink = Linker::link( SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'log' ), + $this->list->msg( 'log' )->escaped(), array(), array( 'page' => $title->getPrefixedText() ) ); + $loglink = $this->list->msg( 'parentheses' )->rawParams( $loglink )->escaped(); // User links and action text $action = $formatter->getActionText(); // Comment @@ -867,6 +903,6 @@ class RevDel_LogItem extends RevDel_Item { $comment = '<span class="history-deleted">' . $comment . '</span>'; } - return "<li>($loglink) $date $action $comment</li>"; + return "<li>$loglink $date $action $comment</li>"; } } diff --git a/includes/revisiondelete/RevisionDeleteAbstracts.php b/includes/revisiondelete/RevisionDeleteAbstracts.php index dc7af194..4f58099f 100644 --- a/includes/revisiondelete/RevisionDeleteAbstracts.php +++ b/includes/revisiondelete/RevisionDeleteAbstracts.php @@ -1,4 +1,25 @@ <?php +/** + * Interface definition for deletable items. + * + * 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 RevisionDelete + */ /** * Abstract base class for a list of deletable items. The list class @@ -16,6 +37,7 @@ abstract class RevDel_List extends RevisionListBase { * Get the DB field name associated with the ID list. * This used to populate the log_search table for finding log entries. * Override this function. + * @return null */ public static function getRelationType() { return null; @@ -25,7 +47,7 @@ abstract class RevDel_List extends RevisionListBase { * Set the visibility for the revisions in this list. Logging and * transactions are done here. * - * @param $params Associative array of parameters. Members are: + * @param $params array Associative array of parameters. Members are: * value: The integer value to set the visibility to * comment: The log comment. * @return Status @@ -37,7 +59,7 @@ abstract class RevDel_List extends RevisionListBase { $this->res = false; $dbw = wfGetDB( DB_MASTER ); $this->doQuery( $dbw ); - $dbw->begin(); + $dbw->begin( __METHOD__ ); $status = Status::newGood(); $missing = array_flip( $this->ids ); $this->clearFileOps(); @@ -110,7 +132,7 @@ abstract class RevDel_List extends RevisionListBase { if ( $status->successCount == 0 ) { $status->ok = false; - $dbw->rollback(); + $dbw->rollback( __METHOD__ ); return $status; } @@ -121,7 +143,7 @@ abstract class RevDel_List extends RevisionListBase { $status->merge( $this->doPreCommitUpdates() ); if ( !$status->isOK() ) { // Fatal error, such as no configured archive directory - $dbw->rollback(); + $dbw->rollback( __METHOD__ ); return $status; } @@ -136,7 +158,7 @@ abstract class RevDel_List extends RevisionListBase { 'authorIds' => $authorIds, 'authorIPs' => $authorIPs ) ); - $dbw->commit(); + $dbw->commit( __METHOD__ ); // Clear caches $status->merge( $this->doPostCommitUpdates() ); @@ -154,7 +176,7 @@ abstract class RevDel_List extends RevisionListBase { /** * Record a log entry on the action - * @param $params Associative array of parameters: + * @param $params array Associative array of parameters: * newBits: The new value of the *_deleted bitfield * oldBits: The old value of the *_deleted bitfield. * title: The target title @@ -189,6 +211,7 @@ abstract class RevDel_List extends RevisionListBase { /** * Get the log action for this list type + * @return string */ public function getLogAction() { return 'revision'; @@ -196,7 +219,7 @@ abstract class RevDel_List extends RevisionListBase { /** * Get log parameter array. - * @param $params Associative array of log parameters, same as updateLog() + * @param $params array Associative array of log parameters, same as updateLog() * @return array */ public function getLogParams( $params ) { @@ -247,6 +270,7 @@ abstract class RevDel_Item extends RevisionItemBase { * Returns true if the item is "current", and the operation to set the given * bits can't be executed for that reason * STUB + * @return bool */ public function isHideCurrentOp( $newBits ) { return false; diff --git a/includes/revisiondelete/RevisionDeleteUser.php b/includes/revisiondelete/RevisionDeleteUser.php index c88b4d91..c02e9c76 100644 --- a/includes/revisiondelete/RevisionDeleteUser.php +++ b/includes/revisiondelete/RevisionDeleteUser.php @@ -1,9 +1,6 @@ <?php /** - * Backend functions for suppressing and unsuppressing all references to a given user, - * used when blocking with HideUser enabled. This was spun out of SpecialBlockip.php - * in 1.18; at some point it needs to be rewritten to either use RevisionDelete abstraction, - * or at least schema abstraction. + * Backend functions for suppressing and unsuppressing all references to a given user. * * 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 @@ -23,6 +20,15 @@ * @file * @ingroup RevisionDelete */ + +/** + * Backend functions for suppressing and unsuppressing all references to a given user, + * used when blocking with HideUser enabled. This was spun out of SpecialBlockip.php + * in 1.18; at some point it needs to be rewritten to either use RevisionDelete abstraction, + * or at least schema abstraction. + * + * @ingroup RevisionDelete + */ class RevisionDeleteUser { /** @@ -30,14 +36,14 @@ class RevisionDeleteUser { * @param $name String username * @param $userId Int user id * @param $op String operator '|' or '&' - * @param $dbw null|Database, if you happen to have one lying around + * @param $dbw null|DatabaseBase, if you happen to have one lying around * @return bool */ private static function setUsernameBitfields( $name, $userId, $op, $dbw ) { - if( $op !== '|' && $op !== '&' ){ + if ( !$userId || ( $op !== '|' && $op !== '&' ) ) { return false; // sanity check } - if( !$dbw instanceof DatabaseBase ){ + if ( !$dbw instanceof DatabaseBase ) { $dbw = wfGetDB( DB_MASTER ); } @@ -127,4 +133,4 @@ class RevisionDeleteUser { public static function unsuppressUserName( $name, $userId, $dbw = null ) { return self::setUsernameBitfields( $name, $userId, '&', $dbw ); } -}
\ No newline at end of file +} diff --git a/includes/revisiondelete/RevisionDeleter.php b/includes/revisiondelete/RevisionDeleter.php index 59a9fa82..c59edc2a 100644 --- a/includes/revisiondelete/RevisionDeleter.php +++ b/includes/revisiondelete/RevisionDeleter.php @@ -2,12 +2,29 @@ /** * Revision/log/file deletion backend * + * 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 RevisionDelete */ /** * Temporary b/c interface, collection of static functions. * @ingroup SpecialPage + * @ingroup RevisionDelete */ class RevisionDeleter { /** @@ -42,7 +59,7 @@ class RevisionDeleter { * * @param $n Integer: the new bitfield. * @param $o Integer: the old bitfield. - * @return An array as described above. + * @return array An array as described above. * @since 1.19 public */ public static function getChanges( $n, $o ) { @@ -107,69 +124,4 @@ class RevisionDeleter { return $timestamp; } - - /** - * Creates utility links for log entries. - * - * @param $title Title - * @param $paramArray Array - * @param $messages - * @return String - */ - public static function getLogLinks( $title, $paramArray, $messages ) { - global $wgLang; - - if ( count( $paramArray ) >= 2 ) { - // Different revision types use different URL params... - $key = $paramArray[0]; - // $paramArray[1] is a CSV of the IDs - $Ids = explode( ',', $paramArray[1] ); - - $revert = array(); - - // Diff link for single rev deletions - if ( count( $Ids ) == 1 ) { - // Live revision diffs... - if ( in_array( $key, array( 'oldid', 'revision' ) ) ) { - $revert[] = Linker::linkKnown( - $title, - $messages['diff'], - array(), - array( - 'diff' => intval( $Ids[0] ), - 'unhide' => 1 - ) - ); - // Deleted revision diffs... - } elseif ( in_array( $key, array( 'artimestamp','archive' ) ) ) { - $revert[] = Linker::linkKnown( - SpecialPage::getTitleFor( 'Undelete' ), - $messages['diff'], - array(), - array( - 'target' => $title->getPrefixedDBKey(), - 'diff' => 'prev', - 'timestamp' => $Ids[0] - ) - ); - } - } - - // View/modify link... - $revert[] = Linker::linkKnown( - SpecialPage::getTitleFor( 'Revisiondelete' ), - $messages['revdel-restore'], - array(), - array( - 'target' => $title->getPrefixedText(), - 'type' => $key, - 'ids' => implode(',', $Ids), - ) - ); - - // Pipe links - return wfMsg( 'parentheses', $wgLang->pipeList( $revert ) ); - } - return ''; - } } diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index 2f7dfd7e..27a321ac 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -2,6 +2,21 @@ /** * Basic search engine * + * 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 Search */ @@ -65,6 +80,7 @@ class SearchEngine { /** * If this search backend can list/unlist redirects * @deprecated since 1.18 Call supports( 'list-redirects' ); + * @return bool */ function acceptListRedirects() { wfDeprecated( __METHOD__, '1.18' ); @@ -91,7 +107,7 @@ class SearchEngine { * @since 1.18 * @param $feature String * @param $data Mixed - * @return Noolean + * @return bool */ public function setFeatureData( $feature, $data ) { $this->features[$feature] = $data; @@ -147,6 +163,7 @@ class SearchEngine { /** * Really find the title match. + * @return null|\Title */ private static function getNearMatchInternal( $searchterm ) { global $wgContLang, $wgEnableSearchContributorsByIP; @@ -287,6 +304,7 @@ class SearchEngine { * or namespace names * * @param $query String + * @return string */ function replacePrefixes( $query ) { global $wgContLang; @@ -297,7 +315,7 @@ class SearchEngine { return $parsed; } - $allkeyword = wfMsgForContent( 'searchall' ) . ":"; + $allkeyword = wfMessage( 'searchall' )->inContentLanguage()->text() . ":"; if ( strncmp( $query, $allkeyword, strlen( $allkeyword ) ) == 0 ) { $this->namespaces = null; $parsed = substr( $query, strlen( $allkeyword ) ); @@ -391,6 +409,7 @@ class SearchEngine { * and preferences * * @param $namespaces Array + * @return array */ public static function namespacesAsText( $namespaces ) { global $wgContLang; @@ -398,7 +417,7 @@ class SearchEngine { $formatted = array_map( array( $wgContLang, 'getFormattedNsText' ), $namespaces ); foreach ( $formatted as $key => $ns ) { if ( empty( $ns ) ) - $formatted[$key] = wfMsg( 'blanknamespace' ); + $formatted[$key] = wfMessage( 'blanknamespace' )->text(); } return $formatted; } @@ -486,19 +505,6 @@ class SearchEngine { return $wgCanonicalServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns; } } - - /** - * Get internal MediaWiki Suggest template - * - * @return String - */ - public static function getMWSuggestTemplate() { - global $wgMWSuggestTemplate, $wgServer; - if ( $wgMWSuggestTemplate ) - return $wgMWSuggestTemplate; - else - return $wgServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace={namespaces}&suggest'; - } } /** @@ -737,7 +743,10 @@ class SearchResult { protected function initFromTitle( $title ) { $this->mTitle = $title; if ( !is_null( $this->mTitle ) ) { - $this->mRevision = Revision::newFromTitle( $this->mTitle ); + $id = false; + wfRunHooks( 'SearchResultInitFromTitle', array( $title, &$id ) ); + $this->mRevision = Revision::newFromTitle( + $this->mTitle, $id, Revision::READ_NORMAL ); if ( $this->mTitle->getNamespace() === NS_FILE ) $this->mImage = wfFindFile( $this->mTitle ); } @@ -771,7 +780,7 @@ class SearchResult { } /** - * @return Double or null if not supported + * @return float|null if not supported */ function getScore() { return null; @@ -1185,6 +1194,7 @@ class SearchHighlighter { * Do manual case conversion for non-ascii chars * * @param $matches Array + * @return string */ function caseCallback( $matches ) { global $wgContLang; @@ -1305,6 +1315,7 @@ class SearchHighlighter { /** * Basic wikitext removal * @protected + * @return mixed */ function removeWiki( $text ) { $fname = __METHOD__; diff --git a/includes/search/SearchIBM_DB2.php b/includes/search/SearchIBM_DB2.php index c02a009d..51ed000f 100644 --- a/includes/search/SearchIBM_DB2.php +++ b/includes/search/SearchIBM_DB2.php @@ -111,6 +111,7 @@ class SearchIBM_DB2 extends SearchEngine { * The guts shoulds be constructed in queryMain() * @param $filteredTerm String * @param $fulltext Boolean + * @return String */ function getQuery( $filteredTerm, $fulltext ) { return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' . @@ -145,7 +146,9 @@ class SearchIBM_DB2 extends SearchEngine { 'WHERE page_id=si_page AND ' . $match; } - /** @todo document */ + /** @todo document + * @return string + */ function parseQuery($filteredText, $fulltext) { global $wgContLang; $lc = SearchEngine::legalSearchChars(); diff --git a/includes/search/SearchMssql.php b/includes/search/SearchMssql.php index ebf19d3a..69c92ba3 100644 --- a/includes/search/SearchMssql.php +++ b/includes/search/SearchMssql.php @@ -115,6 +115,7 @@ class SearchMssql extends SearchEngine { * * @param $filteredTerm String * @param $fulltext Boolean + * @return String */ function getQuery( $filteredTerm, $fulltext ) { return $this->queryLimit( $this->queryMain( $filteredTerm, $fulltext ) . ' ' . @@ -151,7 +152,9 @@ class SearchMssql extends SearchEngine { 'WHERE page_id=ftindex.[KEY] '; } - /** @todo document */ + /** @todo document + * @return string + */ function parseQuery( $filteredText, $fulltext ) { global $wgContLang; $lc = SearchEngine::legalSearchChars(); @@ -189,6 +192,7 @@ class SearchMssql extends SearchEngine { * @param $id Integer * @param $title String * @param $text String + * @return bool|\ResultWrapper */ function update( $id, $title, $text ) { // We store the column data as UTF-8 byte order marked binary stream @@ -211,6 +215,7 @@ class SearchMssql extends SearchEngine { * * @param $id Integer * @param $title String + * @return bool|\ResultWrapper */ function updateTitle( $id, $title ) { $table = $this->db->tableName( 'searchindex' ); diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php index af8f3875..5cee03e0 100644 --- a/includes/search/SearchMySQL.php +++ b/includes/search/SearchMySQL.php @@ -290,7 +290,7 @@ class SearchMySQL extends SearchEngine { /** * Get the base part of the search query. * - * @param &$query Search query array + * @param &$query array Search query array * @param $filteredTerm String * @param $fulltext Boolean * @since 1.18 (changed) @@ -308,6 +308,7 @@ class SearchMySQL extends SearchEngine { /** * @since 1.18 (changed) + * @return array */ function getCountQuery( $filteredTerm, $fulltext ) { $match = $this->parseQuery( $filteredTerm, $fulltext ); @@ -365,6 +366,7 @@ class SearchMySQL extends SearchEngine { /** * Converts some characters for MySQL's indexing to grok it correctly, * and pads short words to overcome limitations. + * @return mixed|string */ function normalizeText( $string ) { global $wgContLang; @@ -412,6 +414,7 @@ class SearchMySQL extends SearchEngine { * Armor a case-folded UTF-8 string to get through MySQL's * fulltext search without being mucked up by funny charset * settings or anything else of the sort. + * @return string */ protected function stripForSearchCallback( $matches ) { return 'u8' . bin2hex( $matches[1] ); diff --git a/includes/search/SearchOracle.php b/includes/search/SearchOracle.php index 2d6fc3e2..a2db52f3 100644 --- a/includes/search/SearchOracle.php +++ b/includes/search/SearchOracle.php @@ -124,7 +124,7 @@ class SearchOracle extends SearchEngine { /** * Return a LIMIT clause to limit results on the query. * - * @param string + * @param $sql string * * @return String */ @@ -147,6 +147,7 @@ class SearchOracle extends SearchEngine { * The guts shoulds be constructed in queryMain() * @param $filteredTerm String * @param $fulltext Boolean + * @return String */ function getQuery( $filteredTerm, $fulltext ) { return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' . @@ -184,6 +185,7 @@ class SearchOracle extends SearchEngine { /** * Parse a user input search string, and return an SQL fragment to be used * as part of a WHERE clause + * @return string */ function parseQuery($filteredText, $fulltext) { global $wgContLang; diff --git a/includes/search/SearchPostgres.php b/includes/search/SearchPostgres.php index cfe283b2..68648894 100644 --- a/includes/search/SearchPostgres.php +++ b/includes/search/SearchPostgres.php @@ -146,6 +146,7 @@ class SearchPostgres extends SearchEngine { * @param $term String * @param $fulltext String * @param $colname + * @return string */ function searchQuery( $term, $fulltext, $colname ) { # Get the SQL fragment for the given term diff --git a/includes/search/SearchSqlite.php b/includes/search/SearchSqlite.php index cd59eea9..e52e4fe3 100644 --- a/includes/search/SearchSqlite.php +++ b/includes/search/SearchSqlite.php @@ -238,6 +238,7 @@ class SearchSqlite extends SearchEngine { * The guts shoulds be constructed in queryMain() * @param $filteredTerm String * @param $fulltext Boolean + * @return String */ function getQuery( $filteredTerm, $fulltext ) { return $this->limitResult( diff --git a/includes/search/SearchUpdate.php b/includes/search/SearchUpdate.php index a162d2b3..40dd36c2 100644 --- a/includes/search/SearchUpdate.php +++ b/includes/search/SearchUpdate.php @@ -4,6 +4,21 @@ * * See deferred.txt * + * 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 Search */ diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index 617a8026..c5aa2389 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -40,7 +40,12 @@ class ActiveUsersPager extends UsersPager { /** * @var Array */ - protected $groups; + protected $hideGroups = array(); + + /** + * @var Array + */ + protected $hideRights = array(); /** * @param $context IContextSource @@ -73,12 +78,11 @@ class ActiveUsersPager extends UsersPager { $this->opts->fetchValuesFromRequest( $this->getRequest() ); - $this->groups = array(); if ( $this->opts->getValue( 'hidebots' ) == 1 ) { - $this->groups['bot'] = true; + $this->hideRights[] = 'bot'; } if ( $this->opts->getValue( 'hidesysops' ) == 1 ) { - $this->groups['sysop'] = true; + $this->hideGroups[] = 'sysop'; } } @@ -90,8 +94,8 @@ class ActiveUsersPager extends UsersPager { $dbr = wfGetDB( DB_SLAVE ); $conds = array( 'rc_user > 0' ); // Users - no anons $conds[] = 'ipb_deleted IS NULL'; // don't show hidden names - $conds[] = "rc_log_type IS NULL OR rc_log_type != 'newusers'"; - $conds[] = "rc_timestamp >= '{$dbr->timestamp( wfTimestamp( TS_UNIX ) - $this->RCMaxAge*24*3600 )}'"; + $conds[] = 'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ); + $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $this->RCMaxAge*24*3600 ) ); if( $this->requestedUser != '' ) { $conds[] = 'rc_user_text >= ' . $dbr->addQuotes( $this->requestedUser ); @@ -99,19 +103,23 @@ class ActiveUsersPager extends UsersPager { $query = array( 'tables' => array( 'recentchanges', 'user', 'ipblocks' ), - 'fields' => array( 'rc_user_text AS user_name', // inheritance + 'fields' => array( 'user_name' => 'rc_user_text', // inheritance 'rc_user_text', // for Pager 'user_id', - 'COUNT(*) AS recentedits', - 'MAX(ipb_user) AS blocked' + 'recentedits' => 'COUNT(*)', + 'blocked' => 'MAX(ipb_user)' ), 'options' => array( - 'GROUP BY' => 'rc_user_text, user_id', + 'GROUP BY' => array( 'rc_user_text', 'user_id' ), 'USE INDEX' => array( 'recentchanges' => 'rc_user_text' ) ), 'join_conds' => array( 'user' => array( 'INNER JOIN', 'rc_user_text=user_name' ), - 'ipblocks' => array( 'LEFT JOIN', 'user_id=ipb_user AND ipb_auto=0 AND ipb_deleted=1' ), + 'ipblocks' => array( 'LEFT JOIN', array( + 'user_id=ipb_user', + 'ipb_auto' => 0, + 'ipb_deleted' => 1 + )), ), 'conds' => $conds ); @@ -127,12 +135,30 @@ class ActiveUsersPager extends UsersPager { $lang = $this->getLanguage(); $list = array(); - foreach( self::getGroups( $row->user_id ) as $group ) { - if ( isset( $this->groups[$group] ) ) { - return; + $user = User::newFromId( $row->user_id ); + + // User right filter + foreach( $this->hideRights as $right ) { + // Calling User::getRights() within the loop so that + // if the hideRights() filter is empty, we don't have to + // trigger the lazy-init of the big userrights array in the + // User object + if ( in_array( $right, $user->getRights() ) ) { + return ''; + } + } + + // User group filter + // Note: This is a different loop than for user rights, + // because we're reusing it to build the group links + // at the same time + foreach( $user->getGroups() as $group ) { + if ( in_array( $group, $this->hideGroups ) ) { + return ''; } $list[] = self::buildGroupLink( $group, $userName ); } + $groups = $lang->commaList( $list ); $item = $lang->specialList( $ulinks, $groups ); diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php index 2bfea4c3..fe9d41e5 100644 --- a/includes/specials/SpecialAllmessages.php +++ b/includes/specials/SpecialAllmessages.php @@ -146,8 +146,9 @@ class AllmessagesTablePager extends TablePager { function buildForm() { global $wgScript; - $languages = Language::getLanguageNames( false ); - ksort( $languages ); + $attrs = array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ); + $msg = wfMessage( 'allmessages-language' ); + $langSelect = Xml::languageSelector( $this->langcode, false, null, $attrs, $msg ); $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-allmessages-form' ) ) . Xml::fieldset( $this->msg( 'allmessages-filter-legend' )->text() ) . @@ -187,18 +188,8 @@ class AllmessagesTablePager extends TablePager { "</td>\n </tr> <tr>\n - <td class=\"mw-label\">" . - Xml::label( $this->msg( 'allmessages-language' )->text(), 'mw-allmessages-form-lang' ) . - "</td>\n - <td class=\"mw-input\">" . - Xml::openElement( 'select', array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ) ); - - foreach( $languages as $lang => $name ) { - $selected = $lang == $this->langcode; - $out .= Xml::option( $lang . ' - ' . $name, $lang, $selected ) . "\n"; - } - $out .= Xml::closeElement( 'select' ) . - "</td>\n + <td class=\"mw-label\">" . $langSelect[0] . "</td>\n + <td class=\"mw-input\">" . $langSelect[1] . "</td>\n </tr>" . '<tr> @@ -290,6 +281,7 @@ class AllmessagesTablePager extends TablePager { /** * This function normally does a database query to get the results; we need * to make a pretend result using a FakeResultWrapper. + * @return FakeResultWrapper */ function reallyDoQuery( $offset, $limit, $descending ) { $result = new FakeResultWrapper( array() ); @@ -369,7 +361,7 @@ class AllmessagesTablePager extends TablePager { array( 'broken' ) ); } - return $title . ' (' . $talk . ')'; + return $title . ' ' . $this->msg( 'parentheses' )->rawParams( $talk )->escaped(); case 'am_default' : case 'am_actual' : diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php index 960a327a..0f8b2557 100644 --- a/includes/specials/SpecialAllpages.php +++ b/includes/specials/SpecialAllpages.php @@ -30,24 +30,37 @@ class SpecialAllpages extends IncludableSpecialPage { /** * Maximum number of pages to show on single subpage. + * + * @var int $maxPerPage */ protected $maxPerPage = 345; /** * Maximum number of pages to show on single index subpage. + * + * @var int $maxLineCount */ protected $maxLineCount = 100; /** * Maximum number of chars to show for an entry. + * + * @var int $maxPageLength */ protected $maxPageLength = 70; /** * Determines, which message describes the input field 'nsfrom'. + * + * @var string $nsfromMsg */ protected $nsfromMsg = 'allpagesfrom'; + /** + * Constructor + * + * @param $name string: name of the special page, as seen in links and URLs (default: 'Allpages') + */ function __construct( $name = 'Allpages' ){ parent::__construct( $name ); } @@ -70,6 +83,7 @@ class SpecialAllpages extends IncludableSpecialPage { $from = $request->getVal( 'from', null ); $to = $request->getVal( 'to', null ); $namespace = $request->getInt( 'namespace' ); + $hideredirects = $request->getBool( 'hideredirects', false ); $namespaces = $wgContLang->getNamespaces(); @@ -81,11 +95,11 @@ class SpecialAllpages extends IncludableSpecialPage { $out->addModuleStyles( 'mediawiki.special' ); if( $par !== null ) { - $this->showChunk( $namespace, $par, $to ); + $this->showChunk( $namespace, $par, $to, $hideredirects ); } elseif( $from !== null && $to === null ) { - $this->showChunk( $namespace, $from, $to ); + $this->showChunk( $namespace, $from, $to, $hideredirects ); } else { - $this->showToplevel( $namespace, $from, $to ); + $this->showToplevel( $namespace, $from, $to, $hideredirects ); } } @@ -95,8 +109,10 @@ class SpecialAllpages extends IncludableSpecialPage { * @param $namespace Integer: a namespace constant (default NS_MAIN). * @param $from String: dbKey we are starting listing at. * @param $to String: dbKey we are ending listing at. + * @param $hideredirects Bool: dont show redirects (default FALSE) + * @return string */ - function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '' ) { + function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) { global $wgScript; $t = $this->getTitle(); @@ -131,6 +147,12 @@ class SpecialAllpages extends IncludableSpecialPage { array( 'selected' => $namespace ), array( 'name' => 'namespace', 'id' => 'namespace' ) ) . ' ' . + Xml::checkLabel( + $this->msg( 'allpages-hide-redirects' )->text(), + 'hideredirects', + 'hideredirects', + $hideredirects + ) . ' ' . Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . " </td> </tr>"; @@ -145,8 +167,9 @@ class SpecialAllpages extends IncludableSpecialPage { * @param $namespace Integer (default NS_MAIN) * @param $from String: list all pages from this name * @param $to String: list all pages to this name + * @param $hideredirects Bool: dont show redirects (default FALSE) */ - function showToplevel( $namespace = NS_MAIN, $from = '', $to = '' ) { + function showToplevel( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) { $output = $this->getOutput(); # TODO: Either make this *much* faster or cache the title index points @@ -156,6 +179,10 @@ class SpecialAllpages extends IncludableSpecialPage { $out = ""; $where = array( 'page_namespace' => $namespace ); + if ( $hideredirects ) { + $where[ 'page_is_redirect' ] = 0; + } + $from = Title::makeTitleSafe( $namespace, $from ); $to = Title::makeTitleSafe( $namespace, $to ); $from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null; @@ -224,9 +251,9 @@ class SpecialAllpages extends IncludableSpecialPage { // Instead, display the first section directly. if( count( $lines ) <= 2 ) { if( !empty($lines) ) { - $this->showChunk( $namespace, $from, $to ); + $this->showChunk( $namespace, $from, $to, $hideredirects ); } else { - $output->addHTML( $this->namespaceForm( $namespace, $from, $to ) ); + $output->addHTML( $this->namespaceForm( $namespace, $from, $to, $hideredirects ) ); } return; } @@ -236,10 +263,10 @@ class SpecialAllpages extends IncludableSpecialPage { while( count ( $lines ) > 0 ) { $inpoint = array_shift( $lines ); $outpoint = array_shift( $lines ); - $out .= $this->showline( $inpoint, $outpoint, $namespace ); + $out .= $this->showline( $inpoint, $outpoint, $namespace, $hideredirects ); } $out .= Xml::closeElement( 'table' ); - $nsForm = $this->namespaceForm( $namespace, $from, $to ); + $nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects ); # Is there more? if( $this->including() ) { @@ -270,8 +297,10 @@ class SpecialAllpages extends IncludableSpecialPage { * @param $inpoint String: lower limit of pagenames * @param $outpoint String: upper limit of pagenames * @param $namespace Integer (Default NS_MAIN) + * @param $hideredirects Bool: dont show redirects (default FALSE) + * @return string */ - function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { + function showline( $inpoint, $outpoint, $namespace = NS_MAIN, $hideredirects ) { global $wgContLang; $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) ); $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) ); @@ -280,8 +309,14 @@ class SpecialAllpages extends IncludableSpecialPage { $outpointf = $wgContLang->truncate( $outpointf, $this->maxPageLength ); $queryparams = $namespace ? "namespace=$namespace&" : ''; + + $queryhideredirects = array(); + if ($hideredirects) { + $queryhideredirects[ 'hideredirects' ] = 1; + } + $special = $this->getTitle(); - $link = htmlspecialchars( $special->getLocalUrl( $queryparams . 'from=' . urlencode($inpoint) . '&to=' . urlencode($outpoint) ) ); + $link = htmlspecialchars( $special->getLocalUrl( $queryparams . 'from=' . urlencode($inpoint) . '&to=' . urlencode($outpoint), $queryhideredirects ) ); $out = $this->msg( 'alphaindexline' )->rawParams( "<a href=\"$link\">$inpointf</a></td><td>", @@ -294,8 +329,9 @@ class SpecialAllpages extends IncludableSpecialPage { * @param $namespace Integer (Default NS_MAIN) * @param $from String: list all pages from this name (default FALSE) * @param $to String: list all pages to this name (default FALSE) + * @param $hideredirects Bool: dont show redirects (default FALSE) */ - function showChunk( $namespace = NS_MAIN, $from = false, $to = false ) { + function showChunk( $namespace = NS_MAIN, $from = false, $to = false, $hideredirects = false ) { global $wgContLang; $output = $this->getOutput(); @@ -319,6 +355,11 @@ class SpecialAllpages extends IncludableSpecialPage { 'page_namespace' => $namespace, 'page_title >= ' . $dbr->addQuotes( $fromKey ) ); + + if ( $hideredirects ) { + $conds[ 'page_is_redirect' ] = 0; + } + if( $toKey !== "" ) { $conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey ); } @@ -406,7 +447,7 @@ class SpecialAllpages extends IncludableSpecialPage { $self = $this->getTitle(); - $nsForm = $this->namespaceForm( $namespace, $from, $to ); + $nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects ); $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ). '<tr> <td>' . @@ -422,6 +463,9 @@ class SpecialAllpages extends IncludableSpecialPage { if( $namespace ) $query['namespace'] = $namespace; + if( $hideredirects ) + $query['hideredirects'] = $hideredirects; + $prevLink = Linker::linkKnown( $self, $this->msg( 'prevpage', $pt )->escaped(), @@ -433,12 +477,15 @@ class SpecialAllpages extends IncludableSpecialPage { if( $n == $this->maxPerPage && $s = $res->fetchObject() ) { # $s is the first link of the next chunk - $t = Title::MakeTitle($namespace, $s->page_title); + $t = Title::makeTitle($namespace, $s->page_title); $query = array( 'from' => $t->getText() ); if( $namespace ) $query['namespace'] = $namespace; + if( $hideredirects ) + $query['hideredirects'] = $hideredirects; + $nextLink = Linker::linkKnown( $self, $this->msg( 'nextpage', $t->getText() )->escaped(), diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php index 1203e1fd..6e3d49bd 100644 --- a/includes/specials/SpecialAncientpages.php +++ b/includes/specials/SpecialAncientpages.php @@ -41,9 +41,9 @@ class AncientPagesPage extends QueryPage { function getQueryInfo() { return array( 'tables' => array( 'page', 'revision' ), - 'fields' => array( 'page_namespace AS namespace', - 'page_title AS title', - 'rev_timestamp AS value' ), + 'fields' => array( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'rev_timestamp' ), 'conds' => array( 'page_namespace' => MWNamespace::getContentNamespaces(), 'page_is_redirect' => 0, 'page_latest=rev_id' ) diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index da8eed1b..1d6656ab 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -106,9 +106,9 @@ class SpecialBlock extends FormSpecialPage { $form->setSubmitTextMsg( $msg ); # Don't need to do anything if the form has been posted - if( !$this->getRequest()->wasPosted() && $this->preErrors ){ + if ( !$this->getRequest()->wasPosted() && $this->preErrors ) { $s = HTMLForm::formatErrors( $this->preErrors ); - if( $s ){ + if ( $s ) { $form->addHeaderText( Html::rawElement( 'div', array( 'class' => 'error' ), @@ -122,7 +122,7 @@ class SpecialBlock extends FormSpecialPage { * Get the HTMLForm descriptor array for the block form * @return Array */ - protected function getFormFields(){ + protected function getFormFields() { global $wgBlockAllowsUTEdit; $user = $this->getUser(); @@ -144,6 +144,7 @@ class SpecialBlock extends FormSpecialPage { 'tabindex' => '2', 'options' => self::getSuggestedDurations(), 'other' => $this->msg( 'ipbother' )->text(), + 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(), ), 'Reason' => array( 'type' => 'selectandother', @@ -157,14 +158,14 @@ class SpecialBlock extends FormSpecialPage { ), ); - if( self::canBlockEmail( $user ) ) { + if ( self::canBlockEmail( $user ) ) { $a['DisableEmail'] = array( 'type' => 'check', 'label-message' => 'ipbemailban', ); } - if( $wgBlockAllowsUTEdit ){ + if ( $wgBlockAllowsUTEdit ) { $a['DisableUTEdit'] = array( 'type' => 'check', 'label-message' => 'ipb-disableusertalk', @@ -179,7 +180,7 @@ class SpecialBlock extends FormSpecialPage { ); # Allow some users to hide name from block log, blocklist and listusers - if( $user->isAllowed( 'hideuser' ) ) { + if ( $user->isAllowed( 'hideuser' ) ) { $a['HideUser'] = array( 'type' => 'check', 'label-message' => 'ipbhidename', @@ -188,7 +189,7 @@ class SpecialBlock extends FormSpecialPage { } # Watchlist their user page? (Only if user is logged in) - if( $user->isLoggedIn() ) { + if ( $user->isLoggedIn() ) { $a['Watch'] = array( 'type' => 'check', 'label-message' => 'ipbwatchuser', @@ -227,7 +228,7 @@ class SpecialBlock extends FormSpecialPage { * @return Bool whether fields were altered (that is, whether the target is * already blocked) */ - protected function maybeAlterFormDefaults( &$fields ){ + protected function maybeAlterFormDefaults( &$fields ) { # This will be overwritten by request data $fields['Target']['default'] = (string)$this->target; @@ -236,7 +237,7 @@ class SpecialBlock extends FormSpecialPage { $block = Block::newFromTarget( $this->target ); - if( $block instanceof Block && !$block->mAuto # The block exists and isn't an autoblock + if ( $block instanceof Block && !$block->mAuto # The block exists and isn't an autoblock && ( $this->type != Block::TYPE_RANGE # The block isn't a rangeblock || $block->getTarget() == $this->target ) # or if it is, the range is what we're about to block ) @@ -245,15 +246,15 @@ class SpecialBlock extends FormSpecialPage { $fields['CreateAccount']['default'] = $block->prevents( 'createaccount' ); $fields['AutoBlock']['default'] = $block->isAutoblocking(); - if( isset( $fields['DisableEmail'] ) ){ + if ( isset( $fields['DisableEmail'] ) ) { $fields['DisableEmail']['default'] = $block->prevents( 'sendemail' ); } - if( isset( $fields['HideUser'] ) ){ + if ( isset( $fields['HideUser'] ) ) { $fields['HideUser']['default'] = $block->mHideName; } - if( isset( $fields['DisableUTEdit'] ) ){ + if ( isset( $fields['DisableUTEdit'] ) ) { $fields['DisableUTEdit']['default'] = $block->prevents( 'editownusertalk' ); } @@ -265,7 +266,7 @@ class SpecialBlock extends FormSpecialPage { $fields['Reason']['default'] = ''; } - if( $this->getRequest()->wasPosted() ){ + if ( $this->getRequest()->wasPosted() ) { # Ok, so we got a POST submission asking us to reblock a user. So show the # confirm checkbox; the user will only see it if they haven't previously $fields['Confirm']['type'] = 'check'; @@ -276,25 +277,25 @@ class SpecialBlock extends FormSpecialPage { $fields['Confirm']['default'] = 1; } - if( $block->mExpiry == 'infinity' ) { - $fields['Expiry']['default'] = 'indefinite'; + if ( $block->mExpiry == 'infinity' ) { + $fields['Expiry']['default'] = 'infinite'; } else { $fields['Expiry']['default'] = wfTimestamp( TS_RFC2822, $block->mExpiry ); } $this->alreadyBlocked = true; - $this->preErrors[] = array( 'ipb-needreblock', (string)$block->getTarget() ); + $this->preErrors[] = array( 'ipb-needreblock', wfEscapeWikiText( (string)$block->getTarget() ) ); } # We always need confirmation to do HideUser - if( $this->requestedHideUser ){ + if ( $this->requestedHideUser ) { $fields['Confirm']['type'] = 'check'; unset( $fields['Confirm']['default'] ); $this->preErrors[] = 'ipb-confirmhideuser'; } # Or if the user is trying to block themselves - if( (string)$this->target === $this->getUser()->getName() ){ + if ( (string)$this->target === $this->getUser()->getName() ) { $fields['Confirm']['type'] = 'check'; unset( $fields['Confirm']['default'] ); $this->preErrors[] = 'ipb-blockingself'; @@ -303,16 +304,19 @@ class SpecialBlock extends FormSpecialPage { /** * Add header elements like block log entries, etc. + * @return String */ - protected function preText(){ + protected function preText() { + $this->getOutput()->addModules( 'mediawiki.special.block' ); + $text = $this->msg( 'blockiptext' )->parse(); $otherBlockMessages = array(); - if( $this->target !== null ) { + if ( $this->target !== null ) { # Get other blocks, i.e. from GlobalBlocking or TorBlock extension wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockMessages, $this->target ) ); - if( count( $otherBlockMessages ) ) { + if ( count( $otherBlockMessages ) ) { $s = Html::rawElement( 'h2', array(), @@ -321,7 +325,7 @@ class SpecialBlock extends FormSpecialPage { $list = ''; - foreach( $otherBlockMessages as $link ) { + foreach ( $otherBlockMessages as $link ) { $list .= Html::rawElement( 'li', array(), $link ) . "\n"; } @@ -342,9 +346,11 @@ class SpecialBlock extends FormSpecialPage { * Add footer elements to the form * @return string */ - protected function postText(){ + protected function postText() { + $links = array(); + # Link to the user's contributions, if applicable - if( $this->target instanceof User ){ + if ( $this->target instanceof User ) { $contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->target->getName() ); $links[] = Linker::link( $contribsPage, @@ -353,8 +359,8 @@ class SpecialBlock extends FormSpecialPage { } # Link to unblock the specified user, or to a blank unblock form - if( $this->target instanceof User ) { - $message = $this->msg( 'ipb-unblock-addr', $this->target->getName() )->parse(); + if ( $this->target instanceof User ) { + $message = $this->msg( 'ipb-unblock-addr', wfEscapeWikiText( $this->target->getName() ) )->parse(); $list = SpecialPage::getTitleFor( 'Unblock', $this->target->getName() ); } else { $message = $this->msg( 'ipb-unblock' )->parse(); @@ -386,35 +392,35 @@ class SpecialBlock extends FormSpecialPage { $this->getLanguage()->pipeList( $links ) ); - if( $this->target instanceof User ){ + $userTitle = self::getTargetUserTitle( $this->target ); + if ( $userTitle ) { # Get relevant extracts from the block and suppression logs, if possible - $userpage = $this->target->getUserPage(); $out = ''; LogEventsList::showLogExtract( $out, 'block', - $userpage, + $userTitle, '', array( 'lim' => 10, - 'msgKey' => array( 'blocklog-showlog', $userpage->getText() ), + 'msgKey' => array( 'blocklog-showlog', $userTitle->getText() ), 'showIfEmpty' => false ) ); $text .= $out; # Add suppression block entries if allowed - if( $user->isAllowed( 'suppressionlog' ) ) { + if ( $user->isAllowed( 'suppressionlog' ) ) { LogEventsList::showLogExtract( $out, 'suppress', - $userpage, + $userTitle, '', array( 'lim' => 10, 'conds' => array( 'log_action' => array( 'block', 'reblock', 'unblock' ) ), - 'msgKey' => array( 'blocklog-showsuppresslog', $userpage->getText() ), + 'msgKey' => array( 'blocklog-showsuppresslog', $userTitle->getText() ), 'showIfEmpty' => false ) ); @@ -427,6 +433,21 @@ class SpecialBlock extends FormSpecialPage { } /** + * Get a user page target for things like logs. + * This handles account and IP range targets. + * @param $target User|string + * @return Title|null + */ + protected static function getTargetUserTitle( $target ) { + if ( $target instanceof User ) { + return $target->getUserPage(); + } elseif ( IP::isIPAddress( $target ) ) { + return Title::makeTitleSafe( NS_USER, $target ); + } + return null; + } + + /** * Determine the target of the block, and the type of target * TODO: should be in Block.php? * @param $par String subpage parameter passed to setup, or data value from @@ -434,18 +455,18 @@ class SpecialBlock extends FormSpecialPage { * @param $request WebRequest optionally try and get data from a request too * @return array( User|string|null, Block::TYPE_ constant|null ) */ - public static function getTargetAndType( $par, WebRequest $request = null ){ + public static function getTargetAndType( $par, WebRequest $request = null ) { $i = 0; $target = null; - while( true ){ - switch( $i++ ){ + while( true ) { + switch( $i++ ) { case 0: # The HTMLForm will check wpTarget first and only if it doesn't get # a value use the default, which will be generated from the options # below; so this has to have a higher precedence here than $par, or # we could end up with different values in $this->target and the HTMLForm! - if( $request instanceof WebRequest ){ + if ( $request instanceof WebRequest ) { $target = $request->getText( 'wpTarget', null ); } break; @@ -453,13 +474,13 @@ class SpecialBlock extends FormSpecialPage { $target = $par; break; case 2: - if( $request instanceof WebRequest ){ + if ( $request instanceof WebRequest ) { $target = $request->getText( 'ip', null ); } break; case 3: # B/C @since 1.18 - if( $request instanceof WebRequest ){ + if ( $request instanceof WebRequest ) { $target = $request->getText( 'wpBlockAddress', null ); } break; @@ -469,7 +490,7 @@ class SpecialBlock extends FormSpecialPage { list( $target, $type ) = Block::parseTarget( $target ); - if( $type !== null ){ + if ( $type !== null ) { return array( $target, $type ); } } @@ -490,9 +511,9 @@ class SpecialBlock extends FormSpecialPage { list( $target, $type ) = self::getTargetAndType( $value ); - if( $type == Block::TYPE_USER ){ + if ( $type == Block::TYPE_USER ) { # TODO: why do we not have a User->exists() method? - if( !$target->getId() ){ + if ( !$target->getId() ) { return $form->msg( 'nosuchusershort', wfEscapeWikiText( $target->getName() ) ); } @@ -502,31 +523,31 @@ class SpecialBlock extends FormSpecialPage { return $form->msg( 'badaccess', $status ); } - } elseif( $type == Block::TYPE_RANGE ){ + } elseif ( $type == Block::TYPE_RANGE ) { list( $ip, $range ) = explode( '/', $target, 2 ); - if( ( IP::isIPv4( $ip ) && $wgBlockCIDRLimit['IPv4'] == 32 ) + if ( ( IP::isIPv4( $ip ) && $wgBlockCIDRLimit['IPv4'] == 32 ) || ( IP::isIPv6( $ip ) && $wgBlockCIDRLimit['IPv6'] == 128 ) ) { # Range block effectively disabled return $form->msg( 'range_block_disabled' ); } - if( ( IP::isIPv4( $ip ) && $range > 32 ) + if ( ( IP::isIPv4( $ip ) && $range > 32 ) || ( IP::isIPv6( $ip ) && $range > 128 ) ) { # Dodgy range return $form->msg( 'ip_range_invalid' ); } - if( IP::isIPv4( $ip ) && $range < $wgBlockCIDRLimit['IPv4'] ) { + if ( IP::isIPv4( $ip ) && $range < $wgBlockCIDRLimit['IPv4'] ) { return $form->msg( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv4'] ); } - if( IP::isIPv6( $ip ) && $range < $wgBlockCIDRLimit['IPv6'] ) { + if ( IP::isIPv6( $ip ) && $range < $wgBlockCIDRLimit['IPv6'] ) { return $form->msg( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv6'] ); } - } elseif( $type == Block::TYPE_IP ){ + } elseif ( $type == Block::TYPE_IP ) { # All is well } else { return $form->msg( 'badipaddress' ); @@ -551,7 +572,7 @@ class SpecialBlock extends FormSpecialPage { * @param $context IContextSource * @return Bool|String */ - public static function processForm( array $data, IContextSource $context ){ + public static function processForm( array $data, IContextSource $context ) { global $wgBlockAllowsUTEdit; $performer = $context->getUser(); @@ -564,7 +585,7 @@ class SpecialBlock extends FormSpecialPage { $data['Confirm'] = !in_array( $data['Confirm'], array( '', '0', null, false ), true ); list( $target, $type ) = self::getTargetAndType( $data['Target'] ); - if( $type == Block::TYPE_USER ){ + if ( $type == Block::TYPE_USER ) { $user = $target; $target = $user->getName(); $userId = $user->getId(); @@ -576,14 +597,14 @@ class SpecialBlock extends FormSpecialPage { # since both $data['PreviousTarget'] and $target are normalized # but $data['target'] gets overriden by (non-normalized) request variable # from previous request. - if( $target === $performer->getName() && + if ( $target === $performer->getName() && ( $data['PreviousTarget'] !== $target || !$data['Confirm'] ) ) { return array( 'ipb-blockingself' ); } - } elseif( $type == Block::TYPE_RANGE ){ + } elseif ( $type == Block::TYPE_RANGE ) { $userId = 0; - } elseif( $type == Block::TYPE_IP ){ + } elseif ( $type == Block::TYPE_IP ) { $target = $target->getName(); $userId = 0; } else { @@ -591,24 +612,24 @@ class SpecialBlock extends FormSpecialPage { return array( 'badipaddress' ); } - if( ( strlen( $data['Expiry'] ) == 0) || ( strlen( $data['Expiry'] ) > 50 ) + if ( ( strlen( $data['Expiry'] ) == 0) || ( strlen( $data['Expiry'] ) > 50 ) || !self::parseExpiryInput( $data['Expiry'] ) ) { return array( 'ipb_expiry_invalid' ); } - if( !isset( $data['DisableEmail'] ) ){ + if ( !isset( $data['DisableEmail'] ) ) { $data['DisableEmail'] = false; } # If the user has done the form 'properly', they won't even have been given the # option to suppress-block unless they have the 'hideuser' permission - if( !isset( $data['HideUser'] ) ){ + if ( !isset( $data['HideUser'] ) ) { $data['HideUser'] = false; } - if( $data['HideUser'] ) { - if( !$performer->isAllowed('hideuser') ){ + if ( $data['HideUser'] ) { + if ( !$performer->isAllowed('hideuser') ) { # this codepath is unreachable except by a malicious user spoofing forms, # or by race conditions (user has oversight and sysop, loads block form, # and is de-oversighted before submission); so need to fail completely @@ -617,16 +638,16 @@ class SpecialBlock extends FormSpecialPage { } # Recheck params here... - if( $type != Block::TYPE_USER ) { + if ( $type != Block::TYPE_USER ) { $data['HideUser'] = false; # IP users should not be hidden - } elseif( !in_array( $data['Expiry'], array( 'infinite', 'infinity', 'indefinite' ) ) ) { + } elseif ( !in_array( $data['Expiry'], array( 'infinite', 'infinity', 'indefinite' ) ) ) { # Bad expiry. return array( 'ipb_expiry_temp' ); - } elseif( $user->getEditCount() > self::HIDEUSER_CONTRIBLIMIT ) { + } elseif ( $user->getEditCount() > self::HIDEUSER_CONTRIBLIMIT ) { # Typically, the user should have a handful of edits. # Disallow hiding users with many edits for performance. return array( 'ipb_hide_invalid' ); - } elseif( !$data['Confirm'] ){ + } elseif ( !$data['Confirm'] ) { return array( 'ipb-confirmhideuser' ); } } @@ -644,15 +665,15 @@ class SpecialBlock extends FormSpecialPage { $block->isAutoblocking( $data['AutoBlock'] ); $block->mHideName = $data['HideUser']; - if( !wfRunHooks( 'BlockIp', array( &$block, &$performer ) ) ) { + if ( !wfRunHooks( 'BlockIp', array( &$block, &$performer ) ) ) { return array( 'hookaborted' ); } # Try to insert block. Is there a conflicting block? $status = $block->insert(); - if( !$status ) { + if ( !$status ) { # Show form unless the user is already aware of this... - if( !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data ) + if ( !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data ) && $data['PreviousTarget'] !== $target ) ) { return array( array( 'ipb_already_blocked', $block->getTarget() ) ); @@ -662,13 +683,13 @@ class SpecialBlock extends FormSpecialPage { # be sure the user is blocked by now it should work for our purposes $currentBlock = Block::newFromTarget( $target ); - if( $block->equals( $currentBlock ) ) { + if ( $block->equals( $currentBlock ) ) { return array( array( 'ipb_already_blocked', $block->getTarget() ) ); } # If the name was hidden and the blocking user cannot hide # names, then don't allow any block changes... - if( $currentBlock->mHideName && !$performer->isAllowed( 'hideuser' ) ) { + if ( $currentBlock->mHideName && !$performer->isAllowed( 'hideuser' ) ) { return array( 'cant-see-hidden-user' ); } @@ -677,12 +698,12 @@ class SpecialBlock extends FormSpecialPage { $logaction = 'reblock'; # Unset _deleted fields if requested - if( $currentBlock->mHideName && !$data['HideUser'] ) { + if ( $currentBlock->mHideName && !$data['HideUser'] ) { RevisionDeleteUser::unsuppressUserName( $target, $userId ); } # If hiding/unhiding a name, this should go in the private logs - if( (bool)$currentBlock->mHideName ){ + if ( (bool)$currentBlock->mHideName ) { $data['HideUser'] = true; } } @@ -693,12 +714,12 @@ class SpecialBlock extends FormSpecialPage { wfRunHooks( 'BlockIpComplete', array( $block, $performer ) ); # Set *_deleted fields if requested - if( $data['HideUser'] ) { + if ( $data['HideUser'] ) { RevisionDeleteUser::suppressUserName( $target, $userId ); } # Can't watch a rangeblock - if( $type != Block::TYPE_RANGE && $data['Watch'] ) { + if ( $type != Block::TYPE_RANGE && $data['Watch'] ) { $performer->addWatch( Title::makeTitle( NS_USER, $target ) ); } @@ -736,18 +757,18 @@ class SpecialBlock extends FormSpecialPage { * the wiki's content language * @return Array */ - public static function getSuggestedDurations( $lang = null ){ + public static function getSuggestedDurations( $lang = null ) { $a = array(); $msg = $lang === null ? wfMessage( 'ipboptions' )->inContentLanguage()->text() : wfMessage( 'ipboptions' )->inLanguage( $lang )->text(); - if( $msg == '-' ){ + if ( $msg == '-' ) { return array(); } - foreach( explode( ',', $msg ) as $option ) { - if( strpos( $option, ':' ) === false ){ + foreach ( explode( ',', $msg ) as $option ) { + if ( strpos( $option, ':' ) === false ) { $option = "$option:$option"; } @@ -766,7 +787,7 @@ class SpecialBlock extends FormSpecialPage { */ public static function parseExpiryInput( $expiry ) { static $infinity; - if( $infinity == null ){ + if ( $infinity == null ) { $infinity = wfGetDB( DB_SLAVE )->getInfinity(); } @@ -811,8 +832,8 @@ class SpecialBlock extends FormSpecialPage { $user = User::newFromName( $user ); } - if( $performer->isBlocked() ){ - if( $user instanceof User && $user->getId() == $performer->getId() ) { + if ( $performer->isBlocked() ) { + if ( $user instanceof User && $user->getId() == $performer->getId() ) { # User is trying to unblock themselves if ( $performer->isAllowed( 'unblockself' ) ) { return true; @@ -836,40 +857,41 @@ class SpecialBlock extends FormSpecialPage { * reader for this block, to provide more information in the logs * @param $data Array from HTMLForm data * @param $type Block::TYPE_ constant (USER, RANGE, or IP) - * @return array + * @return string */ protected static function blockLogFlags( array $data, $type ) { global $wgBlockAllowsUTEdit; $flags = array(); - # when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log - if( !$data['HardBlock'] && $type != Block::TYPE_USER ){ + # when blocking a user the option 'anononly' is not available/has no effect + # -> do not write this into log + if ( !$data['HardBlock'] && $type != Block::TYPE_USER ) { // For grepping: message block-log-flags-anononly $flags[] = 'anononly'; } - if( $data['CreateAccount'] ){ + if ( $data['CreateAccount'] ) { // For grepping: message block-log-flags-nocreate $flags[] = 'nocreate'; } # Same as anononly, this is not displayed when blocking an IP address - if( !$data['AutoBlock'] && $type == Block::TYPE_USER ){ + if ( !$data['AutoBlock'] && $type == Block::TYPE_USER ) { // For grepping: message block-log-flags-noautoblock $flags[] = 'noautoblock'; } - if( $data['DisableEmail'] ){ + if ( $data['DisableEmail'] ) { // For grepping: message block-log-flags-noemail $flags[] = 'noemail'; } - if( $wgBlockAllowsUTEdit && $data['DisableUTEdit'] ){ + if ( $wgBlockAllowsUTEdit && $data['DisableUTEdit'] ) { // For grepping: message block-log-flags-nousertalk $flags[] = 'nousertalk'; } - if( $data['HideUser'] ){ + if ( $data['HideUser'] ) { // For grepping: message block-log-flags-hiddenname $flags[] = 'hiddenname'; } @@ -894,7 +916,7 @@ class SpecialBlock extends FormSpecialPage { public function onSuccess() { $out = $this->getOutput(); $out->setPageTitle( $this->msg( 'blockipsuccesssub' ) ); - $out->addWikiMsg( 'blockipsuccesstext', $this->target ); + $out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( $this->target ) ); } } diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index 0a3a28fe..7143d5bc 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -372,7 +372,7 @@ class BlockListPager extends TablePager { 'ipb_user', 'ipb_by', 'ipb_by_text', - 'user_name AS by_user_name', + 'by_user_name' => 'user_name', 'ipb_reason', 'ipb_timestamp', 'ipb_auto', diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php index 48ca4f05..bf7de3f5 100644 --- a/includes/specials/SpecialBooksources.php +++ b/includes/specials/SpecialBooksources.php @@ -46,7 +46,7 @@ class SpecialBookSources extends SpecialPage { /** * Show the special page * - * @param $isbn ISBN passed as a subpage parameter + * @param $isbn string ISBN passed as a subpage parameter */ public function execute( $isbn ) { $this->setHeaders(); @@ -63,7 +63,8 @@ class SpecialBookSources extends SpecialPage { /** * Returns whether a given ISBN (10 or 13) is valid. True indicates validity. - * @param isbn ISBN passed for check + * @param isbn string ISBN passed for check + * @return bool */ public static function isValidISBN( $isbn ) { $isbn = self::cleanIsbn( $isbn ); @@ -100,7 +101,7 @@ class SpecialBookSources extends SpecialPage { /** * Trim ISBN and remove characters which aren't required * - * @param $isbn Unclean ISBN + * @param $isbn string Unclean ISBN * @return string */ private static function cleanIsbn( $isbn ) { @@ -142,7 +143,7 @@ class SpecialBookSources extends SpecialPage { $page = $this->msg( 'booksources' )->inContentLanguage()->text(); $title = Title::makeTitleSafe( NS_PROJECT, $page ); # Show list in content language if( is_object( $title ) && $title->exists() ) { - $rev = Revision::newFromTitle( $title ); + $rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) ); return true; } @@ -160,8 +161,8 @@ class SpecialBookSources extends SpecialPage { /** * Format a book source list item * - * @param $label Book source label - * @param $url Book source URL + * @param $label string Book source label + * @param $url string Book source URL * @return string */ private function makeListItem( $label, $url ) { diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index b8dbe9e8..8119e6d1 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -27,7 +27,7 @@ * * @ingroup SpecialPage */ -class BrokenRedirectsPage extends PageQueryPage { +class BrokenRedirectsPage extends QueryPage { function __construct( $name = 'BrokenRedirects' ) { parent::__construct( $name ); @@ -45,9 +45,9 @@ class BrokenRedirectsPage extends PageQueryPage { return array( 'tables' => array( 'redirect', 'p1' => 'page', 'p2' => 'page' ), - 'fields' => array( 'p1.page_namespace AS namespace', - 'p1.page_title AS title', - 'p1.page_title AS value', + 'fields' => array( 'namespace' => 'p1.page_namespace', + 'title' => 'p1.page_title', + 'value' => 'p1.page_title', 'rd_namespace', 'rd_title' ), diff --git a/includes/specials/SpecialCachedPage.php b/includes/specials/SpecialCachedPage.php new file mode 100644 index 00000000..b3f6c720 --- /dev/null +++ b/includes/specials/SpecialCachedPage.php @@ -0,0 +1,198 @@ +<?php + +/** + * Abstract special page class with scaffolding for caching HTML and other values + * in a single blob. + * + * Before using any of the caching functionality, call startCache. + * After the last call to either getCachedValue or addCachedHTML, call saveCache. + * + * To get a cached value or compute it, use getCachedValue like this: + * $this->getCachedValue( $callback ); + * + * To add HTML that should be cached, use addCachedHTML like this: + * $this->addCachedHTML( $callback ); + * + * The callback function is only called when needed, so do all your expensive + * computations here. This function should returns the HTML to be cached. + * It should not add anything to the PageOutput object! + * + * 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 SpecialPage + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @since 1.20 + */ +abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { + + /** + * CacheHelper object to which we forward the non-SpecialPage specific caching work. + * Initialized in startCache. + * + * @since 1.20 + * @var CacheHelper + */ + protected $cacheHelper; + + /** + * If the cache is enabled or not. + * + * @since 1.20 + * @var boolean + */ + protected $cacheEnabled = true; + + /** + * Gets called after @see SpecialPage::execute. + * + * @since 1.20 + * + * @param $subPage string|null + */ + protected function afterExecute( $subPage ) { + $this->saveCache(); + + parent::afterExecute( $subPage ); + } + + /** + * Sets if the cache should be enabled or not. + * + * @since 1.20 + * @param boolean $cacheEnabled + */ + public function setCacheEnabled( $cacheEnabled ) { + $this->cacheHelper->setCacheEnabled( $cacheEnabled ); + } + + /** + * Initializes the caching. + * Should be called before the first time anything is added via addCachedHTML. + * + * @since 1.20 + * + * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + */ + public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { + if ( !isset( $this->cacheHelper ) ) { + $this->cacheHelper = new CacheHelper(); + + $this->cacheHelper->setCacheEnabled( $this->cacheEnabled ); + $this->cacheHelper->setOnInitializedHandler( array( $this, 'onCacheInitialized' ) ); + + $keyArgs = $this->getCacheKey(); + + if ( array_key_exists( 'action', $keyArgs ) && $keyArgs['action'] === 'purge' ) { + unset( $keyArgs['action'] ); + } + + $this->cacheHelper->setCacheKey( $keyArgs ); + + if ( $this->getRequest()->getText( 'action' ) === 'purge' ) { + $this->cacheHelper->rebuildOnDemand(); + } + } + + $this->cacheHelper->startCache( $cacheExpiry, $cacheEnabled ); + } + + /** + * Get a cached value if available or compute it if not and then cache it if possible. + * The provided $computeFunction is only called when the computation needs to happen + * and should return a result value. $args are arguments that will be passed to the + * compute function when called. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array|mixed $args + * @param string|null $key + * + * @return mixed + */ + public function getCachedValue( $computeFunction, $args = array(), $key = null ) { + return $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ); + } + + /** + * Add some HTML to be cached. + * This is done by providing a callback function that should + * return the HTML to be added. It will only be called if the + * item is not in the cache yet or when the cache has been invalidated. + * + * @since 1.20 + * + * @param {function} $computeFunction + * @param array $args + * @param string|null $key + */ + public function addCachedHTML( $computeFunction, $args = array(), $key = null ) { + $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ) ); + } + + /** + * Saves the HTML to the cache in case it got recomputed. + * Should be called after the last time anything is added via addCachedHTML. + * + * @since 1.20 + */ + public function saveCache() { + if ( isset( $this->cacheHelper ) ) { + $this->cacheHelper->saveCache(); + } + } + + /** + * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. + * + * @since 1.20 + * + * @param integer $cacheExpiry + */ + public function setExpiry( $cacheExpiry ) { + $this->cacheHelper->setExpiry( $cacheExpiry ); + } + + /** + * Returns the variables used to constructed the cache key in an array. + * + * @since 1.20 + * + * @return array + */ + protected function getCacheKey() { + return array( + $this->mName, + $this->getLanguage()->getCode() + ); + } + + /** + * Gets called after the cache got initialized. + * + * @since 1.20 + * + * @param boolean $hasCached + */ + public function onCacheInitialized( $hasCached ) { + if ( $hasCached ) { + $this->getOutput()->setSubtitle( $this->cacheHelper->getCachedNotice( $this->getContext() ) ); + } + } + +} diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php index 338cd706..1232e3fa 100644 --- a/includes/specials/SpecialCategories.php +++ b/includes/specials/SpecialCategories.php @@ -59,16 +59,13 @@ class SpecialCategories extends SpecialPage { * @ingroup SpecialPage Pager */ class CategoryPager extends AlphabeticPager { - private $conds = array( 'cat_pages > 0' ); - function __construct( IContextSource $context, $from ) { parent::__construct( $context ); $from = str_replace( ' ', '_', $from ); if( $from !== '' ) { $from = Title::capitalize( $from, NS_CATEGORY ); - $dbr = wfGetDB( DB_SLAVE ); - $this->conds[] = 'cat_title >= ' . $dbr->addQuotes( $from ); - $this->setOffset( '' ); + $this->setOffset( $from ); + $this->setIncludeOffset( true ); } } @@ -76,7 +73,7 @@ class CategoryPager extends AlphabeticPager { return array( 'tables' => array( 'category' ), 'fields' => array( 'cat_title','cat_pages' ), - 'conds' => $this->conds, + 'conds' => array( 'cat_pages > 0' ), 'options' => array( 'USE INDEX' => 'cat_title' ), ); } diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php index 0f85f516..fc726106 100644 --- a/includes/specials/SpecialChangeEmail.php +++ b/includes/specials/SpecialChangeEmail.php @@ -27,10 +27,26 @@ * @ingroup SpecialPage */ class SpecialChangeEmail extends UnlistedSpecialPage { + + /** + * Users password + * @var string + */ + protected $mPassword; + + /** + * Users new email address + * @var string + */ + protected $mNewEmail; + public function __construct() { parent::__construct( 'ChangeEmail' ); } + /** + * @return Bool + */ function isListed() { global $wgAuth; return $wgAuth->allowPropChange( 'emailaddress' ); @@ -42,11 +58,13 @@ class SpecialChangeEmail extends UnlistedSpecialPage { function execute( $par ) { global $wgAuth; - $this->checkReadOnly(); - $this->setHeaders(); $this->outputHeader(); + $out = $this->getOutput(); + $out->disallowUserJs(); + $out->addModules( 'mediawiki.special.changeemail' ); + if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) { $this->error( 'cannotchangeemail' ); return; @@ -65,9 +83,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage { return; } - $out = $this->getOutput(); - $out->disallowUserJs(); - $out->addModules( 'mediawiki.special.changeemail' ); + $this->checkReadOnly(); $this->mPassword = $request->getVal( 'wpPassword' ); $this->mNewEmail = $request->getVal( 'wpNewEmail' ); @@ -90,6 +106,9 @@ class SpecialChangeEmail extends UnlistedSpecialPage { $this->showForm(); } + /** + * @param $type string + */ protected function doReturnTo( $type = 'hard' ) { $titleObj = Title::newFromText( $this->getRequest()->getVal( 'returnto' ) ); if ( !$titleObj instanceof Title ) { @@ -102,11 +121,15 @@ class SpecialChangeEmail extends UnlistedSpecialPage { } } + /** + * @param $msg string + */ protected function error( $msg ) { $this->getOutput()->wrapWikiMsg( "<p class='error'>\n$1\n</p>", $msg ); } protected function showForm() { + global $wgRequirePasswordforEmailChange; $user = $this->getUser(); $oldEmailText = $user->getEmail() @@ -123,13 +146,20 @@ class SpecialChangeEmail extends UnlistedSpecialPage { Html::hidden( 'token', $user->getEditToken() ) . "\n" . Html::hidden( 'returnto', $this->getRequest()->getVal( 'returnto' ) ) . "\n" . $this->msg( 'changeemail-text' )->parseAsBlock() . "\n" . - Xml::openElement( 'table', array( 'id' => 'mw-changeemail-table' ) ) . "\n" . - $this->pretty( array( - array( 'wpName', 'username', 'text', $user->getName() ), - array( 'wpOldEmail', 'changeemail-oldemail', 'text', $oldEmailText ), - array( 'wpNewEmail', 'changeemail-newemail', 'input', $this->mNewEmail ), - array( 'wpPassword', 'yourpassword', 'password', $this->mPassword ), - ) ) . "\n" . + Xml::openElement( 'table', array( 'id' => 'mw-changeemail-table' ) ) . "\n" + ); + $items = array( + array( 'wpName', 'username', 'text', $user->getName() ), + array( 'wpOldEmail', 'changeemail-oldemail', 'text', $oldEmailText ), + array( 'wpNewEmail', 'changeemail-newemail', 'input', $this->mNewEmail ), + ); + if ( $wgRequirePasswordforEmailChange ) { + $items[] = array( 'wpPassword', 'yourpassword', 'password', $this->mPassword ); + } + + $this->getOutput()->addHTML( + $this->pretty( $items ) . + "\n" . "<tr>\n" . "<td></td>\n" . '<td class="mw-input">' . @@ -143,6 +173,10 @@ class SpecialChangeEmail extends UnlistedSpecialPage { ); } + /** + * @param $fields array + * @return string + */ protected function pretty( $fields ) { $out = ''; foreach ( $fields as $list ) { @@ -173,6 +207,9 @@ class SpecialChangeEmail extends UnlistedSpecialPage { } /** + * @param $user User + * @param $pass string + * @param $newaddr string * @return bool|string true or string on success, false on failure */ protected function attemptChange( User $user, $pass, $newaddr ) { @@ -187,7 +224,8 @@ class SpecialChangeEmail extends UnlistedSpecialPage { return false; } - if ( !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) { + global $wgRequirePasswordforEmailChange; + if ( $wgRequirePasswordforEmailChange && !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) { $this->error( 'wrongpassword' ); return false; } @@ -196,18 +234,20 @@ class SpecialChangeEmail extends UnlistedSpecialPage { LoginForm::clearLoginThrottle( $user->getName() ); } - list( $status, $info ) = Preferences::trySetUserEmail( $user, $newaddr ); - if ( $status !== true ) { - if ( $status instanceof Status ) { - $this->getOutput()->addHTML( - '<p class="error">' . - $this->getOutput()->parseInline( $status->getWikiText( $info ) ) . - '</p>' ); - } + $oldaddr = $user->getEmail(); + $status = $user->setEmailWithConfirmation( $newaddr ); + if ( !$status->isGood() ) { + $this->getOutput()->addHTML( + '<p class="error">' . + $this->getOutput()->parseInline( $status->getWikiText( 'mailerror' ) ) . + '</p>' ); return false; } + wfRunHooks( 'PrefsEmailAudit', array( $user, $oldaddr, $newaddr ) ); + $user->saveSettings(); - return $info ? $info : true; + + return $status->value; } } diff --git a/includes/specials/SpecialChangePassword.php b/includes/specials/SpecialChangePassword.php index f6482ef5..41b3b255 100644 --- a/includes/specials/SpecialChangePassword.php +++ b/includes/specials/SpecialChangePassword.php @@ -37,7 +37,9 @@ class SpecialChangePassword extends UnlistedSpecialPage { function execute( $par ) { global $wgAuth; - $this->checkReadOnly(); + $this->setHeaders(); + $this->outputHeader(); + $this->getOutput()->disallowUserJs(); $request = $this->getRequest(); $this->mUserName = trim( $request->getVal( 'wpName' ) ); @@ -46,10 +48,6 @@ class SpecialChangePassword extends UnlistedSpecialPage { $this->mRetype = $request->getVal( 'wpRetype' ); $this->mDomain = $request->getVal( 'wpDomain' ); - $this->setHeaders(); - $this->outputHeader(); - $this->getOutput()->disallowUserJs(); - $user = $this->getUser(); if( !$request->wasPosted() && !$user->isLoggedIn() ) { $this->error( $this->msg( 'resetpass-no-info' )->text() ); @@ -61,12 +59,11 @@ class SpecialChangePassword extends UnlistedSpecialPage { return; } + $this->checkReadOnly(); + if( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'token' ) ) ) { try { - if ( isset( $_SESSION['wsDomain'] ) ) { - $this->mDomain = $_SESSION['wsDomain']; - } - $wgAuth->setDomain( $this->mDomain ); + $this->mDomain = $wgAuth->getDomain(); if( !$wgAuth->allowPasswordChange() ) { $this->error( $this->msg( 'resetpass_forbidden' )->text() ); return; @@ -136,6 +133,15 @@ class SpecialChangePassword extends UnlistedSpecialPage { $oldpassMsg = 'oldpassword'; $submitMsg = 'resetpass-submit-loggedin'; } + $extraFields = array(); + wfRunHooks( 'ChangePasswordForm', array( &$extraFields ) ); + $prettyFields = array( + array( 'wpName', 'username', 'text', $this->mUserName ), + array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ), + array( 'wpNewPassword', 'newpassword', 'password', null ), + array( 'wpRetype', 'retypenew', 'password', null ), + ); + $prettyFields = array_merge( $prettyFields, $extraFields ); $this->getOutput()->addHTML( Xml::fieldset( $this->msg( 'resetpass_header' )->text() ) . Xml::openElement( 'form', @@ -149,12 +155,7 @@ class SpecialChangePassword extends UnlistedSpecialPage { Html::hidden( 'returnto', $this->getRequest()->getVal( 'returnto' ) ) . "\n" . $this->msg( 'resetpass_text' )->parseAsBlock() . "\n" . Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . "\n" . - $this->pretty( array( - array( 'wpName', 'username', 'text', $this->mUserName ), - array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ), - array( 'wpNewPassword', 'newpassword', 'password', null ), - array( 'wpRetype', 'retypenew', 'password', null ), - ) ) . "\n" . + $this->pretty( $prettyFields ) . "\n" . $rememberMe . "<tr>\n" . "<td></td>\n" . diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index 912f7733..3e9ce128 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -110,7 +110,7 @@ class EmailConfirmation extends UnlistedSpecialPage { * Attempt to confirm the user's email address and show success or failure * as needed; if successful, take the user to log in * - * @param $code Confirmation code + * @param $code string Confirmation code */ function attemptConfirm( $code ) { $user = User::newFromConfirmationCode( $code ); @@ -156,7 +156,7 @@ class EmailInvalidation extends UnlistedSpecialPage { * Attempt to invalidate the user's email address and show success or failure * as needed; if successful, link to main page * - * @param $code Confirmation code + * @param $code string Confirmation code */ function attemptInvalidate( $code ) { $user = User::newFromConfirmationCode( $code ); diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 31df4a9b..54f8e261 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -44,10 +44,7 @@ class SpecialContributions extends SpecialPage { $this->opts = array(); $request = $this->getRequest(); - if ( $par == 'newbies' ) { - $target = 'newbies'; - $this->opts['contribs'] = 'newbie'; - } elseif ( $par !== null ) { + if ( $par !== null ) { $target = $par; } else { $target = $request->getVal( 'target' ); @@ -57,6 +54,9 @@ class SpecialContributions extends SpecialPage { if ( $request->getVal( 'contribs' ) == 'newbie' ) { $target = 'newbies'; $this->opts['contribs'] = 'newbie'; + } elseif ( $par === 'newbies' ) { // b/c for WMF + $target = 'newbies'; + $this->opts['contribs'] = 'newbie'; } else { $this->opts['contribs'] = 'user'; } @@ -192,18 +192,20 @@ class SpecialContributions extends SpecialPage { } $out->preventClickjacking( $pager->getPreventClickjacking() ); + # Show the appropriate "footer" message - WHOIS tools, etc. - if ( $this->opts['contribs'] != 'newbie' ) { + if ( $this->opts['contribs'] == 'newbie' ) { + $message = 'sp-contributions-footer-newbies'; + } elseif( IP::isIPAddress( $target ) ) { + $message = 'sp-contributions-footer-anon'; + } elseif( $userObj->isAnon() ) { + // No message for non-existing users + $message = ''; + } else { $message = 'sp-contributions-footer'; - if ( IP::isIPAddress( $target ) ) { - $message = 'sp-contributions-footer-anon'; - } else { - if ( $userObj->isAnon() ) { - // No message for non-existing users - return; - } - } + } + if( $message ) { if ( !$this->msg( $message, $target )->isDisabled() ) { $out->wrapWikiMsg( "<div class='mw-contributions-footer'>\n$1\n</div>", @@ -227,12 +229,15 @@ class SpecialContributions extends SpecialPage { } $nt = $userObj->getUserPage(); $talk = $userObj->getTalkPage(); + $links = ''; if ( $talk ) { $tools = $this->getUserLinks( $nt, $talk, $userObj ); $links = $this->getLanguage()->pipeList( $tools ); // Show a note if the user is blocked and display the last block log entry. - if ( $userObj->isBlocked() ) { + // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs, + // and also this will display a totally irrelevant log entry as a current block. + if ( $userObj->isBlocked() && $userObj->getBlock()->getType() != Block::TYPE_AUTO ) { $out = $this->getOutput(); // showLogExtract() wants first parameter by reference LogEventsList::showLogExtract( $out, @@ -258,9 +263,11 @@ class SpecialContributions extends SpecialPage { // languages that want to put the "for" bit right after $user but before // $links. If 'contribsub' is around, use it for reverse compatibility, // otherwise use 'contribsub2'. + // @todo Should this be removed at some point? $oldMsg = $this->msg( 'contribsub' ); if ( $oldMsg->exists() ) { - return $oldMsg->rawParams( "$user ($links)" ); + $linksWithParentheses = $this->msg( 'parentheses' )->rawParams( $links )->escaped(); + return $oldMsg->rawParams( "$user $linksWithParentheses" ); } else { return $this->msg( 'contribsub2' )->rawParams( $user, $links ); } @@ -331,7 +338,7 @@ class SpecialContributions extends SpecialPage { # Add a link to change user rights for privileged users $userrightsPage = new UserrightsPage(); $userrightsPage->setContext( $this->getContext() ); - if ( $id !== null && $userrightsPage->userCanChangeRights( $target ) ) { + if ( $userrightsPage->userCanChangeRights( $target ) ) { $tools[] = Linker::linkKnown( SpecialPage::getTitleFor( 'Userrights', $username ), $this->msg( 'sp-contributions-userrights' )->escaped() @@ -434,7 +441,7 @@ class SpecialContributions extends SpecialPage { 'target', $this->opts['target'], 'text', - array( 'size' => '20', 'required' => '', 'class' => 'mw-input' ) + + array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) + ( $this->opts['target'] ? array() : array( 'autofocus' ) ) ) . ' ' @@ -449,7 +456,15 @@ class SpecialContributions extends SpecialPage { ) ) . Xml::tags( 'td', null, - Xml::namespaceSelector( $this->opts['namespace'], '' ) . ' ' . + Html::namespaceSelector( array( + 'selected' => $this->opts['namespace'], + 'all' => '', + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) ) . + ' ' . Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ), Xml::checkLabel( $this->msg( 'invert' )->text(), @@ -542,6 +557,11 @@ class ContribsPager extends ReverseChronologicalPager { var $namespace = '', $mDb; var $preventClickjacking = false; + /** + * @var array + */ + protected $mParentLens; + function __construct( IContextSource $context, array $options ) { parent::__construct( $context ); @@ -574,6 +594,66 @@ class ContribsPager extends ReverseChronologicalPager { return $query; } + /** + * This method basically executes the exact same code as the parent class, though with + * a hook added, to allow extentions to add additional queries. + * + * @param $offset String: index offset, inclusive + * @param $limit Integer: exact query limit + * @param $descending Boolean: query direction, false for ascending, true for descending + * @return ResultWrapper + */ + function reallyDoQuery( $offset, $limit, $descending ) { + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $descending ); + $pager = $this; + + /* + * This hook will allow extensions to add in additional queries, so they can get their data + * in My Contributions as well. Extensions should append their results to the $data array. + * + * Extension queries have to implement the navbar requirement as well. They should + * - have a column aliased as $pager->getIndexField() + * - have LIMIT set + * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset + * - have the ORDER BY specified based upon the details provided by the navbar + * + * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY + * + * &$data: an array of results of all contribs queries + * $pager: the ContribsPager object hooked into + * $offset: see phpdoc above + * $limit: see phpdoc above + * $descending: see phpdoc above + */ + $data = array( $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ) ); + wfRunHooks( 'ContribsPager::reallyDoQuery', array( &$data, $pager, $offset, $limit, $descending ) ); + + $result = array(); + + // loop all results and collect them in an array + foreach ( $data as $j => $query ) { + foreach ( $query as $i => $row ) { + // use index column as key, allowing us to easily sort in PHP + $result[$row->{$this->getIndexField()} . "-$i"] = $row; + } + } + + // sort results + if ( $descending ) { + ksort( $result ); + } else { + krsort( $result ); + } + + // enforce limit + $result = array_slice( $result, 0, $limit ); + + // get rid of array keys + $result = array_values( $result ); + + return new FakeResultWrapper( $result ); + } + function getQueryInfo() { list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond(); @@ -624,20 +704,30 @@ class ContribsPager extends ReverseChronologicalPager { $join_conds = array(); $tables = array( 'revision', 'page', 'user' ); if ( $this->contribs == 'newbie' ) { - $tables[] = 'user_groups'; $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); $condition[] = 'rev_user >' . (int)( $max - $max / 100 ); - $condition[] = 'ug_group IS NULL'; $index = 'user_timestamp'; - # @todo FIXME: Other groups may have 'bot' rights - $join_conds['user_groups'] = array( 'LEFT JOIN', "ug_user = rev_user AND ug_group = 'bot'" ); + # ignore local groups with the bot right + # @todo FIXME: Global groups may have 'bot' rights + $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); + if( count( $groupsWithBotPermission ) ) { + $tables[] = 'user_groups'; + $condition[] = 'ug_group IS NULL'; + $join_conds['user_groups'] = array( + 'LEFT JOIN', array( + 'ug_user = rev_user', + 'ug_group' => $groupsWithBotPermission + ) + ); + } } else { - if ( IP::isIPAddress( $this->target ) ) { + $uid = User::idFromName( $this->target ); + if ( $uid ) { + $condition['rev_user'] = $uid; + $index = 'user_timestamp'; + } else { $condition['rev_user_text'] = $this->target; $index = 'usertext_timestamp'; - } else { - $condition['rev_user'] = User::idFromName( $this->target ); - $index = 'user_timestamp'; } } if ( $this->deletedOnly ) { @@ -678,52 +768,29 @@ class ContribsPager extends ReverseChronologicalPager { } function doBatchLookups() { - $this->mResult->rewind(); - $revIds = array(); - foreach ( $this->mResult as $row ) { - if( $row->rev_parent_id ) { - $revIds[] = $row->rev_parent_id; - } - } - $this->mParentLens = $this->getParentLengths( $revIds ); - $this->mResult->rewind(); // reset - # Do a link batch query $this->mResult->seek( 0 ); + $revIds = array(); $batch = new LinkBatch(); # Give some pointers to make (last) links foreach ( $this->mResult as $row ) { - if ( $this->contribs === 'newbie' ) { // multiple users - $batch->add( NS_USER, $row->user_name ); - $batch->add( NS_USER_TALK, $row->user_name ); + if( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { + $revIds[] = $row->rev_parent_id; + } + if ( isset( $row->rev_id ) ) { + if ( $this->contribs === 'newbie' ) { // multiple users + $batch->add( NS_USER, $row->user_name ); + $batch->add( NS_USER_TALK, $row->user_name ); + } + $batch->add( $row->page_namespace, $row->page_title ); } - $batch->add( $row->page_namespace, $row->page_title ); } + $this->mParentLens = Revision::getParentLengths( $this->getDatabase(), $revIds ); $batch->execute(); $this->mResult->seek( 0 ); } /** - * Do a batched query to get the parent revision lengths - */ - private function getParentLengths( array $revIds ) { - $revLens = array(); - if ( !$revIds ) { - return $revLens; // empty - } - wfProfileIn( __METHOD__ ); - $res = $this->getDatabase()->select( 'revision', - array( 'rev_id', 'rev_len' ), - array( 'rev_id' => $revIds ), - __METHOD__ ); - foreach ( $res as $row ) { - $revLens[$row->rev_id] = $row->rev_len; - } - wfProfileOut( __METHOD__ ); - return $revLens; - } - - /** * @return string */ function getStartBody() { @@ -746,142 +813,153 @@ class ContribsPager extends ReverseChronologicalPager { * was not written by the target user. * * @todo This would probably look a lot nicer in a table. + * @param $row + * @return string */ function formatRow( $row ) { wfProfileIn( __METHOD__ ); - $rev = new Revision( $row ); + $ret = ''; $classes = array(); - $page = Title::newFromRow( $row ); - $link = Linker::link( - $page, - htmlspecialchars( $page->getPrefixedText() ), - array(), - $page->isRedirect() ? array( 'redirect' => 'no' ) : array() - ); - # Mark current revisions - $topmarktext = ''; - if ( $row->rev_id == $row->page_latest ) { - $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; - # Add rollback link - if ( !$row->page_is_new && $page->quickUserCan( 'rollback' ) - && $page->quickUserCan( 'edit' ) ) - { - $this->preventClickjacking(); - $topmarktext .= ' ' . Linker::generateRollback( $rev ); + /* + * There may be more than just revision rows. To make sure that we'll only be processing + * revisions here, let's _try_ to build a revision out of our row (without displaying + * notices though) and then trying to grab data from the built object. If we succeed, + * we're definitely dealing with revision data and we may proceed, if not, we'll leave it + * to extensions to subscribe to the hook to parse the row. + */ + wfSuppressWarnings(); + $rev = new Revision( $row ); + $validRevision = $rev->getParentId() !== null; + wfRestoreWarnings(); + + if ( $validRevision ) { + $classes = array(); + + $page = Title::newFromRow( $row ); + $link = Linker::link( + $page, + htmlspecialchars( $page->getPrefixedText() ), + array( 'class' => 'mw-contributions-title' ), + $page->isRedirect() ? array( 'redirect' => 'no' ) : array() + ); + # Mark current revisions + $topmarktext = ''; + $user = $this->getUser(); + if ( $row->rev_id == $row->page_latest ) { + $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; + # Add rollback link + if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user ) + && $page->quickUserCan( 'edit', $user ) ) + { + $this->preventClickjacking(); + $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() ); + } } - } - $user = $this->getUser(); - # Is there a visible previous revision? - if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) { - $difftext = Linker::linkKnown( + # Is there a visible previous revision? + if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) { + $difftext = Linker::linkKnown( + $page, + $this->messages['diff'], + array(), + array( + 'diff' => 'prev', + 'oldid' => $row->rev_id + ) + ); + } else { + $difftext = $this->messages['diff']; + } + $histlink = Linker::linkKnown( $page, - $this->messages['diff'], + $this->messages['hist'], array(), - array( - 'diff' => 'prev', - 'oldid' => $row->rev_id - ) + array( 'action' => 'history' ) ); - } else { - $difftext = $this->messages['diff']; - } - $histlink = Linker::linkKnown( - $page, - $this->messages['hist'], - array(), - array( 'action' => 'history' ) - ); - if ( $row->rev_parent_id === null ) { - // For some reason rev_parent_id isn't populated for this row. - // Its rumoured this is true on wikipedia for some revisions (bug 34922). - // Next best thing is to have the total number of bytes. - $chardiff = ' . . ' . Linker::formatRevisionSize( $row->rev_len ) . ' . . '; - } else { - $parentLen = isset( $this->mParentLens[$row->rev_parent_id] ) ? $this->mParentLens[$row->rev_parent_id] : 0; - $chardiff = ' . . ' . ChangesList::showCharacterDifference( - $parentLen, $row->rev_len ) . ' . . '; - } + if ( $row->rev_parent_id === null ) { + // For some reason rev_parent_id isn't populated for this row. + // Its rumoured this is true on wikipedia for some revisions (bug 34922). + // Next best thing is to have the total number of bytes. + $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . Linker::formatRevisionSize( $row->rev_len ) . ' <span class="mw-changeslist-separator">. .</span> '; + } else { + $parentLen = isset( $this->mParentLens[$row->rev_parent_id] ) ? $this->mParentLens[$row->rev_parent_id] : 0; + $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . ChangesList::showCharacterDifference( + $parentLen, $row->rev_len, $this->getContext() ) . ' <span class="mw-changeslist-separator">. .</span> '; + } - $lang = $this->getLanguage(); - $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); - $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); - if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { - $d = Linker::linkKnown( - $page, - htmlspecialchars( $date ), - array(), - array( 'oldid' => intval( $row->rev_id ) ) - ); - } else { - $d = htmlspecialchars( $date ); - } - if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $d = '<span class="history-deleted">' . $d . '</span>'; - } + $lang = $this->getLanguage(); + $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); + $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); + if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { + $d = Linker::linkKnown( + $page, + htmlspecialchars( $date ), + array( 'class' => 'mw-changeslist-date' ), + array( 'oldid' => intval( $row->rev_id ) ) + ); + } else { + $d = htmlspecialchars( $date ); + } + if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $d = '<span class="history-deleted">' . $d . '</span>'; + } - # Show user names for /newbies as there may be different users. - # Note that we already excluded rows with hidden user names. - if ( $this->contribs == 'newbie' ) { - $userlink = ' . . ' . Linker::userLink( $rev->getUser(), $rev->getUserText() ); - $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( - Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; - } else { - $userlink = ''; - } + # Show user names for /newbies as there may be different users. + # Note that we already excluded rows with hidden user names. + if ( $this->contribs == 'newbie' ) { + $userlink = ' . . ' . Linker::userLink( $rev->getUser(), $rev->getUserText() ); + $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( + Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; + } else { + $userlink = ''; + } - if ( $rev->getParentId() === 0 ) { - $nflag = ChangesList::flag( 'newpage' ); - } else { - $nflag = ''; - } + if ( $rev->getParentId() === 0 ) { + $nflag = ChangesList::flag( 'newpage' ); + } else { + $nflag = ''; + } - if ( $rev->isMinor() ) { - $mflag = ChangesList::flag( 'minor' ); - } else { - $mflag = ''; - } + if ( $rev->isMinor() ) { + $mflag = ChangesList::flag( 'minor' ); + } else { + $mflag = ''; + } - $del = Linker::getRevDeleteLink( $user, $rev, $page ); - if ( $del !== '' ) { - $del .= ' '; - } + $del = Linker::getRevDeleteLink( $user, $rev, $page ); + if ( $del !== '' ) { + $del .= ' '; + } - $diffHistLinks = '(' . $difftext . $this->messages['pipe-separator'] . $histlink . ')'; - $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}"; + $diffHistLinks = $this->msg( 'parentheses' )->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink )->escaped(); + $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}"; - # Denote if username is redacted for this edit - if ( $rev->isDeleted( Revision::DELETED_USER ) ) { - $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>"; - } + # Denote if username is redacted for this edit + if ( $rev->isDeleted( Revision::DELETED_USER ) ) { + $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>"; + } - # Tags, if any. - list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' ); - $classes = array_merge( $classes, $newClasses ); - $ret .= " $tagSummary"; + # Tags, if any. + list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' ); + $classes = array_merge( $classes, $newClasses ); + $ret .= " $tagSummary"; + } // Let extensions add data - wfRunHooks( 'ContributionsLineEnding', array( &$this, &$ret, $row ) ); + wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); $classes = implode( ' ', $classes ); $ret = "<li class=\"$classes\">$ret</li>\n"; + wfProfileOut( __METHOD__ ); return $ret; } /** - * Get the Database object in use - * - * @return DatabaseBase - */ - public function getDatabase() { - return $this->mDb; - } - - /** * Overwrite Pager function and return a helpful comment + * @return string */ function getSqlComment() { if ( $this->namespace || $this->deletedOnly ) { @@ -895,6 +973,9 @@ class ContribsPager extends ReverseChronologicalPager { $this->preventClickjacking = true; } + /** + * @return bool + */ public function getPreventClickjacking() { return $this->preventClickjacking; } diff --git a/includes/specials/SpecialDeadendpages.php b/includes/specials/SpecialDeadendpages.php index 1266a0ce..f4904a50 100644 --- a/includes/specials/SpecialDeadendpages.php +++ b/includes/specials/SpecialDeadendpages.php @@ -39,7 +39,7 @@ class DeadendPagesPage extends PageQueryPage { /** * LEFT JOIN is expensive * - * @return true + * @return bool */ function isExpensive() { return true; @@ -50,7 +50,7 @@ class DeadendPagesPage extends PageQueryPage { } /** - * @return false + * @return bool */ function sortDescending() { return false; @@ -59,9 +59,9 @@ class DeadendPagesPage extends PageQueryPage { function getQueryInfo() { return array( 'tables' => array( 'page', 'pagelinks' ), - 'fields' => array( 'page_namespace AS namespace', - 'page_title AS title', - 'page_title AS value' + 'fields' => array( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' ), 'conds' => array( 'pl_from IS NULL', 'page_namespace' => MWNamespace::getContentNamespaces(), diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 3498a16d..c880b617 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -25,7 +25,6 @@ * Implements Special:DeletedContributions to display archived revisions * @ingroup SpecialPage */ - class DeletedContribsPager extends IndexPager { public $mDefaultDirection = true; var $messages, $target; @@ -54,9 +53,9 @@ class DeletedContribsPager extends IndexPager { $user = $this->getUser(); // Paranoia: avoid brute force searches (bug 17792) if( !$user->isAllowed( 'deletedhistory' ) ) { - $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::DELETED_USER) . ' = 0'; + $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::DELETED_USER ) . ' = 0'; } elseif( !$user->isAllowed( 'suppressrevision' ) ) { - $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::SUPPRESSED_USER) . + $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::SUPPRESSED_USER ) . ' != ' . Revision::SUPPRESSED_USER; } return array( @@ -95,17 +94,17 @@ class DeletedContribsPager extends IndexPager { if ( isset( $this->mNavigationBar ) ) { return $this->mNavigationBar; } - $lang = $this->getLanguage(); - $fmtLimit = $lang->formatNum( $this->mLimit ); + $linkTexts = array( - 'prev' => $this->msg( 'pager-newer-n', $fmtLimit )->escaped(), - 'next' => $this->msg( 'pager-older-n', $fmtLimit )->escaped(), + 'prev' => $this->msg( 'pager-newer-n' )->numParams( $this->mLimit )->escaped(), + 'next' => $this->msg( 'pager-older-n' )->numParams( $this->mLimit )->escaped(), 'first' => $this->msg( 'histlast' )->escaped(), 'last' => $this->msg( 'histfirst' )->escaped() ); $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); + $lang = $this->getLanguage(); $limits = $lang->pipeList( $limitLinks ); $this->mNavigationBar = "(" . $lang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " . @@ -130,6 +129,8 @@ class DeletedContribsPager extends IndexPager { * written by the target user. * * @todo This would probably look a lot nicer in a table. + * @param $row + * @return string */ function formatRow( $row ) { wfProfileIn( __METHOD__ ); @@ -190,7 +191,7 @@ class DeletedContribsPager extends IndexPager { $link = Linker::linkKnown( $undelete, $date, - array(), + array( 'class' => 'mw-changeslist-date' ), array( 'target' => $page->getPrefixedText(), 'timestamp' => $rev->getTimestamp() @@ -202,7 +203,11 @@ class DeletedContribsPager extends IndexPager { $link = '<span class="history-deleted">' . $link . '</span>'; } - $pagelink = Linker::link( $page ); + $pagelink = Linker::link( + $page, + null, + array( 'class' => 'mw-changeslist-title' ) + ); if( $rev->isMinor() ) { $mflag = ChangesList::flag( 'minor' ); @@ -221,7 +226,8 @@ class DeletedContribsPager extends IndexPager { array( $last, $dellog, $reviewlink ) ) )->escaped() ); - $ret = "{$del}{$link} {$tools} . . {$mflag} {$pagelink} {$comment}"; + $separator = '<span class="mw-changeslist-separator">. .</span>'; + $ret = "{$del}{$link} {$tools} {$separator} {$mflag} {$pagelink} {$comment}"; # Denote if username is redacted for this edit if( $rev->isDeleted( Revision::DELETED_USER ) ) { @@ -237,7 +243,7 @@ class DeletedContribsPager extends IndexPager { /** * Get the Database object in use * - * @return Database + * @return DatabaseBase */ public function getDatabase() { return $this->mDb; @@ -254,12 +260,12 @@ class DeletedContributionsPage extends SpecialPage { * Special page "deleted user contributions". * Shows a list of the deleted contributions of a user. * - * @return none * @param $par String: (optional) user name of the user for which to show the contributions */ function execute( $par ) { global $wgQueryPageDefaultLimit; $this->setHeaders(); + $this->outputHeader(); $user = $this->getUser(); @@ -293,6 +299,7 @@ class DeletedContributionsPage extends SpecialPage { $out->addHTML( $this->getForm( '' ) ); return; } + $this->getSkin()->setRelevantUser( $userObj ); $target = $userObj->getName(); $out->addSubtitle( $this->getSubTitle( $userObj ) ); @@ -346,6 +353,7 @@ class DeletedContributionsPage extends SpecialPage { } else { $user = Linker::link( $userObj->getUserPage(), htmlspecialchars( $userObj->getName() ) ); } + $links = ''; $nt = $userObj->getUserPage(); $id = $userObj->getID(); $talk = $nt->getTalkPage(); @@ -387,6 +395,13 @@ class DeletedContributionsPage extends SpecialPage { ) ); } + + # Uploads + $tools[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Listfiles', $userObj->getName() ), + $this->msg( 'sp-contributions-uploads' )->escaped() + ); + # Other logs link $tools[] = Linker::linkKnown( SpecialPage::getTitleFor( 'Log' ), @@ -403,7 +418,7 @@ class DeletedContributionsPage extends SpecialPage { # Add a link to change user rights for privileged users $userrightsPage = new UserrightsPage(); $userrightsPage->setContext( $this->getContext() ); - if( $id !== null && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) { + if( $userrightsPage->userCanChangeRights( $userObj ) ) { $tools[] = Linker::linkKnown( SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ), $this->msg( 'sp-contributions-userrights' )->escaped() @@ -450,6 +465,7 @@ class DeletedContributionsPage extends SpecialPage { /** * Generates the namespace selector form with hidden attributes. * @param $options Array: the options to be included. + * @return string */ function getForm( $options ) { global $wgScript; @@ -489,8 +505,17 @@ class DeletedContributionsPage extends SpecialPage { 'size' => '20', 'required' => '' ) + ( $options['target'] ? array() : array( 'autofocus' ) ) ) . ' '. - Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . ' ' . - Xml::namespaceSelector( $options['namespace'], '' ) . ' ' . + Html::namespaceSelector( + array( + 'selected' => $options['namespace'], + 'all' => '', + 'label' => $this->msg( 'namespace' )->text() + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ) . ' ' . Xml::submitButton( $this->msg( 'sp-contributions-submit' )->text() ) . Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php index 2b05fb6b..48180a77 100644 --- a/includes/specials/SpecialDisambiguations.php +++ b/includes/specials/SpecialDisambiguations.php @@ -26,27 +26,35 @@ * * @ingroup SpecialPage */ -class DisambiguationsPage extends PageQueryPage { +class DisambiguationsPage extends QueryPage { function __construct( $name = 'Disambiguations' ) { parent::__construct( $name ); } - function isExpensive() { return true; } - function isSyndicated() { return false; } + function isExpensive() { + return true; + } + + function isSyndicated() { + return false; + } function getPageHeader() { return $this->msg( 'disambiguations-text' )->parseAsBlock(); } - function getQueryInfo() { + /** + * @return string|bool False on failure + */ + function getQueryFromLinkBatch() { $dbr = wfGetDB( DB_SLAVE ); $dMsgText = $this->msg( 'disambiguationspage' )->inContentLanguage()->text(); $linkBatch = new LinkBatch; # If the text can be treated as a title, use it verbatim. # Otherwise, pull the titles from the links table - $dp = Title::newFromText($dMsgText); + $dp = Title::newFromText( $dMsgText ); if( $dp ) { if( $dp->getNamespace() != NS_TEMPLATE ) { # @todo FIXME: We assume the disambiguation message is a template but @@ -71,25 +79,38 @@ class DisambiguationsPage extends PageQueryPage { } } $set = $linkBatch->constructSet( 'tl', $dbr ); + if( $set === false ) { # We must always return a valid SQL query, but this way # the DB will always quickly return an empty result $set = 'FALSE'; - wfDebug("Mediawiki:disambiguationspage message does not link to any templates!\n"); + wfDebug( "Mediawiki:disambiguationspage message does not link to any templates!\n" ); } + return $set; + } + function getQueryInfo() { // @todo FIXME: What are pagelinks and p2 doing here? return array ( - 'tables' => array( 'templatelinks', 'p1' => 'page', 'pagelinks', 'p2' => 'page' ), - 'fields' => array( 'p1.page_namespace AS namespace', - 'p1.page_title AS title', - 'pl_from AS value' ), - 'conds' => array( $set, - 'p1.page_id = tl_from', - 'pl_namespace = p1.page_namespace', - 'pl_title = p1.page_title', - 'p2.page_id = pl_from', - 'p2.page_namespace' => MWNamespace::getContentNamespaces() ) + 'tables' => array( + 'templatelinks', + 'p1' => 'page', + 'pagelinks', + 'p2' => 'page' + ), + 'fields' => array( + 'namespace' => 'p1.page_namespace', + 'title' => 'p1.page_title', + 'value' => 'pl_from' + ), + 'conds' => array( + $this->getQueryFromLinkBatch(), + 'p1.page_id = tl_from', + 'pl_namespace = p1.page_namespace', + 'pl_title = p1.page_title', + 'p2.page_id = pl_from', + 'p2.page_namespace' => MWNamespace::getContentNamespaces() + ) ); } @@ -108,17 +129,17 @@ class DisambiguationsPage extends PageQueryPage { * @param $res */ function preprocessResults( $db, $res ) { + if ( !$res->numRows() ) { + return; + } + $batch = new LinkBatch; foreach ( $res as $row ) { $batch->add( $row->namespace, $row->title ); } $batch->execute(); - // Back to start for display - if ( $db->numRows( $res ) > 0 ) { - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); - } + $res->seek( 0 ); } function formatResult( $skin, $result ) { @@ -126,10 +147,14 @@ class DisambiguationsPage extends PageQueryPage { $dp = Title::makeTitle( $result->namespace, $result->title ); $from = Linker::link( $title ); - $edit = Linker::link( $title, $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->escaped(), - array(), array( 'redirect' => 'no', 'action' => 'edit' ) ); - $arr = $this->getLanguage()->getArrow(); - $to = Linker::link( $dp ); + $edit = Linker::link( + $title, + $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->escaped(), + array(), + array( 'redirect' => 'no', 'action' => 'edit' ) + ); + $arr = $this->getLanguage()->getArrow(); + $to = Linker::link( $dp ); return "$from $edit $arr $to"; } diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php index a6df66f6..5864ca9f 100644 --- a/includes/specials/SpecialDoubleRedirects.php +++ b/includes/specials/SpecialDoubleRedirects.php @@ -27,7 +27,7 @@ * * @ingroup SpecialPage */ -class DoubleRedirectsPage extends PageQueryPage { +class DoubleRedirectsPage extends QueryPage { function __construct( $name = 'DoubleRedirects' ) { parent::__construct( $name ); @@ -47,13 +47,13 @@ class DoubleRedirectsPage extends PageQueryPage { 'tables' => array ( 'ra' => 'redirect', 'rb' => 'redirect', 'pa' => 'page', 'pb' => 'page', 'pc' => 'page' ), - 'fields' => array ( 'pa.page_namespace AS namespace', - 'pa.page_title AS title', - 'pa.page_title AS value', - 'pb.page_namespace AS nsb', - 'pb.page_title AS tb', - 'pc.page_namespace AS nsc', - 'pc.page_title AS tc' ), + 'fields' => array ( 'namespace' => 'pa.page_namespace', + 'title' => 'pa.page_title', + 'value' => 'pa.page_title', + 'nsb' => 'pb.page_namespace', + 'tb' => 'pb.page_title', + 'nsc' => 'pc.page_namespace', + 'tc' => 'pc.page_title' ), 'conds' => array ( 'ra.rd_from = pa.page_id', 'pb.page_namespace = ra.rd_namespace', 'pb.page_title = ra.rd_title', diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index 9c9689ae..23cd9aa6 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -1,9 +1,36 @@ <?php +/** + * @defgroup Watchlist Users watchlist handling + */ + +/** + * Implements Special:EditWatchlist + * + * 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 SpecialPage + * @ingroup Watchlist + */ /** * Provides the UI through which users can perform editing * operations on their watchlist * + * @ingroup SpecialPage * @ingroup Watchlist * @author Rob Church <robchur@gmail.com> */ @@ -76,7 +103,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $form = $this->getRawForm(); if( $form->show() ){ $out->addHTML( $this->successMessage ); - $out->returnToMain(); + $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) ); } break; @@ -86,7 +113,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $form = $this->getNormalForm(); if( $form->show() ){ $out->addHTML( $this->successMessage ); - $out->returnToMain(); + $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) ); } elseif ( $this->toc !== false ) { $out->prependHTML( $this->toc ); } @@ -102,21 +129,28 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * @return array */ private function extractTitles( $list ) { - $titles = array(); $list = explode( "\n", trim( $list ) ); if( !is_array( $list ) ) { return array(); } + $titles = array(); foreach( $list as $text ) { $text = trim( $text ); if( strlen( $text ) > 0 ) { $title = Title::newFromText( $text ); if( $title instanceof Title && $title->isWatchable() ) { - $titles[] = $title->getPrefixedText(); + $titles[] = $title; } } } - return array_unique( $titles ); + + GenderCache::singleton()->doTitlesArray( $titles ); + + $list = array(); + foreach( $titles as $title ) { + $list[] = $title->getPrefixedText(); + } + return array_unique( $list ); } public function submitRaw( $data ){ @@ -214,22 +248,30 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $dbr = wfGetDB( DB_MASTER ); $res = $dbr->select( 'watchlist', - '*', array( + 'wl_namespace', 'wl_title' + ), array( 'wl_user' => $this->getUser()->getId(), ), __METHOD__ ); if( $res->numRows() > 0 ) { + $titles = array(); foreach ( $res as $row ) { $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ); if ( $this->checkTitle( $title, $row->wl_namespace, $row->wl_title ) && !$title->isTalkPage() ) { - $list[] = $title->getPrefixedText(); + $titles[] = $title; } } $res->free(); + + GenderCache::singleton()->doTitlesArray( $titles ); + + foreach( $titles as $title ) { + $list[] = $title->getPrefixedText(); + } } $this->cleanupWatchlist(); return $list; @@ -250,7 +292,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { array( 'wl_namespace', 'wl_title' ), array( 'wl_user' => $this->getUser()->getId() ), __METHOD__, - array( 'ORDER BY' => 'wl_namespace, wl_title' ) + array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' ) ) ); $lb = new LinkBatch(); @@ -270,7 +312,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * * @param Title $title * @param int $namespace - * @param String $dbKey + * @param String $dbKey * @return bool: Whether this item is valid */ private function checkTitle( $title, $namespace, $dbKey ) { @@ -294,18 +336,20 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * Attempts to clean up broken items */ private function cleanupWatchlist() { - if ( count( $this->badItems ) ) { - $dbw = wfGetDB( DB_MASTER ); + if( !count( $this->badItems ) ) { + return; //nothing to do } + $dbw = wfGetDB( DB_MASTER ); + $user = $this->getUser(); foreach ( $this->badItems as $row ) { list( $title, $namespace, $dbKey ) = $row; - wfDebug( "User {$this->getUser()} has broken watchlist item ns($namespace):$dbKey, " + wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, " . ( $title ? 'cleaning up' : 'deleting' ) . ".\n" ); $dbw->delete( 'watchlist', array( - 'wl_user' => $this->getUser()->getId(), + 'wl_user' => $user->getId(), 'wl_namespace' => $namespace, 'wl_title' => $dbKey, ), @@ -314,7 +358,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { // Can't just do an UPDATE instead of DELETE/INSERT due to unique index if ( $title ) { - $this->getUser()->addWatch( $title ); + $user->addWatch( $title ); } } } @@ -408,7 +452,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { foreach( $data as $titles ) { $this->unwatchTitles( $titles ); - $removed += $titles; + $removed = array_merge( $removed, $titles ); } if( count( $removed ) > 0 ) { @@ -445,7 +489,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $title = Title::makeTitleSafe( $namespace, $dbkey ); if ( $this->checkTitle( $title, $namespace, $dbkey ) ) { $text = $this->buildRemoveLine( $title ); - $fields['TitlesNs'.$namespace]['options'][$text] = $title->getEscapedText(); + $fields['TitlesNs'.$namespace]['options'][$text] = htmlspecialchars( $title->getPrefixedText() ); $count++; } } @@ -455,7 +499,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { if ( count( $fields ) > 1 && $count > 30 ) { $this->toc = Linker::tocIndent(); $tocLength = 0; - foreach( $fields as $key => $data ) { + foreach( $fields as $data ) { # strip out the 'ns' prefix from the section name: $ns = substr( $data['section'], 2 ); @@ -572,7 +616,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * Build a set of links for convenient navigation * between watchlist viewing and editing modes * - * @param $unused Unused + * @param $unused * @return string */ public static function buildTools( $unused ) { @@ -588,12 +632,12 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw' $tools[] = Linker::linkKnown( SpecialPage::getTitleFor( $arr[0], $arr[1] ), - wfMsgHtml( "watchlisttools-{$mode}" ) + wfMessage( "watchlisttools-{$mode}" )->escaped() ); } return Html::rawElement( 'span', array( 'class' => 'mw-watchlist-toollinks' ), - wfMsg( 'parentheses', $wgLang->pipeList( $tools ) ) ); + wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text() ); } } diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index 314da727..4d875e6e 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -33,6 +33,15 @@ class SpecialEmailUser extends UnlistedSpecialPage { parent::__construct( 'Emailuser' ); } + public function getDescription() { + $target = self::getTarget( $this->mTarget ); + if( !$target instanceof User ) { + return $this->msg( 'emailuser-title-notarget' )->text(); + } + + return $this->msg( 'emailuser-title-target', $target->getName() )->text(); + } + protected function getFormFields() { return array( 'From' => array( @@ -61,18 +70,19 @@ class SpecialEmailUser extends UnlistedSpecialPage { ), 'Subject' => array( 'type' => 'text', - 'default' => wfMsgExt( 'defemailsubject', array( 'content', 'parsemag' ), $this->getUser()->getName() ), + 'default' => $this->msg( 'defemailsubject', + $this->getUser()->getName() )->inContentLanguage()->text(), 'label-message' => 'emailsubject', 'maxlength' => 200, 'size' => 60, - 'required' => 1, + 'required' => true, ), 'Text' => array( 'type' => 'textarea', 'rows' => 20, 'cols' => 80, 'label-message' => 'emailmessage', - 'required' => 1, + 'required' => true, ), 'CCMe' => array( 'type' => 'check', @@ -83,13 +93,18 @@ class SpecialEmailUser extends UnlistedSpecialPage { } public function execute( $par ) { - $this->setHeaders(); - $this->outputHeader(); $out = $this->getOutput(); $out->addModuleStyles( 'mediawiki.special' ); + $this->mTarget = is_null( $par ) ? $this->getRequest()->getVal( 'wpTarget', $this->getRequest()->getVal( 'target', '' ) ) : $par; + + // This needs to be below assignment of $this->mTarget because + // getDescription() needs it to determine the correct page title. + $this->setHeaders(); + $this->outputHeader(); + // error out if sending user cannot do this $error = self::getPermissionsError( $this->getUser(), $this->getRequest()->getVal( 'wpEditToken' ) ); switch ( $error ) { @@ -124,18 +139,17 @@ class SpecialEmailUser extends UnlistedSpecialPage { $this->mTargetObj = $ret; $form = new HTMLForm( $this->getFormFields(), $this->getContext() ); - $form->addPreText( wfMsgExt( 'emailpagetext', 'parseinline' ) ); - $form->setSubmitText( wfMsg( 'emailsend' ) ); + $form->addPreText( $this->msg( 'emailpagetext' )->parse() ); + $form->setSubmitTextMsg( 'emailsend' ); $form->setTitle( $this->getTitle() ); - $form->setSubmitCallback( array( __CLASS__, 'submit' ) ); - $form->setWrapperLegend( wfMsgExt( 'email-legend', 'parsemag' ) ); + $form->setSubmitCallback( array( __CLASS__, 'uiSubmit' ) ); + $form->setWrapperLegendMsg( 'email-legend' ); $form->loadData(); if( !wfRunHooks( 'EmailUserForm', array( &$form ) ) ) { return false; } - $out->setPageTitle( $this->msg( 'emailpage' ) ); $result = $form->show(); if( $result === true || ( $result instanceof Status && $result->isGood() ) ) { @@ -224,15 +238,27 @@ class SpecialEmailUser extends UnlistedSpecialPage { $string = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'askusername' ) ) . Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . Xml::openElement( 'fieldset' ) . - Html::rawElement( 'legend', null, wfMessage( 'emailtarget' )->parse() ) . - Xml::inputLabel( wfMessage( 'emailusername' )->text(), 'target', 'emailusertarget', 30, $name ) . ' ' . - Xml::submitButton( wfMessage( 'emailusernamesubmit' )->text() ) . + Html::rawElement( 'legend', null, $this->msg( 'emailtarget' )->parse() ) . + Xml::inputLabel( $this->msg( 'emailusername' )->text(), 'target', 'emailusertarget', 30, $name ) . ' ' . + Xml::submitButton( $this->msg( 'emailusernamesubmit' )->text() ) . Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n"; return $string; } /** + * Submit callback for an HTMLForm object, will simply call submit(). + * + * @since 1.20 + * @param $data array + * @param $form HTMLForm object + * @return Status|string|bool + */ + public static function uiSubmit( array $data, HTMLForm $form ) { + return self::submit( $data, $form->getContext() ); + } + + /** * Really send a mail. Permissions should have been checked using * getPermissionsError(). It is probably also a good * idea to check the edit token and ping limiter in advance. @@ -240,25 +266,22 @@ class SpecialEmailUser extends UnlistedSpecialPage { * @return Mixed: Status object, or potentially a String on error * or maybe even true on success if anything uses the EmailUser hook. */ - public static function submit( $data ) { - global $wgUser, $wgUserEmailUseReplyTo; + public static function submit( array $data, IContextSource $context ) { + global $wgUserEmailUseReplyTo; $target = self::getTarget( $data['Target'] ); if( !$target instanceof User ) { - return wfMsgExt( $target . 'text', 'parse' ); + return $context->msg( $target . 'text' )->parseAsBlock(); } $to = new MailAddress( $target ); - $from = new MailAddress( $wgUser ); + $from = new MailAddress( $context->getUser() ); $subject = $data['Subject']; $text = $data['Text']; // Add a standard footer and trim up trailing newlines $text = rtrim( $text ) . "\n\n-- \n"; - $text .= wfMsgExt( - 'emailuserfooter', - array( 'content', 'parsemag' ), - array( $from->name, $to->name ) - ); + $text .= $context->msg( 'emailuserfooter', + $from->name, $to->name )->inContentLanguage()->text(); $error = ''; if( !wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$text, &$error ) ) ) { @@ -302,11 +325,8 @@ class SpecialEmailUser extends UnlistedSpecialPage { // unless they are emailing themselves, in which case one // copy of the message is sufficient. if ( $data['CCMe'] && $to != $from ) { - $cc_subject = wfMsg( - 'emailccsubject', - $target->getName(), - $subject - ); + $cc_subject = $context->msg( 'emailccsubject' )->rawParams( + $target->getName(), $subject )->text(); wfRunHooks( 'EmailUserCC', array( &$from, &$from, &$cc_subject, &$text ) ); $ccStatus = UserMailer::send( $from, $from, $cc_subject, $text ); $status->merge( $ccStatus ); diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index d061389e..b4294b32 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -93,6 +93,13 @@ class SpecialExport extends SpecialPage { elseif( $request->getCheck( 'exportall' ) && $wgExportAllowAll ) { $this->doExport = true; $exportall = true; + + /* Although $page and $history are not used later on, we + nevertheless set them to avoid that PHP notices about using + undefined variables foul up our XML output (see call to + doExport(...) further down) */ + $page = ''; + $history = ''; } elseif( $request->wasPosted() && $par == '' ) { $page = $request->getText( 'pages' ); @@ -181,17 +188,26 @@ class SpecialExport extends SpecialPage { $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl( 'action=submit' ) ) ); - $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . ' '; - $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />'; + $form .= Xml::inputLabel( $this->msg( 'export-addcattext' )->text(), 'catname', 'catname', 40 ) . ' '; + $form .= Xml::submitButton( $this->msg( 'export-addcat' )->text(), array( 'name' => 'addcat' ) ) . '<br />'; if ( $wgExportFromNamespaces ) { - $form .= Xml::namespaceSelector( $nsindex, null, 'nsindex', wfMsg( 'export-addnstext' ) ) . ' '; - $form .= Xml::submitButton( wfMsg( 'export-addns' ), array( 'name' => 'addns' ) ) . '<br />'; + $form .= Html::namespaceSelector( + array( + 'selected' => $nsindex, + 'label' => $this->msg( 'export-addnstext' )->text() + ), array( + 'name' => 'nsindex', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ) . ' '; + $form .= Xml::submitButton( $this->msg( 'export-addns' )->text(), array( 'name' => 'addns' ) ) . '<br />'; } if ( $wgExportAllowAll ) { $form .= Xml::checkLabel( - wfMsg( 'exportall' ), + $this->msg( 'exportall' )->text(), 'exportall', 'exportall', $request->wasPosted() ? $request->getCheck( 'exportall' ) : false @@ -203,29 +219,29 @@ class SpecialExport extends SpecialPage { if( $wgExportAllowHistory ) { $form .= Xml::checkLabel( - wfMsg( 'exportcuronly' ), + $this->msg( 'exportcuronly' )->text(), 'curonly', 'curonly', $request->wasPosted() ? $request->getCheck( 'curonly' ) : true ) . '<br />'; } else { - $out->addHTML( wfMsgExt( 'exportnohistory', 'parse' ) ); + $out->addWikiMsg( 'exportnohistory' ); } $form .= Xml::checkLabel( - wfMsg( 'export-templates' ), + $this->msg( 'export-templates' )->text(), 'templates', 'wpExportTemplates', $request->wasPosted() ? $request->getCheck( 'templates' ) : false ) . '<br />'; if( $wgExportMaxLinkDepth || $this->userCanOverrideExportDepth() ) { - $form .= Xml::inputLabel( wfMsg( 'export-pagelinks' ), 'pagelink-depth', 'pagelink-depth', 20, 0 ) . '<br />'; + $form .= Xml::inputLabel( $this->msg( 'export-pagelinks' )->text(), 'pagelink-depth', 'pagelink-depth', 20, 0 ) . '<br />'; } // Enable this when we can do something useful exporting/importing image information. :) - //$form .= Xml::checkLabel( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />'; + //$form .= Xml::checkLabel( $this->msg( 'export-images' )->text(), 'images', 'wpExportImages', false ) . '<br />'; $form .= Xml::checkLabel( - wfMsg( 'export-download' ), + $this->msg( 'export-download' )->text(), 'wpDownload', 'wpDownload', $request->wasPosted() ? $request->getCheck( 'wpDownload' ) : true @@ -233,14 +249,14 @@ class SpecialExport extends SpecialPage { if ( $wgExportAllowListContributors ) { $form .= Xml::checkLabel( - wfMsg( 'exportlistauthors' ), + $this->msg( 'exportlistauthors' )->text(), 'listauthors', 'listauthors', $request->wasPosted() ? $request->getCheck( 'listauthors' ) : false ) . '<br />'; } - $form .= Xml::submitButton( wfMsg( 'export-submit' ), Linker::tooltipAndAccesskeyAttribs( 'export' ) ); + $form .= Xml::submitButton( $this->msg( 'export-submit' )->text(), Linker::tooltipAndAccesskeyAttribs( 'export' ) ); $form .= Xml::closeElement( 'form' ); $out->addHTML( $form ); @@ -439,7 +455,7 @@ class SpecialExport extends SpecialPage { private function getTemplates( $inputPages, $pageSet ) { return $this->getLinks( $inputPages, $pageSet, 'templatelinks', - array( 'tl_namespace AS namespace', 'tl_title AS title' ), + array( 'namespace' => 'tl_namespace', 'title' => 'tl_title' ), array( 'page_id=tl_from' ) ); } @@ -481,7 +497,7 @@ class SpecialExport extends SpecialPage { for( ; $depth > 0; --$depth ) { $pageSet = $this->getLinks( $inputPages, $pageSet, 'pagelinks', - array( 'pl_namespace AS namespace', 'pl_title AS title' ), + array( 'namespace' => 'pl_namespace', 'title' => 'pl_title' ), array( 'page_id=pl_from' ) ); $inputPages = array_keys( $pageSet ); @@ -503,13 +519,14 @@ class SpecialExport extends SpecialPage { $inputPages, $pageSet, 'imagelinks', - array( NS_FILE . ' AS namespace', 'il_to AS title' ), + array( 'namespace' => NS_FILE, 'title' => 'il_to' ), array( 'page_id=il_from' ) ); } /** * Expand a list of pages to include items used in those pages. + * @return array */ private function getLinks( $inputPages, $pageSet, $table, $fields, $join ) { $dbr = wfGetDB( DB_SLAVE ); diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php index 27d17f63..7e4bc9ce 100644 --- a/includes/specials/SpecialFewestrevisions.php +++ b/includes/specials/SpecialFewestrevisions.php @@ -44,10 +44,10 @@ class FewestrevisionsPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'revision', 'page' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'COUNT(*) AS value', - 'page_is_redirect AS redirect' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'COUNT(*)', + 'redirect' => 'page_is_redirect' ), 'conds' => array ( 'page_namespace' => MWNamespace::getContentNamespaces(), 'page_id = rev_page' ), 'options' => array ( 'HAVING' => 'COUNT(*) > 1', @@ -56,7 +56,7 @@ class FewestrevisionsPage extends QueryPage { // useful to remove this. People _do_ create pages // and never revise them, they aren't necessarily // redirects. - 'GROUP BY' => 'page_namespace, page_title, page_is_redirect' ) + 'GROUP BY' => array( 'page_namespace', 'page_title', 'page_is_redirect' ) ) ); } @@ -68,13 +68,15 @@ class FewestrevisionsPage extends QueryPage { /** * @param $skin Skin object * @param $result Object: database row + * @return String */ function formatResult( $skin, $result ) { global $wgContLang; $nt = Title::makeTitleSafe( $result->namespace, $result->title ); if( !$nt ) { - return '<!-- bad title -->'; + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); } $text = htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) ); @@ -82,7 +84,7 @@ class FewestrevisionsPage extends QueryPage { $nl = $this->msg( 'nrevisions' )->numParams( $result->value )->escaped(); $redirect = isset( $result->redirect ) && $result->redirect ? - ' - ' . wfMsgHtml( 'isredirect' ) : ''; + ' - ' . $this->msg( 'isredirect' )->escaped() : ''; $nlink = Linker::linkKnown( $nt, $nl, diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index 18d19db8..ccf8ba17 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -78,8 +78,8 @@ class FileDuplicateSearchPage extends QueryPage { return array( 'tables' => array( 'image' ), 'fields' => array( - 'img_name AS title', - 'img_sha1 AS value', + 'title' => 'img_name', + 'value' => 'img_sha1', 'img_user_text', 'img_timestamp' ), @@ -157,10 +157,24 @@ class FileDuplicateSearchPage extends QueryPage { ); } + $this->doBatchLookups( $dupes ); $this->showList( $dupes ); } } + function doBatchLookups( $list ) { + $batch = new LinkBatch(); + foreach( $list as $file ) { + $batch->addObj( $file->getTitle() ); + if( $file->isLocal() ) { + $userName = $file->getUser( 'text' ); + $batch->add( NS_USER, $userName ); + $batch->add( NS_USER_TALK, $userName ); + } + } + $batch->execute(); + } + /** * * @param Skin $skin @@ -178,7 +192,17 @@ class FileDuplicateSearchPage extends QueryPage { ); $userText = $result->getUser( 'text' ); - $user = Linker::link( Title::makeTitle( NS_USER, $userText ), $userText ); + if ( $result->isLocal() ) { + $userId = $result->getUser( 'id' ); + $user = Linker::userLink( $userId, $userText ); + $user .= $this->getContext()->msg( 'word-separator' )->plain(); + $user .= '<span style="white-space: nowrap;">'; + $user .= Linker::userToolLinks( $userId, $userText ); + $user .= '</span>'; + } else { + $user = htmlspecialchars( $userText ); + } + $time = $this->getLanguage()->userTimeAndDate( $result->getTimestamp(), $this->getUser() ); return "$plink . . $user . . $time"; diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php index 101a33f4..e0866504 100644 --- a/includes/specials/SpecialFilepath.php +++ b/includes/specials/SpecialFilepath.php @@ -78,10 +78,10 @@ class SpecialFilepath extends SpecialPage { $this->getOutput()->addHTML( Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'specialfilepath' ) ) . Html::openElement( 'fieldset' ) . - Html::element( 'legend', null, wfMsg( 'filepath' ) ) . + Html::element( 'legend', null, $this->msg( 'filepath' )->text() ) . Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::inputLabel( wfMsg( 'filepath-page' ), 'file', 'file', 25, is_object( $title ) ? $title->getText() : '' ) . ' ' . - Xml::submitButton( wfMsg( 'filepath-submit' ) ) . "\n" . + Xml::inputLabel( $this->msg( 'filepath-page' )->text(), 'file', 'file', 25, is_object( $title ) ? $title->getText() : '' ) . ' ' . + Xml::submitButton( $this->msg( 'filepath-submit' )->text() ) . "\n" . Html::closeElement( 'fieldset' ) . Html::closeElement( 'form' ) ); diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index a2380fbe..362fc5cf 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -33,6 +33,7 @@ class SpecialImport extends SpecialPage { private $interwiki = false; private $namespace; + private $rootpage = ''; private $frompage = ''; private $logcomment= false; private $history = true; @@ -100,6 +101,7 @@ class SpecialImport extends SpecialPage { $this->logcomment = $request->getText( 'log-comment' ); $this->pageLinkDepth = $wgExportMaxLinkDepth == 0 ? 0 : $request->getIntOrNull( 'pagelink-depth' ); + $this->rootpage = $request->getText( 'rootpage' );
$user = $this->getUser(); if ( !$user->matchEditToken( $request->getVal( 'editToken' ) ) ) { @@ -137,12 +139,20 @@ class SpecialImport extends SpecialPage { if( !$source->isGood() ) { $out->wrapWikiMsg( "<p class=\"error\">\n$1\n</p>", array( 'importfailed', $source->getWikiText() ) ); } else { - $out->addWikiMsg( "importstart" ); - $importer = new WikiImporter( $source->value ); if( !is_null( $this->namespace ) ) { $importer->setTargetNamespace( $this->namespace ); } + if( !is_null( $this->rootpage ) ) { + $statusRootPage = $importer->setTargetRootPage( $this->rootpage ); + if( !$statusRootPage->isGood() ) { + $out->wrapWikiMsg( "<p class=\"error\">\n$1\n</p>", array( 'import-options-wrong', $statusRootPage->getWikiText(), count( $statusRootPage->getErrorsArray() ) ) ); + return; + } + } + + $out->addWikiMsg( "importstart" ); + $reporter = new ImportReporter( $importer, $isUpload, $this->interwiki , $this->logcomment); $reporter->setContext( $this->getContext() ); $exception = false; @@ -177,18 +187,18 @@ class SpecialImport extends SpecialPage { $out = $this->getOutput(); if( $user->isAllowed( 'importupload' ) ) { - $out->addWikiMsg( "importtext" ); $out->addHTML( - Xml::fieldset( wfMsg( 'import-upload' ) ). + Xml::fieldset( $this->msg( 'import-upload' )->text() ). Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action, 'id' => 'mw-import-upload-form' ) ) . + $this->msg( 'importtext' )->parseAsBlock() . Html::hidden( 'action', 'submit' ) . Html::hidden( 'source', 'upload' ) . Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) . "<tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'import-upload-filename' ), 'xmlimport' ) . + Xml::label( $this->msg( 'import-upload-filename' )->text(), 'xmlimport' ) . "</td> <td class='mw-input'>" . Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' . @@ -196,7 +206,7 @@ class SpecialImport extends SpecialPage { </tr> <tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'import-comment' ), 'mw-import-comment' ) . + Xml::label( $this->msg( 'import-comment' )->text(), 'mw-import-comment' ) . "</td> <td class='mw-input'>" . Xml::input( 'log-comment', 50, '', @@ -204,9 +214,18 @@ class SpecialImport extends SpecialPage { "</td> </tr> <tr> + <td class='mw-label'>" . + Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'rootpage', 50, $this->rootpage, + array( 'id' => 'mw-interwiki-rootpage', 'type' => 'text' ) ) . ' ' . + "</td> + </tr> + <tr> <td></td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'uploadbtn' ) ) . + Xml::submitButton( $this->msg( 'uploadbtn' )->text() ) . "</td> </tr>" . Xml::closeElement( 'table' ). @@ -226,7 +245,7 @@ class SpecialImport extends SpecialPage { if( $wgExportMaxLinkDepth > 0 ) { $importDepth = "<tr> <td class='mw-label'>" . - wfMsgExt( 'export-pagelinks', 'parseinline' ) . + $this->msg( 'export-pagelinks' )->parse() . "</td> <td class='mw-input'>" . Xml::input( 'pagelink-depth', 3, 0 ) . @@ -235,16 +254,16 @@ class SpecialImport extends SpecialPage { } $out->addHTML( - Xml::fieldset( wfMsg( 'importinterwiki' ) ) . + Xml::fieldset( $this->msg( 'importinterwiki' )->text() ) . Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'mw-import-interwiki-form' ) ) . - wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) . + $this->msg( 'import-interwiki-text' )->parseAsBlock() . Html::hidden( 'action', 'submit' ) . Html::hidden( 'source', 'interwiki' ) . Html::hidden( 'editToken', $user->getEditToken() ) . Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) . "<tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'import-interwiki-source' ), 'interwiki' ) . + Xml::label( $this->msg( 'import-interwiki-source' )->text(), 'interwiki' ) . "</td> <td class='mw-input'>" . Xml::openElement( 'select', array( 'name' => 'interwiki' ) ) @@ -263,28 +282,37 @@ class SpecialImport extends SpecialPage { <td> </td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $this->history ) . + Xml::checkLabel( $this->msg( 'import-interwiki-history' )->text(), 'interwikiHistory', 'interwikiHistory', $this->history ) . "</td> </tr> <tr> <td> </td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'import-interwiki-templates' ), 'interwikiTemplates', 'interwikiTemplates', $this->includeTemplates ) . + Xml::checkLabel( $this->msg( 'import-interwiki-templates' )->text(), 'interwikiTemplates', 'interwikiTemplates', $this->includeTemplates ) . "</td> </tr> $importDepth <tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) . + Xml::label( $this->msg( 'import-interwiki-namespace' )->text(), 'namespace' ) . "</td> <td class='mw-input'>" . - Xml::namespaceSelector( $this->namespace, '' ) . + Html::namespaceSelector( + array( + 'selected' => $this->namespace, + 'all' => '', + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ) . "</td> </tr> <tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'import-comment' ), 'mw-interwiki-comment' ) . + Xml::label( $this->msg( 'import-comment' )->text(), 'mw-interwiki-comment' ) . "</td> <td class='mw-input'>" . Xml::input( 'log-comment', 50, '', @@ -292,10 +320,19 @@ class SpecialImport extends SpecialPage { "</td> </tr> <tr> + <td class='mw-label'>" . + Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'rootpage', 50, $this->rootpage, + array( 'id' => 'mw-interwiki-rootpage', 'type' => 'text' ) ) . ' ' . + "</td> + </tr> + <tr> <td> </td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'import-interwiki-submit' ), Linker::tooltipAndAccesskeyAttribs( 'import' ) ) . + Xml::submitButton( $this->msg( 'import-interwiki-submit' )->text(), Linker::tooltipAndAccesskeyAttribs( 'import' ) ) . "</td> </tr>" . Xml::closeElement( 'table' ). @@ -352,8 +389,6 @@ class ImportReporter extends ContextSource { * @return void */ function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) { - global $wgContLang; - $args = func_get_args(); call_user_func_array( $this->mOriginalPageOutCallback, $args ); @@ -364,30 +399,27 @@ class ImportReporter extends ContextSource { $this->mPageCount++; - $localCount = $this->getLanguage()->formatNum( $successCount ); - $contentCount = $wgContLang->formatNum( $successCount ); - if( $successCount > 0 ) { $this->getOutput()->addHTML( "<li>" . Linker::linkKnown( $title ) . " " . - wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) . + $this->msg( 'import-revision-count' )->numParams( $successCount )->escaped() . "</li>\n" ); $log = new LogPage( 'import' ); if( $this->mIsUpload ) { - $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ), - $contentCount ); + $detail = $this->msg( 'import-logentry-upload-detail' )->numParams( + $successCount )->inContentLanguage()->text(); if ( $this->reason ) { - $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; + $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason; } $log->addEntry( 'upload', $title, $detail ); } else { $interwiki = '[[:' . $this->mInterwiki . ':' . $origTitle->getPrefixedText() . ']]'; - $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ), - $contentCount, $interwiki ); + $detail = $this->msg( 'import-logentry-interwiki-detail' )->numParams( + $successCount )->params( $interwiki )->inContentLanguage()->text(); if ( $this->reason ) { - $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; + $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason; } $log->addEntry( 'interwiki', $title, $detail ); } @@ -395,7 +427,7 @@ class ImportReporter extends ContextSource { $comment = $detail; // quick $dbw = wfGetDB( DB_MASTER ); $latest = $title->getLatestRevID(); - $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true ); + $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleID(), $comment, true ); if (!is_null($nullRevision)) { $nullRevision->insertOn( $dbw ); $page = WikiPage::factory( $title ); @@ -405,15 +437,14 @@ class ImportReporter extends ContextSource { } } else { $this->getOutput()->addHTML( "<li>" . Linker::linkKnown( $title ) . " " . - wfMsgHtml( 'import-nonewrevisions' ) . "</li>\n" ); + $this->msg( 'import-nonewrevisions' )->escaped() . "</li>\n" ); } } function close() { $out = $this->getOutput(); if ( $this->mLogItemCount > 0 ) { - $msg = wfMsgExt( 'imported-log-entries', 'parseinline', - $this->getLanguage()->formatNum( $this->mLogItemCount ) ); + $msg = $this->msg( 'imported-log-entries' )->numParams( $this->mLogItemCount )->parse(); $out->addHTML( Xml::tags( 'li', null, $msg ) ); } elseif( $this->mPageCount == 0 && $this->mLogItemCount == 0 ) { $out->addHTML( "</ul>\n" ); diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php index d7e1655f..c217eccb 100644 --- a/includes/specials/SpecialJavaScriptTest.php +++ b/includes/specials/SpecialJavaScriptTest.php @@ -1,5 +1,29 @@ <?php - +/** + * Implements Special:JavaScriptTest + * + * 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 SpecialPage + */ + +/** + * @ingroup SpecialPage + */ class SpecialJavaScriptTest extends SpecialPage { /** @@ -37,26 +61,24 @@ class SpecialJavaScriptTest extends SpecialPage { // No framework specified if ( $par == '' ) { - $out->setPagetitle( wfMsgHtml( 'javascripttest' ) ); + $out->setPageTitle( $this->msg( 'javascripttest' ) ); $summary = $this->wrapSummaryHtml( - wfMsgHtml( 'javascripttest-pagetext-noframework' ) . $this->getFrameworkListHtml(), + $this->msg( 'javascripttest-pagetext-noframework' )->escaped() . $this->getFrameworkListHtml(), 'noframework' ); $out->addHtml( $summary ); // Matched! Display proper title and initialize the framework } elseif ( isset( self::$frameworks[$framework] ) ) { - $out->setPagetitle( wfMsgHtml( 'javascripttest-title', wfMsgHtml( "javascripttest-$framework-name" ) ) ); - $out->setSubtitle( - wfMessage( 'javascripttest-backlink' )->rawParams( Linker::linkKnown( $this->getTitle() ) )->escaped() - ); + $out->setPageTitle( $this->msg( 'javascripttest-title', $this->msg( "javascripttest-$framework-name" )->plain() ) ); + $out->setSubtitle( $this->msg( 'javascripttest-backlink' )->rawParams( Linker::linkKnown( $this->getTitle() ) ) ); $this->{self::$frameworks[$framework]}(); // Framework not found, display error } else { - $out->setPagetitle( wfMsgHtml( 'javascripttest' ) ); + $out->setPageTitle( $this->msg( 'javascripttest' ) ); $summary = $this->wrapSummaryHtml( '<p class="error">' - . wfMsgHtml( 'javascripttest-pagetext-unknownframework', $par ) + . $this->msg( 'javascripttest-pagetext-unknownframework', $par )->escaped() . '</p>' . $this->getFrameworkListHtml(), 'unknownframework' @@ -75,11 +97,11 @@ class SpecialJavaScriptTest extends SpecialPage { $list .= Html::rawElement( 'li', array(), - Linker::link( $this->getTitle( $framework ), wfMsgHtml( "javascripttest-$framework-name" ) ) + Linker::link( $this->getTitle( $framework ), $this->msg( "javascripttest-$framework-name" )->escaped() ) ); } $list .= '</ul>'; - $msg = wfMessage( 'javascripttest-pagetext-frameworks' )->rawParams( $list )->parseAsBlock(); + $msg = $this->msg( 'javascripttest-pagetext-frameworks' )->rawParams( $list )->parseAsBlock(); return $msg; } @@ -90,6 +112,7 @@ class SpecialJavaScriptTest extends SpecialPage { * be thrown. * @param $html String: The raw HTML. * @param $state String: State, one of 'noframework', 'unknownframework' or 'frameworkfound' + * @return string */ private function wrapSummaryHtml( $html, $state ) { $validStates = array( 'noframework', 'unknownframework', 'frameworkfound' ); @@ -106,7 +129,7 @@ class SpecialJavaScriptTest extends SpecialPage { * Initialize the page for QUnit. */ private function initQUnitTesting() { - global $wgJavaScriptTestConfig, $wgLang; + global $wgJavaScriptTestConfig; $out = $this->getOutput(); @@ -114,11 +137,11 @@ class SpecialJavaScriptTest extends SpecialPage { $qunitTestModules = $out->getResourceLoader()->getTestModuleNames( 'qunit' ); $out->addModules( $qunitTestModules ); - $summary = wfMessage( 'javascripttest-qunit-intro' ) + $summary = $this->msg( 'javascripttest-qunit-intro' ) ->params( $wgJavaScriptTestConfig['qunit']['documentation'] ) ->parseAsBlock(); - $header = wfMessage( 'javascripttest-qunit-heading' )->escaped(); - $userDir = $wgLang->getDir(); + $header = $this->msg( 'javascripttest-qunit-heading' )->escaped(); + $userDir = $this->getLanguage()->getDir(); $baseHtml = <<<HTML <div class="mw-content-ltr"> @@ -132,6 +155,14 @@ class SpecialJavaScriptTest extends SpecialPage { HTML; $out->addHtml( $this->wrapSummaryHtml( $summary, 'frameworkfound' ) . $baseHtml ); + // This special page is disabled by default ($wgEnableJavaScriptTest), and contains + // no sensitive data. In order to allow TestSwarm to embed it into a test client window, + // we need to allow iframing of this page. + $out->allowClickjacking(); + + // Used in ./tests/qunit/data/testrunner.js, see also documentation of + // $wgJavaScriptTestConfig in DefaultSettings.php + $out->addJsConfigVars( 'QUnitTestSwarmInjectJSPath', $wgJavaScriptTestConfig['qunit']['testswarm-injectjs'] ); } public function isListed(){ diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index d3ab2f04..0810ee77 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -88,13 +88,22 @@ class LinkSearchPage extends QueryPage { $s = Xml::openElement( 'form', array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $GLOBALS['wgScript'] ) ) . Html::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) . '<fieldset>' . - Xml::element( 'legend', array(), wfMsg( 'linksearch' ) ) . - Xml::inputLabel( wfMsg( 'linksearch-pat' ), 'target', 'target', 50, $target ) . ' '; + Xml::element( 'legend', array(), $this->msg( 'linksearch' )->text() ) . + Xml::inputLabel( $this->msg( 'linksearch-pat' )->text(), 'target', 'target', 50, $target ) . ' '; if ( !$wgMiserMode ) { - $s .= Xml::label( wfMsg( 'linksearch-ns' ), 'namespace' ) . ' ' . - Xml::namespaceSelector( $namespace, '' ); + $s .= Html::namespaceSelector( + array( + 'selected' => $namespace, + 'all' => '', + 'label' => $this->msg( 'linksearch-ns' )->text() + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ); } - $s .= Xml::submitButton( wfMsg( 'linksearch-ok' ) ) . + $s .= Xml::submitButton( $this->msg( 'linksearch-ok' )->text() ) . '</fieldset>' . Xml::closeElement( 'form' ); $out->addHTML( $s ); @@ -112,6 +121,7 @@ class LinkSearchPage extends QueryPage { /** * Disable RSS/Atom feeds + * @return bool */ function isSyndicated() { return false; @@ -161,9 +171,9 @@ class LinkSearchPage extends QueryPage { $like = $dbr->buildLike( $stripped ); $retval = array ( 'tables' => array ( 'page', 'externallinks' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'el_index AS value', 'el_to AS url' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'el_index', 'url' => 'el_to' ), 'conds' => array ( 'page_id = el_from', "$clause $like" ), 'options' => array( 'USE INDEX' => $clause ) @@ -180,7 +190,7 @@ class LinkSearchPage extends QueryPage { $pageLink = Linker::linkKnown( $title ); $urlLink = Linker::makeExternalLink( $url, $url ); - return wfMsgHtml( 'linksearch-line', $urlLink, $pageLink ); + return $this->msg( 'linksearch-line' )->rawParams( $urlLink, $pageLink )->escaped(); } /** @@ -203,6 +213,7 @@ class LinkSearchPage extends QueryPage { * We do a truncated index search, so the optimizer won't trust * it as good enough for optimizing sort. The implicit ordering * from the scan will usually do well enough for our needs. + * @return array */ function getOrderFields() { return array(); diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index b5754991..cc055221 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -107,15 +107,15 @@ class ImageListPager extends TablePager { if ( !$this->mFieldNames ) { global $wgMiserMode; $this->mFieldNames = array( - 'img_timestamp' => wfMsg( 'listfiles_date' ), - 'img_name' => wfMsg( 'listfiles_name' ), - 'thumb' => wfMsg( 'listfiles_thumb' ), - 'img_size' => wfMsg( 'listfiles_size' ), - 'img_user_text' => wfMsg( 'listfiles_user' ), - 'img_description' => wfMsg( 'listfiles_description' ), + 'img_timestamp' => $this->msg( 'listfiles_date' )->text(), + 'img_name' => $this->msg( 'listfiles_name' )->text(), + 'thumb' => $this->msg( 'listfiles_thumb' )->text(), + 'img_size' => $this->msg( 'listfiles_size' )->text(), + 'img_user_text' => $this->msg( 'listfiles_user' )->text(), + 'img_description' => $this->msg( 'listfiles_description' )->text(), ); if( !$wgMiserMode ) { - $this->mFieldNames['count'] = wfMsg( 'listfiles_count' ); + $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text(); } } return $this->mFieldNames; @@ -156,9 +156,8 @@ class ImageListPager extends TablePager { if( $dbr->implicitGroupby() ) { $options = array( 'GROUP BY' => 'img_name' ); } else { - $columnlist = implode( ',', - preg_grep( '/^img/', array_keys( $this->getFieldNames() ) ) ); - $options = array( 'GROUP BY' => "img_user, $columnlist" ); + $columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) ); + $options = array( 'GROUP BY' => array_merge( array( 'img_user' ), $columnlist ) ); } $join_conds = array( 'oldimage' => array( 'LEFT JOIN', 'oi_name = img_name' ) ); } @@ -175,20 +174,14 @@ class ImageListPager extends TablePager { return 'img_timestamp'; } - function getStartBody() { - # Do a link batch query for user pages - if ( $this->mResult->numRows() ) { - $lb = new LinkBatch; - $this->mResult->seek( 0 ); - foreach ( $this->mResult as $row ) { - if ( $row->img_user ) { - $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) ); - } - } - $lb->execute(); + function doBatchLookups() { + $userIds = array(); + $this->mResult->seek( 0 ); + foreach ( $this->mResult as $row ) { + $userIds[] = $row->img_user; } - - return parent::getStartBody(); + # Do a link batch query for names and userpages + UserCache::singleton()->doQuery( $userIds, array( 'userpage' ), __METHOD__ ); } function formatValue( $field, $value ) { @@ -198,10 +191,10 @@ class ImageListPager extends TablePager { $thumb = $file->transform( array( 'width' => 180, 'height' => 360 ) ); return $thumb->toHtml( array( 'desc-link' => true ) ); case 'img_timestamp': - return htmlspecialchars( $this->getLanguage()->timeanddate( $value, true ) ); + return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) ); case 'img_name': static $imgfile = null; - if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' ); + if ( $imgfile === null ) $imgfile = $this->msg( 'imgfile' )->text(); // Weird files can maybe exist? Bug 22227 $filePage = Title::makeTitleSafe( NS_FILE, $value ); @@ -211,15 +204,17 @@ class ImageListPager extends TablePager { array( 'href' => wfLocalFile( $filePage )->getURL() ), $imgfile ); - return "$link ($download)"; + $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped(); + return "$link $download"; } else { return htmlspecialchars( $value ); } case 'img_user_text': if ( $this->mCurrentRow->img_user ) { + $name = User::whoIs( $this->mCurrentRow->img_user ); $link = Linker::link( - Title::makeTitle( NS_USER, $value ), - htmlspecialchars( $value ) + Title::makeTitle( NS_USER, $name ), + htmlspecialchars( $name ) ); } else { $link = htmlspecialchars( $value ); @@ -253,9 +248,10 @@ class ImageListPager extends TablePager { ) ); return Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' ) ) . - Xml::fieldset( wfMsg( 'listfiles' ) ) . + Xml::fieldset( $this->msg( 'listfiles' )->text() ) . + Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . Xml::buildForm( $inputForm, 'table_pager_limit_submit' ) . - $this->getHiddenFields( array( 'limit', 'ilsearch', 'user' ) ) . + $this->getHiddenFields( array( 'limit', 'ilsearch', 'user', 'title' ) ) . Html::closeElement( 'fieldset' ) . Html::closeElement( 'form' ) . "\n"; } @@ -290,4 +286,8 @@ class ImageListPager extends TablePager { } return $queries; } + + function getTitle() { + return SpecialPage::getTitleFor( 'Listfiles' ); + } } diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php index 91d8ed87..1f95c225 100644 --- a/includes/specials/SpecialListgrouprights.php +++ b/includes/specials/SpecialListgrouprights.php @@ -54,8 +54,8 @@ class SpecialListGroupRights extends SpecialPage { $out->addHTML( Xml::openElement( 'table', array( 'class' => 'wikitable mw-listgrouprights-table' ) ) . '<tr>' . - Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) . - Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) . + Xml::element( 'th', null, $this->msg( 'listgrouprights-group' )->text() ) . + Xml::element( 'th', null, $this->msg( 'listgrouprights-rights' )->text() ) . '</tr>' ); @@ -77,10 +77,10 @@ class SpecialListGroupRights extends SpecialPage { ? 'all' : $group; - $msg = wfMessage( 'group-' . $groupname ); + $msg = $this->msg( 'group-' . $groupname ); $groupnameLocalized = !$msg->isBlank() ? $msg->text() : $groupname; - $msg = wfMessage( 'grouppage-' . $groupname )->inContentLanguage(); + $msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage(); $grouppageLocalized = !$msg->isBlank() ? $msg->text() : MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname; @@ -99,12 +99,12 @@ class SpecialListGroupRights extends SpecialPage { // Link to Special:listusers for implicit group 'user' $grouplink = '<br />' . Linker::linkKnown( SpecialPage::getTitleFor( 'Listusers' ), - wfMsgHtml( 'listgrouprights-members' ) + $this->msg( 'listgrouprights-members' )->escaped() ); } elseif ( !in_array( $group, $wgImplicitGroups ) ) { $grouplink = '<br />' . Linker::linkKnown( SpecialPage::getTitleFor( 'Listusers' ), - wfMsgHtml( 'listgrouprights-members' ), + $this->msg( 'listgrouprights-members' )->escaped(), array(), array( 'group' => $group ) ); @@ -152,59 +152,59 @@ class SpecialListGroupRights extends SpecialPage { foreach( $permissions as $permission => $granted ) { //show as granted only if it isn't revoked to prevent duplicate display of permissions if( $granted && ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) ) { - $description = wfMsgExt( 'listgrouprights-right-display', array( 'parseinline' ), + $description = $this->msg( 'listgrouprights-right-display', User::getRightDescription( $permission ), '<span class="mw-listgrouprights-right-name">' . $permission . '</span>' - ); + )->parse(); $r[] = $description; } } foreach( $revoke as $permission => $revoked ) { if( $revoked ) { - $description = wfMsgExt( 'listgrouprights-right-revoked', array( 'parseinline' ), + $description = $this->msg( 'listgrouprights-right-revoked', User::getRightDescription( $permission ), '<span class="mw-listgrouprights-right-name">' . $permission . '</span>' - ); + )->parse(); $r[] = $description; } } sort( $r ); $lang = $this->getLanguage(); if( $add === true ){ - $r[] = wfMsgExt( 'listgrouprights-addgroup-all', array( 'escape' ) ); + $r[] = $this->msg( 'listgrouprights-addgroup-all' )->escaped(); } elseif( is_array( $add ) && count( $add ) ) { $add = array_values( array_unique( $add ) ); - $r[] = wfMsgExt( 'listgrouprights-addgroup', array( 'parseinline' ), + $r[] = $this->msg( 'listgrouprights-addgroup', $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), count( $add ) - ); + )->parse(); } if( $remove === true ){ - $r[] = wfMsgExt( 'listgrouprights-removegroup-all', array( 'escape' ) ); + $r[] = $this->msg( 'listgrouprights-removegroup-all' )->escaped(); } elseif( is_array( $remove ) && count( $remove ) ) { $remove = array_values( array_unique( $remove ) ); - $r[] = wfMsgExt( 'listgrouprights-removegroup', array( 'parseinline' ), + $r[] = $this->msg( 'listgrouprights-removegroup', $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), count( $remove ) - ); + )->parse(); } if( $addSelf === true ){ - $r[] = wfMsgExt( 'listgrouprights-addgroup-self-all', array( 'escape' ) ); + $r[] = $this->msg( 'listgrouprights-addgroup-self-all' )->escaped(); } elseif( is_array( $addSelf ) && count( $addSelf ) ) { $addSelf = array_values( array_unique( $addSelf ) ); - $r[] = wfMsgExt( 'listgrouprights-addgroup-self', array( 'parseinline' ), + $r[] = $this->msg( 'listgrouprights-addgroup-self', $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ), count( $addSelf ) - ); + )->parse(); } if( $removeSelf === true ){ - $r[] = wfMsgExt( 'listgrouprights-removegroup-self-all', array( 'escape' ) ); + $r[] = $this->msg( 'listgrouprights-removegroup-self-all' )->parse(); } elseif( is_array( $removeSelf ) && count( $removeSelf ) ) { $removeSelf = array_values( array_unique( $removeSelf ) ); - $r[] = wfMsgExt( 'listgrouprights-removegroup-self', array( 'parseinline' ), + $r[] = $this->msg( 'listgrouprights-removegroup-self', $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ), count( $removeSelf ) - ); + )->parse(); } if( empty( $r ) ) { return ''; diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php index f9cf3e6e..fe338a08 100644 --- a/includes/specials/SpecialListredirects.php +++ b/includes/specials/SpecialListredirects.php @@ -41,14 +41,14 @@ class ListredirectsPage extends QueryPage { function getQueryInfo() { return array( 'tables' => array( 'p1' => 'page', 'redirect', 'p2' => 'page' ), - 'fields' => array( 'p1.page_namespace AS namespace', - 'p1.page_title AS title', - 'p1.page_title AS value', + 'fields' => array( 'namespace' => 'p1.page_namespace', + 'title' => 'p1.page_title', + 'value' => 'p1.page_title', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki', - 'p2.page_id AS redirid' ), + 'redirid' => 'p2.page_id' ), 'conds' => array( 'p1.page_is_redirect' => 1 ), 'join_conds' => array( 'redirect' => array( 'LEFT JOIN', 'rd_from=p1.page_id' ), diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index d743712d..1089fbbe 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -34,7 +34,11 @@ */ class UsersPager extends AlphabeticPager { - function __construct( IContextSource $context = null, $par = null ) { + /** + * @param $context IContextSource + * @param $par null|array + */ + function __construct( IContextSource $context = null, $par = null, $including = null ) { if ( $context ) { $this->setContext( $context ); } @@ -58,6 +62,7 @@ class UsersPager extends AlphabeticPager { } $this->editsOnly = $request->getBool( 'editsOnly' ); $this->creationSort = $request->getBool( 'creationSort' ); + $this->including = $including; $this->requestedUser = ''; if ( $un != '' ) { @@ -69,15 +74,21 @@ class UsersPager extends AlphabeticPager { parent::__construct(); } + /** + * @return string + */ function getIndexField() { return $this->creationSort ? 'user_id' : 'user_name'; } + /** + * @return Array + */ function getQueryInfo() { $dbr = wfGetDB( DB_SLAVE ); $conds = array(); // Don't show hidden names - if( !$this->getUser()->isAllowed('hideuser') ) { + if( !$this->getUser()->isAllowed( 'hideuser' ) ) { $conds[] = 'ipb_deleted IS NULL'; } @@ -105,18 +116,22 @@ class UsersPager extends AlphabeticPager { $query = array( 'tables' => array( 'user', 'user_groups', 'ipblocks'), 'fields' => array( - $this->creationSort ? 'MAX(user_name) AS user_name' : 'user_name', - $this->creationSort ? 'user_id' : 'MAX(user_id) AS user_id', - 'MAX(user_editcount) AS edits', - 'COUNT(ug_group) AS numgroups', - 'MAX(ug_group) AS singlegroup', // the usergroup if there is only one - 'MIN(user_registration) AS creation', - 'MAX(ipb_deleted) AS ipb_deleted' // block/hide status + 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name', + 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)', + 'edits' => 'MAX(user_editcount)', + 'numgroups' => 'COUNT(ug_group)', + 'singlegroup' => 'MAX(ug_group)', // the usergroup if there is only one + 'creation' => 'MIN(user_registration)', + 'ipb_deleted' => 'MAX(ipb_deleted)' // block/hide status ), 'options' => $options, 'join_conds' => array( 'user_groups' => array( 'LEFT JOIN', 'user_id=ug_user' ), - 'ipblocks' => array( 'LEFT JOIN', 'user_id=ipb_user AND ipb_deleted=1 AND ipb_auto=0' ), + 'ipblocks' => array( 'LEFT JOIN', array( + 'user_id=ipb_user', + 'ipb_deleted' => 1, + 'ipb_auto' => 0 + )), ), 'conds' => $conds ); @@ -125,95 +140,101 @@ class UsersPager extends AlphabeticPager { return $query; } + /** + * @param $row Object + * @return String + */ function formatRow( $row ) { - if ($row->user_id == 0) #Bug 16487 + if ( $row->user_id == 0 ) { #Bug 16487 return ''; + } - $userPage = Title::makeTitle( NS_USER, $row->user_name ); - $name = Linker::link( $userPage, htmlspecialchars( $userPage->getText() ) ); + $userName = $row->user_name; + + $ulinks = Linker::userLink( $row->user_id, $userName ); + $ulinks .= Linker::userToolLinks( $row->user_id, $userName ); $lang = $this->getLanguage(); + $groups = ''; $groups_list = self::getGroups( $row->user_id ); - if( count( $groups_list ) > 0 ) { + if( !$this->including && count( $groups_list ) > 0 ) { $list = array(); foreach( $groups_list as $group ) - $list[] = self::buildGroupLink( $group, $userPage->getText() ); + $list[] = self::buildGroupLink( $group, $userName ); $groups = $lang->commaList( $list ); - } else { - $groups = ''; } - $item = $lang->specialList( $name, $groups ); + $item = $lang->specialList( $ulinks, $groups ); if( $row->ipb_deleted ) { $item = "<span class=\"deleted\">$item</span>"; } + $edits = ''; global $wgEdititis; - if ( $wgEdititis ) { - $editCount = $lang->formatNum( $row->edits ); - $edits = ' [' . wfMsgExt( 'usereditcount', array( 'parsemag', 'escape' ), $editCount ) . ']'; - } else { - $edits = ''; + if ( !$this->including && $wgEdititis ) { + $edits = ' [' . $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped() . ']'; } $created = ''; # Some rows may be NULL - if( $row->creation ) { - $d = $lang->date( wfTimestamp( TS_MW, $row->creation ), true ); - $t = $lang->time( wfTimestamp( TS_MW, $row->creation ), true ); - $created = ' (' . wfMsgExt( 'usercreated', array( 'parsemag', 'escape' ), $d, $t, $row->user_name ) . ')'; + if( !$this->including && $row->creation ) { + $user = $this->getUser(); + $d = $lang->userDate( $row->creation, $user ); + $t = $lang->userTime( $row->creation, $user ); + $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped(); + $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped(); } wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) ); - return "<li>{$item}{$edits}{$created}</li>"; + return Html::rawElement( 'li', array(), "{$item}{$edits}{$created}" ); } - function getBody() { - if( !$this->mQueryDone ) { - $this->doQuery(); - } - $this->mResult->rewind(); - $batch = new LinkBatch; + function doBatchLookups() { + $batch = new LinkBatch(); + # Give some pointers to make user links foreach ( $this->mResult as $row ) { - $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); + $batch->add( NS_USER, $row->user_name ); + $batch->add( NS_USER_TALK, $row->user_name ); } $batch->execute(); $this->mResult->rewind(); - return parent::getBody(); } + /** + * @return string + */ function getPageHeader( ) { global $wgScript; - // @todo Add a PrefixedBaseDBKey + list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() ); # Form tag $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listusers-form' ) ) . - Xml::fieldset( wfMsg( 'listusers' ) ) . + Xml::fieldset( $this->msg( 'listusers' )->text() ) . Html::hidden( 'title', $self ); # Username field - $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' . + $out .= Xml::label( $this->msg( 'listusersfrom' )->text(), 'offset' ) . ' ' . Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' '; # Group drop-down list - $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' . + $out .= Xml::label( $this->msg( 'group' )->text(), 'group' ) . ' ' . Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) . - Xml::option( wfMsg( 'group-all' ), '' ); + Xml::option( $this->msg( 'group-all' )->text(), '' ); foreach( $this->getAllGroups() as $group => $groupText ) $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup ); $out .= Xml::closeElement( 'select' ) . '<br />'; - $out .= Xml::checkLabel( wfMsg('listusers-editsonly'), 'editsOnly', 'editsOnly', $this->editsOnly ); + $out .= Xml::checkLabel( $this->msg( 'listusers-editsonly' )->text(), 'editsOnly', 'editsOnly', $this->editsOnly ); $out .= ' '; - $out .= Xml::checkLabel( wfMsg('listusers-creationsort'), 'creationSort', 'creationSort', $this->creationSort ); + $out .= Xml::checkLabel( $this->msg( 'listusers-creationsort' )->text(), 'creationSort', 'creationSort', $this->creationSort ); $out .= '<br />'; wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) ); # Submit button and form bottom $out .= Html::hidden( 'limit', $this->mLimit ); - $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ); + $out .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ); wfRunHooks( 'SpecialListusersHeader', array( $this, &$out ) ); $out .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); @@ -240,10 +261,12 @@ class UsersPager extends AlphabeticPager { */ function getDefaultQuery() { $query = parent::getDefaultQuery(); - if( $this->requestedGroup != '' ) + if( $this->requestedGroup != '' ) { $query['group'] = $this->requestedGroup; - if( $this->requestedUser != '' ) + } + if( $this->requestedUser != '' ) { $query['username'] = $this->requestedUser; + } wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) ); return $query; } @@ -282,6 +305,7 @@ class SpecialListUsers extends SpecialPage { */ public function __construct() { parent::__construct( 'Listusers' ); + $this->mIncludable = true; } /** @@ -293,18 +317,22 @@ class SpecialListUsers extends SpecialPage { $this->setHeaders(); $this->outputHeader(); - $up = new UsersPager( $this->getContext(), $par ); + $up = new UsersPager( $this->getContext(), $par, $this->including() ); # getBody() first to check, if empty $usersbody = $up->getBody(); - $s = $up->getPageHeader(); + $s = ''; + if ( !$this->including() ) { + $s = $up->getPageHeader(); + } + if( $usersbody ) { $s .= $up->getNavigationBar(); $s .= Html::rawElement( 'ul', array(), $usersbody ); $s .= $up->getNavigationBar(); } else { - $s .= wfMessage( 'listusers-noresult' )->parseAsBlock(); + $s .= $this->msg( 'listusers-noresult' )->parseAsBlock(); } $this->getOutput()->addHTML( $s ); diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php index c1453518..d71ac6e1 100644 --- a/includes/specials/SpecialLockdb.php +++ b/includes/specials/SpecialLockdb.php @@ -87,13 +87,11 @@ class SpecialLockdb extends FormSpecialPage { } fwrite( $fp, $data['Reason'] ); $timestamp = wfTimestampNow(); - fwrite( $fp, "\n<p>" . wfMsgExt( - 'lockedbyandtime', - array( 'content', 'parsemag' ), + fwrite( $fp, "\n<p>" . $this->msg( 'lockedbyandtime', $this->getUser()->getName(), - $wgContLang->date( $timestamp ), - $wgContLang->time( $timestamp ) - ) . "</p>\n" ); + $wgContLang->date( $timestamp, false, false ), + $wgContLang->time( $timestamp, false, false ) + )->inContentLanguage()->text() . "</p>\n" ); fclose( $fp ); return Status::newGood(); diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index 64190df1..7800e566 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -33,7 +33,7 @@ class SpecialLog extends SpecialPage { /** * List log type for which the target is a user * Thus if the given target is in NS_MAIN we can alter it to be an NS_USER - * Title user instead. + * Title user instead. */ private $typeOnUser = array( 'block', @@ -47,7 +47,7 @@ class SpecialLog extends SpecialPage { public function execute( $par ) { global $wgLogRestrictions; - + $this->setHeaders(); $this->outputHeader(); @@ -65,7 +65,7 @@ class SpecialLog extends SpecialPage { // Set values $opts->fetchValuesFromRequest( $this->getRequest() ); - if ( $par ) { + if ( $par !== null ) { $this->parseParams( $opts, (string)$par ); } @@ -131,7 +131,7 @@ class SpecialLog extends SpecialPage { private function show( FormOptions $opts, array $extraConds ) { # Create a LogPager item to get the results and a LogEventsList item to format them... - $loglist = new LogEventsList( $this->getSkin(), $this->getOutput(), 0 ); + $loglist = new LogEventsList( $this->getContext(), null, LogEventsList::USE_REVDEL_CHECKBOXES ); $pager = new LogPager( $loglist, $opts->getValue( 'type' ), $opts->getValue( 'user' ), $opts->getValue( 'page' ), $opts->getValue( 'pattern' ), $extraConds, $opts->getValue( 'year' ), $opts->getValue( 'month' ), $opts->getValue( 'tagfilter' ) ); @@ -152,9 +152,7 @@ class SpecialLog extends SpecialPage { if ( $logBody ) { $this->getOutput()->addHTML( $pager->getNavigationBar() . - $loglist->beginLogEventsList() . - $logBody . - $loglist->endLogEventsList() . + $this->getRevisionButton( $loglist->beginLogEventsList() . $logBody . $loglist->endLogEventsList() ) . $pager->getNavigationBar() ); } else { @@ -162,6 +160,29 @@ class SpecialLog extends SpecialPage { } } + private function getRevisionButton( $formcontents ) { + # If the user doesn't have the ability to delete log entries, don't bother showing him/her the button. + if ( !$this->getUser()->isAllowedAll( 'deletedhistory', 'deletelogentry' ) ) { + return $formcontents; + } + + # Show button to hide log entries + global $wgScript; + $s = Html::openElement( 'form', array( 'action' => $wgScript, 'id' => 'mw-log-deleterevision-submit' ) ) . "\n"; + $s .= Html::hidden( 'title', SpecialPage::getTitleFor( 'Revisiondelete' ) ) . "\n"; + $s .= Html::hidden( 'target', SpecialPage::getTitleFor( 'Log' ) ) . "\n"; + $s .= Html::hidden( 'type', 'logging' ) . "\n"; + $button = Html::element( 'button', + array( 'type' => 'submit', 'class' => "deleterevision-log-submit mw-log-deleterevision-button" ), + $this->msg( 'showhideselectedlogentries' )->text() + ) . "\n"; + $s .= $button . $formcontents . $button; + $s .= Html::closeElement( 'form' ); + + return $s; + } + + /** * Set page title and show header for this log type * @param $type string diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php index 0800e43c..763bbdb1 100644 --- a/includes/specials/SpecialLonelypages.php +++ b/includes/specials/SpecialLonelypages.php @@ -34,7 +34,7 @@ class LonelyPagesPage extends PageQueryPage { } function getPageHeader() { - return wfMsgExt( 'lonelypagestext', array( 'parse' ) ); + return $this->msg( 'lonelypagestext' )->parseAsBlock(); } function sortDescending() { @@ -50,9 +50,9 @@ class LonelyPagesPage extends PageQueryPage { return array ( 'tables' => array ( 'page', 'pagelinks', 'templatelinks' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'page_title AS value' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' ), 'conds' => array ( 'pl_namespace IS NULL', 'page_namespace' => MWNamespace::getContentNamespaces(), 'page_is_redirect' => 0, diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index 2213ffa4..104c653f 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -45,9 +45,9 @@ class MIMEsearchPage extends QueryPage { public function getQueryInfo() { return array( 'tables' => array( 'image' ), - 'fields' => array( "'" . NS_FILE . "' AS namespace", - 'img_name AS title', - 'img_major_mime AS value', + 'fields' => array( 'namespace' => NS_FILE, + 'title' => 'img_name', + 'value' => 'img_major_mime', 'img_size', 'img_width', 'img_height', @@ -59,17 +59,19 @@ class MIMEsearchPage extends QueryPage { } function execute( $par ) { + global $wgScript; + $mime = $par ? $par : $this->getRequest()->getText( 'mime' ); $this->setHeaders(); $this->outputHeader(); $this->getOutput()->addHTML( - Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => SpecialPage::getTitleFor( 'MIMEsearch' )->getLocalUrl() ) ) . + Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgScript ) ) . Xml::openElement( 'fieldset' ) . - Html::hidden( 'title', SpecialPage::getTitleFor( 'MIMEsearch' )->getPrefixedText() ) . - Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) . - Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' . - Xml::submitButton( wfMsg( 'ilsubmit' ) ) . + Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . + Xml::element( 'legend', null, $this->msg( 'mimesearch' )->text() ) . + Xml::inputLabel( $this->msg( 'mimetype' )->text(), 'mime', 'mime', 20, $mime ) . ' ' . + Xml::submitButton( $this->msg( 'ilsubmit' )->text() ) . Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) ); @@ -93,17 +95,16 @@ class MIMEsearchPage extends QueryPage { htmlspecialchars( $text ) ); - $download = Linker::makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) ); + $download = Linker::makeMediaLinkObj( $nt, $this->msg( 'download' )->escaped() ); + $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped(); $lang = $this->getLanguage(); $bytes = htmlspecialchars( $lang->formatSize( $result->img_size ) ); - $dimensions = htmlspecialchars( wfMsg( 'widthheight', - $lang->formatNum( $result->img_width ), - $lang->formatNum( $result->img_height ) - ) ); + $dimensions = $this->msg( 'widthheight' )->numParams( $result->img_width, + $result->img_height )->escaped(); $user = Linker::link( Title::makeTitle( NS_USER, $result->img_user_text ), htmlspecialchars( $result->img_user_text ) ); - $time = htmlspecialchars( $lang->timeanddate( $result->img_timestamp ) ); + $time = htmlspecialchars( $lang->userTimeAndDate( $result->img_timestamp, $this->getUser() ) ); - return "($download) $plink . . $dimensions . . $bytes . . $user . . $time"; + return "$download $plink . . $dimensions . . $bytes . . $user . . $time"; } /** diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index 19650da9..1f057499 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -76,7 +76,7 @@ class SpecialMergeHistory extends SpecialPage { function preCacheMessages() { // Precache various messages if( !isset( $this->message ) ) { - $this->message['last'] = wfMsgExt( 'last', array( 'escape' ) ); + $this->message['last'] = $this->msg( 'last' )->escaped(); } } @@ -90,7 +90,8 @@ class SpecialMergeHistory extends SpecialPage { $this->outputHeader(); if( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) { - return $this->merge(); + $this->merge(); + return; } if ( !$this->mSubmitted ) { @@ -100,23 +101,23 @@ class SpecialMergeHistory extends SpecialPage { $errors = array(); if ( !$this->mTargetObj instanceof Title ) { - $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) ); + $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock(); } elseif( !$this->mTargetObj->exists() ) { - $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ), + $errors[] = $this->msg( 'mergehistory-no-source', array( 'parse' ), wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) - ); + )->parseAsBlock(); } if ( !$this->mDestObj instanceof Title ) { - $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) ); + $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock(); } elseif( !$this->mDestObj->exists() ) { - $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ), + $errors[] = $this->msg( 'mergehistory-no-destination', array( 'parse' ), wfEscapeWikiText( $this->mDestObj->getPrefixedText() ) - ); + )->parseAsBlock(); } if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) { - $errors[] = wfMsgExt( 'mergehistory-same-destination', array( 'parse' ) ); + $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock(); } if ( count( $errors ) ) { @@ -139,19 +140,19 @@ class SpecialMergeHistory extends SpecialPage { 'action' => $wgScript ) ) . '<fieldset>' . Xml::element( 'legend', array(), - wfMsg( 'mergehistory-box' ) ) . + $this->msg( 'mergehistory-box' )->text() ) . Html::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) . Html::hidden( 'submitted', '1' ) . Html::hidden( 'mergepoint', $this->mTimestamp ) . Xml::openElement( 'table' ) . '<tr> - <td>' . Xml::label( wfMsg( 'mergehistory-from' ), 'target' ) . '</td> + <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td> <td>' . Xml::input( 'target', 30, $this->mTarget, array( 'id' => 'target' ) ) . '</td> </tr><tr> - <td>' . Xml::label( wfMsg( 'mergehistory-into' ), 'dest' ) . '</td> + <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td> <td>' . Xml::input( 'dest', 30, $this->mDest, array( 'id' => 'dest' ) ) . '</td> </tr><tr><td>' . - Xml::submitButton( wfMsg( 'mergehistory-go' ) ) . + Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) . '</td></tr>' . Xml::closeElement( 'table' ) . '</fieldset>' . @@ -187,12 +188,12 @@ class SpecialMergeHistory extends SpecialPage { # in a nice little table $table = Xml::openElement( 'fieldset' ) . - wfMsgExt( 'mergehistory-merge', array( 'parseinline' ), - $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) . + $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(), + $this->mDestObj->getPrefixedText() )->parse() . Xml::openElement( 'table', array( 'id' => 'mw-mergehistory-table' ) ) . '<tr> <td class="mw-label">' . - Xml::label( wfMsg( 'mergehistory-reason' ), 'wpComment' ) . + Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) . '</td> <td class="mw-input">' . Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) . @@ -201,7 +202,7 @@ class SpecialMergeHistory extends SpecialPage { <tr> <td> </td> <td class="mw-submit">' . - Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) . + Xml::submitButton( $this->msg( 'mergehistory-submit' )->text(), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) . '</td> </tr>' . Xml::closeElement( 'table' ) . @@ -212,7 +213,7 @@ class SpecialMergeHistory extends SpecialPage { $out->addHTML( '<h2 id="mw-mergehistory">' . - wfMsgHtml( 'mergehistory-list' ) . "</h2>\n" + $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n" ); if( $haveRevisions ) { @@ -225,8 +226,9 @@ class SpecialMergeHistory extends SpecialPage { $out->addWikiMsg( 'mergehistory-empty' ); } - # Show relevant lines from the deletion log: - $out->addHTML( '<h2>' . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" ); + # Show relevant lines from the merge log: + $mergeLogPage = new LogPage( 'merge' ); + $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" ); LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj ); # When we submit, go by page ID to avoid some nasty but unlikely collisions. @@ -251,9 +253,11 @@ class SpecialMergeHistory extends SpecialPage { $ts = wfTimestamp( TS_MW, $row->rev_timestamp ); $checkBox = Xml::radio( 'mergepoint', $ts, false ); + $user = $this->getUser(); + $pageLink = Linker::linkKnown( $rev->getTitle(), - htmlspecialchars( $this->getLanguage()->timeanddate( $ts ) ), + htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ), array(), array( 'oldid' => $rev->getId() ) ); @@ -262,7 +266,7 @@ class SpecialMergeHistory extends SpecialPage { } # Last link - if( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { + if( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { $last = $this->message['last']; } elseif( isset( $this->prevId[$row->rev_id] ) ) { $last = Linker::linkKnown( @@ -284,7 +288,8 @@ class SpecialMergeHistory extends SpecialPage { } $comment = Linker::revComment( $rev ); - return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>"; + return Html::rawElement( 'li', array(), + $this->msg( 'mergehistory-revisionrow' )->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() ); } function merge() { @@ -296,7 +301,7 @@ class SpecialMergeHistory extends SpecialPage { if( is_null( $targetTitle ) || is_null( $destTitle ) ) { return false; // validate these } - if( $targetTitle->getArticleId() == $destTitle->getArticleId() ) { + if( $targetTitle->getArticleID() == $destTitle->getArticleID() ) { return false; } # Verify that this timestamp is valid @@ -355,18 +360,18 @@ class SpecialMergeHistory extends SpecialPage { ); if( !$haveRevisions ) { if( $this->mComment ) { - $comment = wfMsgForContent( + $comment = $this->msg( 'mergehistory-comment', $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $this->mComment - ); + )->inContentLanguage()->text(); } else { - $comment = wfMsgForContent( + $comment = $this->msg( 'mergehistory-autocomment', $targetTitle->getPrefixedText(), $destTitle->getPrefixedText() - ); + )->inContentLanguage()->text(); } $mwRedir = MagicWord::get( 'redirect' ); $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destTitle->getPrefixedText() . "]]\n"; @@ -404,9 +409,8 @@ class SpecialMergeHistory extends SpecialPage { array( $destTitle->getPrefixedText(), $timestampLimit ) ); - $this->getOutput()->addHTML( - wfMsgExt( 'mergehistory-success', array('parseinline'), - $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) ); + $this->getOutput()->addWikiMsg( 'mergehistory-success', + $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ); wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) ); diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php index 98b73675..3f0bafa3 100644 --- a/includes/specials/SpecialMostcategories.php +++ b/includes/specials/SpecialMostcategories.php @@ -41,27 +41,57 @@ class MostcategoriesPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'categorylinks', 'page' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'COUNT(*) AS value' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'COUNT(*)' ), 'conds' => array ( 'page_namespace' => MWNamespace::getContentNamespaces() ), 'options' => array ( 'HAVING' => 'COUNT(*) > 1', - 'GROUP BY' => 'page_namespace, page_title' ), + 'GROUP BY' => array( 'page_namespace', 'page_title' ) ), 'join_conds' => array ( 'page' => array ( 'LEFT JOIN', 'page_id = cl_from' ) ) ); } /** + * @param $db DatabaseBase + * @param $res + */ + function preprocessResults( $db, $res ) { + # There's no point doing a batch check if we aren't caching results; + # the page must exist for it to have been pulled out of the table + if ( !$this->isCached() || !$res->numRows() ) { + return; + } + + $batch = new LinkBatch(); + foreach ( $res as $row ) { + $batch->add( $row->namespace, $row->title ); + } + $batch->execute(); + + $res->seek( 0 ); + } + + /** * @param $skin Skin * @param $result * @return string */ function formatResult( $skin, $result ) { $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if ( !$title ) { + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); + } + + if ( $this->isCached() ) { + $link = Linker::link( $title ); + } else { + $link = Linker::linkKnown( $title ); + } $count = $this->msg( 'ncategories' )->numParams( $result->value )->escaped(); - $link = Linker::link( $title ); + return $this->getLanguage()->specialList( $link, $count ); } } diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php index 7805e53e..3d797908 100644 --- a/includes/specials/SpecialMostimages.php +++ b/includes/specials/SpecialMostimages.php @@ -41,9 +41,9 @@ class MostimagesPage extends ImageQueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'imagelinks' ), - 'fields' => array ( "'" . NS_FILE . "' AS namespace", - 'il_to AS title', - 'COUNT(*) AS value' ), + 'fields' => array ( 'namespace' => NS_FILE, + 'title' => 'il_to', + 'value' => 'COUNT(*)' ), 'options' => array ( 'GROUP BY' => 'il_to', 'HAVING' => 'COUNT(*) > 1' ) ); diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php new file mode 100644 index 00000000..894d697b --- /dev/null +++ b/includes/specials/SpecialMostinterwikis.php @@ -0,0 +1,112 @@ +<?php +/** + * Implements Special:Mostinterwikis + * + * Copyright © 2012 Umherirrender + * + * 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 SpecialPage + * @author Umherirrender + */ + +/** + * A special page that listed pages that have highest interwiki count + * + * @ingroup SpecialPage + */ +class MostinterwikisPage extends QueryPage { + + function __construct( $name = 'Mostinterwikis' ) { + parent::__construct( $name ); + } + + function isExpensive() { return true; } + function isSyndicated() { return false; } + + function getQueryInfo() { + return array ( + 'tables' => array ( + 'langlinks', + 'page' + ), 'fields' => array ( + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'COUNT(*)' + ), 'conds' => array ( + 'page_namespace' => MWNamespace::getContentNamespaces() + ), 'options' => array ( + 'HAVING' => 'COUNT(*) > 1', + 'GROUP BY' => array ( + 'page_namespace', + 'page_title' + ) + ), 'join_conds' => array ( + 'page' => array ( + 'LEFT JOIN', + 'page_id = ll_from' + ) + ) + ); + } + + /** + * Pre-fill the link cache + * + * @param $db DatabaseBase + * @param $res + */ + function preprocessResults( $db, $res ) { + # There's no point doing a batch check if we aren't caching results; + # the page must exist for it to have been pulled out of the table + if ( !$this->isCached() || !$res->numRows() ) { + return; + } + + $batch = new LinkBatch; + foreach ( $res as $row ) { + $batch->add( $row->namespace, $row->title ); + } + $batch->execute(); + + // Back to start for display + $res->seek( 0 ); + } + + /** + * @param $skin Skin + * @param $result + * @return string + */ + function formatResult( $skin, $result ) { + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if ( !$title ) { + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); + } + + if ( $this->isCached() ) { + $link = Linker::link( $title ); + } else { + $link = Linker::linkKnown( $title ); + } + + $count = $this->msg( 'ninterwikis' )->numParams( $result->value )->escaped(); + + return $this->getLanguage()->specialList( $link, $count ); + } +} diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php index a16f0872..89c43509 100644 --- a/includes/specials/SpecialMostlinked.php +++ b/includes/specials/SpecialMostlinked.php @@ -42,13 +42,13 @@ class MostlinkedPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'pagelinks', 'page' ), - 'fields' => array ( 'pl_namespace AS namespace', - 'pl_title AS title', - 'COUNT(*) AS value', + 'fields' => array ( 'namespace' => 'pl_namespace', + 'title' => 'pl_title', + 'value' => 'COUNT(*)', 'page_namespace' ), 'options' => array ( 'HAVING' => 'COUNT(*) > 1', - 'GROUP BY' => 'pl_namespace, pl_title, '. - 'page_namespace' ), + 'GROUP BY' => array( 'pl_namespace', 'pl_title', + 'page_namespace' ) ), 'join_conds' => array ( 'page' => array ( 'LEFT JOIN', array ( 'page_namespace = pl_namespace', 'page_title = pl_title' ) ) ) @@ -62,12 +62,12 @@ class MostlinkedPage extends QueryPage { * @param $res */ function preprocessResults( $db, $res ) { - if( $db->numRows( $res ) > 0 ) { + if ( $res->numRows() > 0 ) { $linkBatch = new LinkBatch(); foreach ( $res as $row ) { $linkBatch->add( $row->namespace, $row->title ); } - $db->dataSeek( $res, 0 ); + $res->seek( 0 ); $linkBatch->execute(); } } @@ -94,7 +94,8 @@ class MostlinkedPage extends QueryPage { function formatResult( $skin, $result ) { $title = Title::makeTitleSafe( $result->namespace, $result->title ); if ( !$title ) { - return '<!-- ' . htmlspecialchars( "Invalid title: [[$title]]" ) . ' -->'; + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); } $link = Linker::link( $title ); $wlh = $this->makeWlhLink( $title, diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php index 7fb9dea9..dadef8bf 100644 --- a/includes/specials/SpecialMostlinkedcategories.php +++ b/includes/specials/SpecialMostlinkedcategories.php @@ -40,9 +40,9 @@ class MostlinkedCategoriesPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'category' ), - 'fields' => array ( 'cat_title AS title', - NS_CATEGORY . ' AS namespace', - 'cat_pages AS value' ), + 'fields' => array ( 'title' => 'cat_title', + 'namespace' => NS_CATEGORY, + 'value' => 'cat_pages' ), ); } @@ -55,6 +55,10 @@ class MostlinkedCategoriesPage extends QueryPage { * @param $res DatabaseResult */ function preprocessResults( $db, $res ) { + if ( !$res->numRows() ) { + return; + } + $batch = new LinkBatch; foreach ( $res as $row ) { $batch->add( NS_CATEGORY, $row->title ); @@ -62,10 +66,7 @@ class MostlinkedCategoriesPage extends QueryPage { $batch->execute(); // Back to start for display - if ( $db->numRows( $res ) > 0 ) { - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); - } + $res->seek( 0 ); } /** @@ -76,7 +77,12 @@ class MostlinkedCategoriesPage extends QueryPage { function formatResult( $skin, $result ) { global $wgContLang; - $nt = Title::makeTitle( NS_CATEGORY, $result->title ); + $nt = Title::makeTitleSafe( NS_CATEGORY, $result->title ); + if ( !$nt ) { + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), NS_CATEGORY, $result->title ) ); + } + $text = $wgContLang->convert( $nt->getText() ); $plink = Linker::link( $nt, htmlspecialchars( $text ) ); diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php index 6fb09426..22932e5c 100644 --- a/includes/specials/SpecialMostlinkedtemplates.php +++ b/includes/specials/SpecialMostlinkedtemplates.php @@ -64,28 +64,32 @@ class MostlinkedTemplatesPage extends QueryPage { public function getQueryInfo() { return array ( 'tables' => array ( 'templatelinks' ), - 'fields' => array ( 'tl_namespace AS namespace', - 'tl_title AS title', - 'COUNT(*) AS value' ), + 'fields' => array ( 'namespace' => 'tl_namespace', + 'title' => 'tl_title', + 'value' => 'COUNT(*)' ), 'conds' => array ( 'tl_namespace' => NS_TEMPLATE ), - 'options' => array( 'GROUP BY' => 'tl_namespace, tl_title' ) + 'options' => array( 'GROUP BY' => array( 'tl_namespace', 'tl_title' ) ) ); } /** * Pre-cache page existence to speed up link generation * - * @param $db Database connection + * @param $db DatabaseBase connection * @param $res ResultWrapper */ public function preprocessResults( $db, $res ) { + if ( !$res->numRows() ) { + return; + } + $batch = new LinkBatch(); foreach ( $res as $row ) { $batch->add( $row->namespace, $row->title ); } $batch->execute(); - if( $db->numRows( $res ) > 0 ) - $db->dataSeek( $res, 0 ); + + $res->seek( 0 ); } /** @@ -96,7 +100,11 @@ class MostlinkedTemplatesPage extends QueryPage { * @return String */ public function formatResult( $skin, $result ) { - $title = Title::makeTitle( $result->namespace, $result->title ); + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if ( !$title ) { + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); + } return $this->getLanguage()->specialList( Linker::link( $title ), diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 5536fbc9..af3dbf3e 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -141,13 +141,13 @@ class MovePageForm extends UnlistedSpecialPage { && $newTitle->quickUserCan( 'delete', $user ) ) { $out->addWikiMsg( 'delete_and_move_text', $newTitle->getPrefixedText() ); - $movepagebtn = wfMsg( 'delete_and_move' ); + $movepagebtn = $this->msg( 'delete_and_move' )->text(); $submitVar = 'wpDeleteAndMove'; $confirm = " <tr> <td></td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) . + Xml::checkLabel( $this->msg( 'delete_and_move_confirm' )->text(), 'wpConfirm', 'wpConfirm' ) . "</td> </tr>"; $err = array(); @@ -157,7 +157,7 @@ class MovePageForm extends UnlistedSpecialPage { } $out->addWikiMsg( $wgFixDoubleRedirects ? 'movepagetext' : 'movepagetext-noredirectfixer' ); - $movepagebtn = wfMsg( 'movepagebtn' ); + $movepagebtn = $this->msg( 'movepagebtn' )->text(); $submitVar = 'wpMove'; $confirm = false; } @@ -246,14 +246,22 @@ class MovePageForm extends UnlistedSpecialPage { // Byte limit (not string length limit) for wpReason and wpNewTitleMain // is enforced in the mediawiki.special.movePage module + $immovableNamespaces = array(); + + foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) { + if ( !MWNamespace::isMovable( $nsId ) ) { + $immovableNamespaces[] = $nsId; + } + } + $out->addHTML( Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) . Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'move-page-legend' ) ) . - Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-movepage-table' ) ) . + Xml::element( 'legend', null, $this->msg( 'move-page-legend' )->text() ) . + Xml::openElement( 'table', array( 'id' => 'mw-movepage-table' ) ) . "<tr> <td class='mw-label'>" . - wfMsgHtml( 'movearticle' ) . + $this->msg( 'movearticle' )->escaped() . "</td> <td class='mw-input'> <strong>{$oldTitleLink}</strong> @@ -261,11 +269,14 @@ class MovePageForm extends UnlistedSpecialPage { </tr> <tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'newtitle' ), 'wpNewTitleMain' ) . + Xml::label( $this->msg( 'newtitle' )->text(), 'wpNewTitleMain' ) . "</td> <td class='mw-input'>" . Html::namespaceSelector( - array( 'selected' => $newTitle->getNamespace() ), + array( + 'selected' => $newTitle->getNamespace(), + 'exclude' => $immovableNamespaces + ), array( 'name' => 'wpNewTitleNs', 'id' => 'wpNewTitleNs' ) ) . Xml::input( 'wpNewTitleMain', 60, $wgContLang->recodeForEdit( $newTitle->getText() ), array( @@ -278,7 +289,7 @@ class MovePageForm extends UnlistedSpecialPage { </tr> <tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'movereason' ), 'wpReason' ) . + Xml::label( $this->msg( 'movereason' )->text(), 'wpReason' ) . "</td> <td class='mw-input'>" . Html::element( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2, @@ -292,7 +303,7 @@ class MovePageForm extends UnlistedSpecialPage { <tr> <td></td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) . + Xml::checkLabel( $this->msg( 'movetalk' )->text(), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) . "</td> </tr>" ); @@ -303,7 +314,7 @@ class MovePageForm extends UnlistedSpecialPage { <tr> <td></td> <td class='mw-input' >" . - Xml::checkLabel( wfMsg( 'move-leave-redirect' ), 'wpLeaveRedirect', + Xml::checkLabel( $this->msg( 'move-leave-redirect' )->text(), 'wpLeaveRedirect', 'wpLeaveRedirect', $this->leaveRedirect ) . "</td> </tr>" @@ -315,7 +326,7 @@ class MovePageForm extends UnlistedSpecialPage { <tr> <td></td> <td class='mw-input' >" . - Xml::checkLabel( wfMsg( 'fix-double-redirects' ), 'wpFixRedirects', + Xml::checkLabel( $this->msg( 'fix-double-redirects' )->text(), 'wpFixRedirects', 'wpFixRedirects', $this->fixRedirects ) . "</td> </tr>" @@ -335,15 +346,11 @@ class MovePageForm extends UnlistedSpecialPage { array( 'id' => 'wpMovesubpages' ) ) . ' ' . Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ), - wfMsgExt( + $this->msg( ( $this->oldTitle->hasSubpages() ? 'move-subpages' - : 'move-talk-subpages' ), - array( 'parseinline' ), - $this->getLanguage()->formatNum( $wgMaximumMovedPages ), - # $2 to allow use of PLURAL in message. - $wgMaximumMovedPages - ) + : 'move-talk-subpages' ) + )->numParams( $wgMaximumMovedPages )->params( $wgMaximumMovedPages )->parse() ) . "</td> </tr>" @@ -351,14 +358,14 @@ class MovePageForm extends UnlistedSpecialPage { } $watchChecked = $user->isLoggedIn() && ($this->watch || $user->getBoolOption( 'watchmoves' ) - || $this->oldTitle->userIsWatching()); + || $user->isWatched( $this->oldTitle ) ); # Don't allow watching if user is not logged in if( $user->isLoggedIn() ) { $out->addHTML( " <tr> <td></td> <td class='mw-input'>" . - Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) . + Xml::checkLabel( $this->msg( 'move-watch' )->text(), 'wpWatch', 'watch', $watchChecked ) . "</td> </tr>"); } @@ -384,7 +391,7 @@ class MovePageForm extends UnlistedSpecialPage { } function doSubmit() { - global $wgMaximumMovedPages, $wgFixDoubleRedirects, $wgDeleteRevisionsLimit; + global $wgMaximumMovedPages, $wgFixDoubleRedirects; $user = $this->getUser(); @@ -421,7 +428,7 @@ class MovePageForm extends UnlistedSpecialPage { return; } - $reason = wfMessage( 'delete_and_move_reason', $ot )->inContentLanguage()->text(); + $reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text(); // Delete an associated image if there is if ( $nt->getNamespace() == NS_FILE ) { @@ -433,8 +440,9 @@ class MovePageForm extends UnlistedSpecialPage { $error = ''; // passed by ref $page = WikiPage::factory( $nt ); - if ( !$page->doDeleteArticle( $reason, false, 0, true, $error, $user ) ) { - $this->showForm( array( array( 'cannotdelete', wfEscapeWikiText( $nt->getPrefixedText() ) ) ) ); + $deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user ); + if ( !$deleteStatus->isGood() ) { + $this->showForm( $deleteStatus->getErrorsArray() ); return; } } @@ -459,7 +467,7 @@ class MovePageForm extends UnlistedSpecialPage { wfRunHooks( 'SpecialMovepageAfterMove', array( &$this, &$ot, &$nt ) ); $out = $this->getOutput(); - $out->setPagetitle( wfMsg( 'pagemovedsub' ) ); + $out->setPageTitle( $this->msg( 'pagemovedsub' ) ); $oldLink = Linker::link( $ot, @@ -472,7 +480,7 @@ class MovePageForm extends UnlistedSpecialPage { $newText = $nt->getPrefixedText(); $msgName = $createRedirect ? 'movepage-moved-redirect' : 'movepage-moved-noredirect'; - $out->addHTML( wfMessage( 'movepage-moved' )->rawParams( $oldLink, + $out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink, $newLink )->params( $oldText, $newText )->parseAsBlock() ); $out->addWikiMsg( $msgName ); @@ -562,15 +570,15 @@ class MovePageForm extends UnlistedSpecialPage { $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); if( !$newSubpage ) { $oldLink = Linker::linkKnown( $oldSubpage ); - $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, - htmlspecialchars(Title::makeName( $newNs, $newPageName ))); + $extraOutput []= $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink + )->params( Title::makeName( $newNs, $newPageName ) )->escaped(); continue; } # This was copy-pasted from Renameuser, bleh. if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) { $link = Linker::linkKnown( $newSubpage ); - $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link ); + $extraOutput []= $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped(); } else { $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect ); if( $success === true ) { @@ -584,16 +592,16 @@ class MovePageForm extends UnlistedSpecialPage { array( 'redirect' => 'no' ) ); $newLink = Linker::linkKnown( $newSubpage ); - $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink ); + $extraOutput []= $this->msg( 'movepage-page-moved' )->rawParams( $oldLink, $newLink )->escaped(); ++$count; if( $count >= $wgMaximumMovedPages ) { - $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $this->getLanguage()->formatNum( $wgMaximumMovedPages ) ); + $extraOutput []= $this->msg( 'movepage-max-pages' )->numParams( $wgMaximumMovedPages )->escaped(); break; } } else { $oldLink = Linker::linkKnown( $oldSubpage ); $newLink = Linker::link( $newSubpage ); - $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, $newLink ); + $extraOutput []= $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink, $newLink )->escaped(); } } @@ -621,8 +629,9 @@ class MovePageForm extends UnlistedSpecialPage { } function showLogFragment( $title ) { + $moveLogPage = new LogPage( 'move' ); $out = $this->getOutput(); - $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'move' ) ) ); + $out->addHTML( Xml::element( 'h2', null, $moveLogPage->getName()->text() ) ); LogEventsList::showLogExtract( $out, 'move', $title ); } diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index b88123dc..350aac63 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -58,6 +58,9 @@ class NewFilesPager extends ReverseChronologicalPager { function __construct( IContextSource $context, $par = null ) { $this->like = $context->getRequest()->getText( 'like' ); $this->showbots = $context->getRequest()->getBool( 'showbots' , 0 ); + if ( is_numeric( $par ) ) { + $this->setLimit( $par ); + } parent::__construct( $context ); } @@ -68,15 +71,18 @@ class NewFilesPager extends ReverseChronologicalPager { $tables = array( 'image' ); if( !$this->showbots ) { - $tables[] = 'user_groups'; - $conds[] = 'ug_group IS NULL'; - $jconds['user_groups'] = array( - 'LEFT JOIN', - array( - 'ug_group' => User::getGroupsWithPermission( 'bot' ), - 'ug_user = img_user' - ) - ); + $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); + if( count( $groupsWithBotPermission ) ) { + $tables[] = 'user_groups'; + $conds[] = 'ug_group IS NULL'; + $jconds['user_groups'] = array( + 'LEFT JOIN', + array( + 'ug_group' => $groupsWithBotPermission, + 'ug_user = img_user' + ) + ); + } } if( !$wgMiserMode && $this->like !== null ){ @@ -123,7 +129,7 @@ class NewFilesPager extends ReverseChronologicalPager { $this->gallery->add( $title, "$ul<br />\n<i>" - . htmlspecialchars( $this->getLanguage()->timeanddate( $row->img_timestamp, true ) ) + . htmlspecialchars( $this->getLanguage()->userTimeAndDate( $row->img_timestamp, $this->getUser() ) ) . "</i><br />\n" ); } @@ -139,13 +145,13 @@ class NewFilesPager extends ReverseChronologicalPager { ), 'showbots' => array( 'type' => 'check', - 'label' => wfMessage( 'showhidebots', wfMsg( 'show' ) ), + 'label' => $this->msg( 'showhidebots', $this->msg( 'show' )->plain() )->escaped(), 'name' => 'showbots', # 'default' => $this->getRequest()->getBool( 'showbots', 0 ), ), 'limit' => array( 'type' => 'hidden', - 'default' => $this->getRequest()->getText( 'limit' ), + 'default' => $this->mLimit, 'name' => 'limit', ), 'offset' => array( @@ -161,9 +167,9 @@ class NewFilesPager extends ReverseChronologicalPager { $form = new HTMLForm( $fields, $this->getContext() ); $form->setTitle( $this->getTitle() ); - $form->setSubmitText( wfMsg( 'ilsubmit' ) ); + $form->setSubmitTextMsg( 'ilsubmit' ); $form->setMethod( 'get' ); - $form->setWrapperLegend( wfMsg( 'newimages-legend' ) ); + $form->setWrapperLegendMsg( 'newimages-legend' ); return $form; } diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 54bcb97f..8e15d554 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -143,7 +143,9 @@ class SpecialNewpages extends IncludableSpecialPage { return $this->feed( $feedType ); } - $out->setFeedAppendQuery( wfArrayToCGI( $this->opts->getAllValues() ) ); + $allValues = $this->opts->getAllValues(); + unset( $allValues['feed'] ); + $out->setFeedAppendQuery( wfArrayToCGI( $allValues ) ); } $pager = new NewPagesPager( $this, $this->opts ); @@ -165,7 +167,7 @@ class SpecialNewpages extends IncludableSpecialPage { global $wgGroupPermissions; // show/hide links - $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) ); + $showhide = array( $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ); // Option value -> message mapping $filters = array( @@ -197,7 +199,7 @@ class SpecialNewpages extends IncludableSpecialPage { $link = Linker::link( $self, $showhide[$onoff], array(), array( $key => $onoff ) + $changed ); - $links[$key] = wfMsgHtml( $msg, $link ); + $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped(); } return $this->getLanguage()->pipeList( $links ); @@ -230,14 +232,23 @@ class SpecialNewpages extends IncludableSpecialPage { $form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) . Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . - Xml::fieldset( wfMsg( 'newpages' ) ) . + Xml::fieldset( $this->msg( 'newpages' )->text() ) . Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) . '<tr> <td class="mw-label">' . - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . '</td> <td class="mw-input">' . - Xml::namespaceSelector( $namespace, 'all' ) . + Html::namespaceSelector( + array( + 'selected' => $namespace, + 'all' => 'all', + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ) . '</td> </tr>' . ( $tagFilter ? ( '<tr> @@ -251,7 +262,7 @@ class SpecialNewpages extends IncludableSpecialPage { ( $wgEnableNewpagesUserFilter ? '<tr> <td class="mw-label">' . - Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) . + Xml::label( $this->msg( 'newpages-username' )->text(), 'mw-np-username' ) . '</td> <td class="mw-input">' . Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) . @@ -259,7 +270,7 @@ class SpecialNewpages extends IncludableSpecialPage { </tr>' : '' ) . '<tr> <td></td> <td class="mw-submit">' . - Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . '</td> </tr>' . '<tr> @@ -283,6 +294,8 @@ class SpecialNewpages extends IncludableSpecialPage { * @return String */ public function formatRow( $result ) { + $title = Title::newFromRow( $result ); + # Revision deletion works on revisions, so we should cast one $row = array( 'comment' => $result->rc_comment, @@ -291,15 +304,15 @@ class SpecialNewpages extends IncludableSpecialPage { 'user' => $result->rc_user, ); $rev = new Revision( $row ); + $rev->setTitle( $title ); $classes = array(); $lang = $this->getLanguage(); $dm = $lang->getDirMark(); - $title = Title::newFromRow( $result ); $spanTime = Html::element( 'span', array( 'class' => 'mw-newpages-time' ), - $lang->timeanddate( $result->rc_timestamp, true ) + $lang->userTimeAndDate( $result->rc_timestamp, $this->getUser() ) ); $time = Linker::linkKnown( $title, @@ -324,14 +337,15 @@ class SpecialNewpages extends IncludableSpecialPage { ); $histLink = Linker::linkKnown( $title, - wfMsgHtml( 'hist' ), + $this->msg( 'hist' )->escaped(), array(), array( 'action' => 'history' ) ); - $hist = Html::rawElement( 'span', array( 'class' => 'mw-newpages-history' ), wfMsg( 'parentheses', $histLink ) ); + $hist = Html::rawElement( 'span', array( 'class' => 'mw-newpages-history' ), + $this->msg( 'parentheses' )->rawParams( $histLink )->escaped() ); $length = Html::element( 'span', array( 'class' => 'mw-newpages-length' ), - '[' . $this->msg( 'nbytes' )->numParams( $result->length )->text() . ']' + $this->msg( 'brackets' )->params( $this->msg( 'nbytes' )->numParams( $result->length )->text() ) ); $ulink = Linker::revUserTools( $rev ); @@ -346,8 +360,8 @@ class SpecialNewpages extends IncludableSpecialPage { $classes[] = 'mw-newpages-zero-byte-page'; } - # Tags, if any. check for including due to bug 23293 - if ( !$this->including() ) { + # Tags, if any. + if( isset( $result->ts_tags ) ) { list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $result->ts_tags, 'newpages' ); $classes = array_merge( $classes, $newClasses ); } else { @@ -356,11 +370,11 @@ class SpecialNewpages extends IncludableSpecialPage { $css = count( $classes ) ? ' class="' . implode( ' ', $classes ) . '"' : ''; - # Display the old title if the namespace has been changed + # Display the old title if the namespace/title has been changed $oldTitleText = ''; - if ( $result->page_namespace !== $result->rc_namespace ) { - $oldTitleText = wfMessage( 'rc-old-title' )->params( Title::makeTitle( $result->rc_namespace, $result->rc_title ) - ->getPrefixedText() )->escaped(); + $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title ); + if ( !$title->equals( $oldTitle ) ) { + $oldTitleText = $this->msg( 'rc-old-title' )->params( $oldTitle->getPrefixedText() )->escaped(); } return "<li{$css}>{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} {$tagDisplay} {$oldTitleText}</li>\n"; @@ -396,7 +410,7 @@ class SpecialNewpages extends IncludableSpecialPage { $feed = new $wgFeedClasses[$type]( $this->feedTitle(), - wfMsgExt( 'tagline', 'parsemag' ), + $this->msg( 'tagline' )->text(), $this->getTitle()->getFullUrl() ); @@ -420,7 +434,7 @@ class SpecialNewpages extends IncludableSpecialPage { } protected function feedItem( $row ) { - $title = Title::MakeTitle( intval( $row->rc_namespace ), $row->rc_title ); + $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title ); if( $title ) { $date = $row->rc_timestamp; $comments = $title->getTalkPage()->getFullURL(); @@ -445,7 +459,8 @@ class SpecialNewpages extends IncludableSpecialPage { protected function feedItemDesc( $row ) { $revision = Revision::newFromId( $row->rev_id ); if( $revision ) { - return '<p>' . htmlspecialchars( $revision->getUserText() ) . wfMsgForContent( 'colon-separator' ) . + return '<p>' . htmlspecialchars( $revision->getUserText() ) . + $this->msg( 'colon-separator' )->inContentLanguage()->escaped() . htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . "</p>\n<hr />\n<div>" . nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>"; @@ -515,7 +530,7 @@ class NewPagesPager extends ReverseChronologicalPager { $fields = array( 'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text', 'rc_comment', 'rc_timestamp', 'rc_patrolled','rc_id', 'rc_deleted', - 'page_len AS length', 'page_latest AS rev_id', 'ts_tags', 'rc_this_oldid', + 'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid', 'page_namespace', 'page_title' ); $join_conds = array( 'page' => array( 'INNER JOIN', 'page_id=rc_cur_id' ) ); @@ -531,13 +546,10 @@ class NewPagesPager extends ReverseChronologicalPager { 'join_conds' => $join_conds ); - // Empty array for fields, it'll be set by us anyway. - $fields = array(); - // Modify query for tags ChangeTags::modifyDisplayQuery( $info['tables'], - $fields, + $info['fields'], $info['conds'], $info['join_conds'], $info['options'], diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php index 62731e98..efb57657 100644 --- a/includes/specials/SpecialPasswordReset.php +++ b/includes/specials/SpecialPasswordReset.php @@ -65,6 +65,9 @@ class SpecialPasswordReset extends FormSpecialPage { 'type' => 'text', 'label-message' => 'passwordreset-username', ); + if( $this->getUser()->isLoggedIn() ) { + $a['Username']['default'] = $this->getUser()->getName(); + } } if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) { @@ -95,7 +98,7 @@ class SpecialPasswordReset extends FormSpecialPage { } public function alterForm( HTMLForm $form ) { - $form->setSubmitText( wfMessage( "mailmypassword" ) ); + $form->setSubmitTextMsg( 'mailmypassword' ); } protected function preText() { @@ -110,7 +113,7 @@ class SpecialPasswordReset extends FormSpecialPage { if ( isset( $wgPasswordResetRoutes['domain'] ) && $wgPasswordResetRoutes['domain'] ) { $i++; } - return wfMessage( 'passwordreset-pretext', $i )->parseAsBlock(); + return $this->msg( 'passwordreset-pretext', $i )->parseAsBlock(); } /** @@ -151,7 +154,7 @@ class SpecialPasswordReset extends FormSpecialPage { $method = 'email'; $res = wfGetDB( DB_SLAVE )->select( 'user', - '*', + User::selectFields(), array( 'user_email' => $data['Email'] ), __METHOD__ ); @@ -234,12 +237,12 @@ class SpecialPasswordReset extends FormSpecialPage { $password = $user->randomPassword(); $user->setNewpassword( $password ); $user->saveSettings(); - $passwords[] = wfMessage( 'passwordreset-emailelement', $user->getName(), $password - )->inLanguage( $userLanguage )->plain(); // We'll escape the whole thing later + $passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password + )->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later } $passwordBlock = implode( "\n\n", $passwords ); - $this->email = wfMessage( $msg )->inLanguage( $userLanguage ); + $this->email = $this->msg( $msg )->inLanguage( $userLanguage ); $this->email->params( $username, $passwordBlock, @@ -248,7 +251,7 @@ class SpecialPasswordReset extends FormSpecialPage { round( $wgNewPasswordExpiry / 86400 ) ); - $title = wfMessage( 'passwordreset-emailtitle' ); + $title = $this->msg( 'passwordreset-emailtitle' ); $this->result = $firstUser->sendMail( $title->escaped(), $this->email->escaped() ); diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php index 803f03e7..448d1799 100644 --- a/includes/specials/SpecialPopularpages.php +++ b/includes/specials/SpecialPopularpages.php @@ -42,9 +42,9 @@ class PopularPagesPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array( 'page' ), - 'fields' => array( 'page_namespace AS namespace', - 'page_title AS title', - 'page_counter AS value'), + 'fields' => array( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_counter'), 'conds' => array( 'page_is_redirect' => 0, 'page_namespace' => MWNamespace::getContentNamespaces() ) ); } @@ -56,7 +56,13 @@ class PopularPagesPage extends QueryPage { */ function formatResult( $skin, $result ) { global $wgContLang; - $title = Title::makeTitle( $result->namespace, $result->title ); + + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if( !$title ) { + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); + } + $link = Linker::linkKnown( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index 946112bf..c6b2bb6b 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -39,8 +39,7 @@ class SpecialPreferences extends SpecialPage { $user = $this->getUser(); if ( $user->isAnon() ) { - $out->showErrorPage( 'prefsnologin', 'prefsnologintext', array( $this->getTitle()->getPrefixedDBkey() ) ); - return; + throw new ErrorPageError( 'prefsnologin', 'prefsnologintext', array( $this->getTitle()->getPrefixedDBkey() ) ); } $this->checkReadOnly(); @@ -69,7 +68,7 @@ class SpecialPreferences extends SpecialPage { $htmlForm = new HTMLForm( array(), $this->getContext(), 'prefs-restore' ); - $htmlForm->setSubmitText( wfMsg( 'restoreprefs' ) ); + $htmlForm->setSubmitTextMsg( 'restoreprefs' ); $htmlForm->setTitle( $this->getTitle( 'reset' ) ); $htmlForm->setSubmitCallback( array( $this, 'submitReset' ) ); $htmlForm->suppressReset(); @@ -82,7 +81,7 @@ class SpecialPreferences extends SpecialPage { $user->resetOptions(); $user->saveSettings(); - $url = SpecialPage::getTitleFor( 'Preferences' )->getFullURL( 'success' ); + $url = $this->getTitle()->getFullURL( 'success' ); $this->getOutput()->redirect( $url ); diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php index 495f15f7..7740b320 100644 --- a/includes/specials/SpecialPrefixindex.php +++ b/includes/specials/SpecialPrefixindex.php @@ -52,6 +52,7 @@ class SpecialPrefixindex extends SpecialAllpages { $prefix = $request->getVal( 'prefix', '' ); $ns = $request->getIntOrNull( 'namespace' ); $namespace = (int)$ns; // if no namespace given, use 0 (NS_MAIN). + $hideredirects = $request->getBool( 'hideredirects', false ); $namespaces = $wgContLang->getNamespaces(); $out->setPageTitle( @@ -73,29 +74,31 @@ class SpecialPrefixindex extends SpecialAllpages { // Bug 27864: if transcluded, show all pages instead of the form. if ( $this->including() || $showme != '' || $ns !== null ) { - $this->showPrefixChunk( $namespace, $showme, $from ); + $this->showPrefixChunk( $namespace, $showme, $from, $hideredirects ); } else { - $out->addHTML( $this->namespacePrefixForm( $namespace, null ) ); + $out->addHTML( $this->namespacePrefixForm( $namespace, null, $hideredirects ) ); } } /** - * HTML for the top form - * @param $namespace Integer: a namespace constant (default NS_MAIN). - * @param $from String: dbKey we are starting listing at. - */ - function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ) { + * HTML for the top form + * @param $namespace Integer: a namespace constant (default NS_MAIN). + * @param $from String: dbKey we are starting listing at. + * @param $hideredirects Bool: hide redirects (default FALSE) + * @return string + */ + function namespacePrefixForm( $namespace = NS_MAIN, $from = '', $hideredirects = false ) { global $wgScript; $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); $out .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ); $out .= Xml::openElement( 'fieldset' ); - $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) ); + $out .= Xml::element( 'legend', null, $this->msg( 'allpages' )->text() ); $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); $out .= "<tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'allpagesprefix' ), 'nsfrom' ) . + Xml::label( $this->msg( 'allpagesprefix' )->text(), 'nsfrom' ) . "</td> <td class='mw-input'>" . Xml::input( 'prefix', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . @@ -103,13 +106,25 @@ class SpecialPrefixindex extends SpecialAllpages { </tr> <tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . "</td> <td class='mw-input'>" . - Xml::namespaceSelector( $namespace, null ) . ' ' . - Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + Html::namespaceSelector( array( + 'selected' => $namespace, + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) ) . + Xml::checkLabel( + $this->msg( 'allpages-hide-redirects' )->text(), + 'hideredirects', + 'hideredirects', + $hideredirects + ) . ' ' . + Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "</td> - </tr>"; + </tr>"; $out .= Xml::closeElement( 'table' ); $out .= Xml::closeElement( 'fieldset' ); $out .= Xml::closeElement( 'form' ); @@ -121,8 +136,9 @@ class SpecialPrefixindex extends SpecialAllpages { * @param $namespace Integer, default NS_MAIN * @param $prefix String * @param $from String: list all pages from this name (default FALSE) + * @param $hideredirects Bool: hide redirects (default FALSE) */ - function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null ) { + function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null, $hideredirects = false ) { global $wgContLang; if ( $from === null ) { @@ -134,10 +150,10 @@ class SpecialPrefixindex extends SpecialAllpages { $namespaces = $wgContLang->getNamespaces(); if ( !$prefixList || !$fromList ) { - $out = wfMsgExt( 'allpagesbadtitle', 'parse' ); + $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock(); } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { // Show errormessage and reset to NS_MAIN - $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace ); + $out = $this->msg( 'allpages-bad-ns', $namespace )->parse(); $namespace = NS_MAIN; } else { list( $namespace, $prefixKey, $prefix ) = $prefixList; @@ -147,13 +163,19 @@ class SpecialPrefixindex extends SpecialAllpages { $dbr = wfGetDB( DB_SLAVE ); + $conds = array( + 'page_namespace' => $namespace, + 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ), + 'page_title >= ' . $dbr->addQuotes( $fromKey ), + ); + + if ( $hideredirects ) { + $conds['page_is_redirect'] = 0; + } + $res = $dbr->select( 'page', array( 'page_namespace', 'page_title', 'page_is_redirect' ), - array( - 'page_namespace' => $namespace, - 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ), - 'page_title >= ' . $dbr->addQuotes( $fromKey ), - ), + $conds, __METHOD__, array( 'ORDER BY' => 'page_title', @@ -166,7 +188,7 @@ class SpecialPrefixindex extends SpecialAllpages { $n = 0; if( $res->numRows() > 0 ) { - $out = Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-prefixindex-list-table' ) ); + $out = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-list-table' ) ); while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { $t = Title::makeTitle( $s->page_namespace, $s->page_title ); @@ -174,7 +196,8 @@ class SpecialPrefixindex extends SpecialAllpages { $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) . Linker::linkKnown( $t, - htmlspecialchars( $t->getText() ) + htmlspecialchars( $t->getText() ), + $s->page_is_redirect ? array( 'class' => 'mw-redirect' ) : array() ) . ($s->page_is_redirect ? '</div>' : '' ); } else { @@ -202,9 +225,9 @@ class SpecialPrefixindex extends SpecialAllpages { if ( $this->including() ) { $out2 = ''; } else { - $nsForm = $this->namespacePrefixForm( $namespace, $prefix ); + $nsForm = $this->namespacePrefixForm( $namespace, $prefix, $hideredirects ); $self = $this->getTitle(); - $out2 = Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-prefixindex-nav-table' ) ) . + $out2 = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-nav-table' ) ) . '<tr> <td>' . $nsForm . @@ -214,7 +237,8 @@ class SpecialPrefixindex extends SpecialAllpages { if( isset( $res ) && $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { $query = array( 'from' => $s->page_title, - 'prefix' => $prefix + 'prefix' => $prefix, + 'hideredirects' => $hideredirects, ); if( $namespace || ($prefix == '')) { @@ -224,7 +248,7 @@ class SpecialPrefixindex extends SpecialAllpages { } $nextLink = Linker::linkKnown( $self, - wfMsgHtml( 'nextpage', str_replace( '_',' ', htmlspecialchars( $s->page_title ) ) ), + $this->msg( 'nextpage', str_replace( '_',' ', $s->page_title ) )->escaped(), array(), $query ); diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php index eec974fe..74ed5378 100644 --- a/includes/specials/SpecialProtectedpages.php +++ b/includes/specials/SpecialProtectedpages.php @@ -58,21 +58,20 @@ class SpecialProtectedpages extends SpecialPage { $this->getOutput()->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly, $cascadeOnly ) ); if( $pager->getNumRows() ) { - $s = $pager->getNavigationBar(); - $s .= "<ul>" . - $pager->getBody() . - "</ul>"; - $s .= $pager->getNavigationBar(); + $this->getOutput()->addHTML( + $pager->getNavigationBar() . + '<ul>' . $pager->getBody() . '</ul>' . + $pager->getNavigationBar() + ); } else { - $s = '<p>' . wfMsgHtml( 'protectedpagesempty' ) . '</p>'; + $this->getOutput()->addWikiMsg( 'protectedpagesempty' ); } - $this->getOutput()->addHTML( $s ); } /** * Callback function to output a restriction - * @param $row object Protected title - * @return string Formatted <li> element + * @param Title $row Protected title + * @return string Formatted "<li>" element */ public function formatRow( $row ) { wfProfileIn( __METHOD__ ); @@ -88,12 +87,12 @@ class SpecialProtectedpages extends SpecialPage { $description_items = array (); - $protType = wfMsgHtml( 'restriction-level-' . $row->pr_level ); + $protType = $this->msg( 'restriction-level-' . $row->pr_level )->escaped(); $description_items[] = $protType; if( $row->pr_cascade ) { - $description_items[] = wfMsg( 'protect-summary-cascade' ); + $description_items[] = $this->msg( 'protect-summary-cascade' )->text(); } $stxt = ''; @@ -101,15 +100,13 @@ class SpecialProtectedpages extends SpecialPage { $expiry = $lang->formatExpiry( $row->pr_expiry, TS_MW ); if( $expiry != $infinity ) { - - $expiry_description = wfMsg( + $user = $this->getUser(); + $description_items[] = $this->msg( 'protect-expiring-local', - $lang->timeanddate( $expiry, true ), - $lang->date( $expiry, true ), - $lang->time( $expiry, true ) - ); - - $description_items[] = htmlspecialchars($expiry_description); + $lang->userTimeAndDate( $expiry, $user ), + $lang->userDate( $expiry, $user ), + $lang->userTime( $expiry, $user ) + )->escaped(); } if(!is_null($size = $row->page_len)) { @@ -118,25 +115,27 @@ class SpecialProtectedpages extends SpecialPage { # Show a link to the change protection form for allowed users otherwise a link to the protection log if( $this->getUser()->isAllowed( 'protect' ) ) { - $changeProtection = ' (' . Linker::linkKnown( + $changeProtection = Linker::linkKnown( $title, - wfMsgHtml( 'protect_change' ), + $this->msg( 'protect_change' )->escaped(), array(), array( 'action' => 'unprotect' ) - ) . ')'; + ); } else { $ltitle = SpecialPage::getTitleFor( 'Log' ); - $changeProtection = ' (' . Linker::linkKnown( + $changeProtection = Linker::linkKnown( $ltitle, - wfMsgHtml( 'protectlogpage' ), + $this->msg( 'protectlogpage' )->escaped(), array(), array( 'type' => 'protect', 'page' => $title->getPrefixedText() ) - ) . ')'; + ); } + $changeProtection = ' ' . $this->msg( 'parentheses' )->rawParams( $changeProtection )->escaped(); + wfProfileOut( __METHOD__ ); return Html::rawElement( @@ -160,7 +159,7 @@ class SpecialProtectedpages extends SpecialPage { $title = $this->getTitle(); return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) . + Xml::element( 'legend', array(), $this->msg( 'protectedpages' )->text() ) . Html::hidden( 'title', $title->getPrefixedDBkey() ) . "\n" . $this->getNamespaceMenu( $namespace ) . " \n" . $this->getTypeMenu( $type ) . " \n" . @@ -171,7 +170,7 @@ class SpecialProtectedpages extends SpecialPage { "</span><br /><span style='white-space: nowrap'>" . $this->getSizeLimit( $sizetype, $size ) . " \n" . "</span>" . - " " . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . + " " . Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n" . Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); } @@ -184,9 +183,19 @@ class SpecialProtectedpages extends SpecialPage { * @return String */ protected function getNamespaceMenu( $namespace = null ) { - return "<span style='white-space: nowrap'>" . - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' - . Xml::namespaceSelector( $namespace, '' ) . "</span>"; + return Html::rawElement( 'span', array( 'style' => 'white-space: nowrap;' ), + Html::namespaceSelector( + array( + 'selected' => $namespace, + 'all' => '', + 'label' => $this->msg( 'namespace' )->text() + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ) + ); } /** @@ -194,7 +203,7 @@ class SpecialProtectedpages extends SpecialPage { */ protected function getExpiryCheck( $indefOnly ) { return - Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n"; + Xml::checkLabel( $this->msg( 'protectedpages-indef' )->text(), 'indefonly', 'indefonly', $indefOnly ) . "\n"; } /** @@ -202,7 +211,7 @@ class SpecialProtectedpages extends SpecialPage { */ protected function getCascadeCheck( $cascadeOnly ) { return - Xml::checkLabel( wfMsg('protectedpages-cascade'), 'cascadeonly', 'cascadeonly', $cascadeOnly ) . "\n"; + Xml::checkLabel( $this->msg( 'protectedpages-cascade' )->text(), 'cascadeonly', 'cascadeonly', $cascadeOnly ) . "\n"; } /** @@ -212,13 +221,13 @@ class SpecialProtectedpages extends SpecialPage { $max = $sizetype === 'max'; return - Xml::radioLabel( wfMsg('minimum-size'), 'sizetype', 'min', 'wpmin', !$max ) . + Xml::radioLabel( $this->msg( 'minimum-size' )->text(), 'sizetype', 'min', 'wpmin', !$max ) . ' ' . - Xml::radioLabel( wfMsg('maximum-size'), 'sizetype', 'max', 'wpmax', $max ) . + Xml::radioLabel( $this->msg( 'maximum-size' )->text(), 'sizetype', 'max', 'wpmax', $max ) . ' ' . Xml::input( 'size', 9, $size, array( 'id' => 'wpsize' ) ) . ' ' . - Xml::label( wfMsg('pagesize'), 'wpsize' ); + Xml::label( $this->msg( 'pagesize' )->text(), 'wpsize' ); } /** @@ -232,7 +241,7 @@ class SpecialProtectedpages extends SpecialPage { // First pass to load the log names foreach( Title::getFilteredRestrictionTypes( true ) as $type ) { - $text = wfMsg("restriction-$type"); + $text = $this->msg( "restriction-$type" )->text(); $m[$text] = $type; } @@ -243,7 +252,7 @@ class SpecialProtectedpages extends SpecialPage { } return "<span style='white-space: nowrap'>" . - Xml::label( wfMsg('restriction-type') , $this->IdType ) . ' ' . + Xml::label( $this->msg( 'restriction-type' )->text(), $this->IdType ) . ' ' . Xml::tags( 'select', array( 'id' => $this->IdType, 'name' => $this->IdType ), implode( "\n", $options ) ) . "</span>"; @@ -257,14 +266,14 @@ class SpecialProtectedpages extends SpecialPage { protected function getLevelMenu( $pr_level ) { global $wgRestrictionLevels; - $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array + $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); // Temporary array $options = array(); // First pass to load the log names foreach( $wgRestrictionLevels as $type ) { // Messages used can be 'restriction-level-sysop' and 'restriction-level-autoconfirmed' if( $type !='' && $type !='*') { - $text = wfMsg("restriction-level-$type"); + $text = $this->msg( "restriction-level-$type" )->text(); $m[$text] = $type; } } @@ -276,7 +285,7 @@ class SpecialProtectedpages extends SpecialPage { } return "<span style='white-space: nowrap'>" . - Xml::label( wfMsg( 'restriction-level' ) , $this->IdLevel ) . ' ' . + Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . ' ' . Xml::tags( 'select', array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ), implode( "\n", $options ) ) . "</span>"; diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php index 982feb66..a80f0d0a 100644 --- a/includes/specials/SpecialProtectedtitles.php +++ b/includes/specials/SpecialProtectedtitles.php @@ -56,15 +56,14 @@ class SpecialProtectedtitles extends SpecialPage { $this->getOutput()->addHTML( $this->showOptions( $NS, $type, $level ) ); if ( $pager->getNumRows() ) { - $s = $pager->getNavigationBar(); - $s .= "<ul>" . - $pager->getBody() . - "</ul>"; - $s .= $pager->getNavigationBar(); + $this->getOutput()->addHTML( + $pager->getNavigationBar() . + '<ul>' . $pager->getBody() . '</ul>' . + $pager->getNavigationBar() + ); } else { - $s = '<p>' . wfMsgHtml( 'protectedtitlesempty' ) . '</p>'; + $this->getOutput()->addWikiMsg( 'protectedtitlesempty' ); } - $this->getOutput()->addHTML( $s ); } /** @@ -86,21 +85,20 @@ class SpecialProtectedtitles extends SpecialPage { $description_items = array (); - $protType = wfMsgHtml( 'restriction-level-' . $row->pt_create_perm ); + $protType = $this->msg( 'restriction-level-' . $row->pt_create_perm )->escaped(); $description_items[] = $protType; $lang = $this->getLanguage(); $expiry = strlen( $row->pt_expiry ) ? $lang->formatExpiry( $row->pt_expiry, TS_MW ) : $infinity; if( $expiry != $infinity ) { - $expiry_description = wfMsg( + $user = $this->getUser(); + $description_items[] = $this->msg( 'protect-expiring-local', - $lang->timeanddate( $expiry, true ), - $lang->date( $expiry, true ), - $lang->time( $expiry, true ) - ); - - $description_items[] = htmlspecialchars($expiry_description); + $lang->userTimeAndDate( $expiry, $user ), + $lang->userDate( $expiry, $user ), + $lang->userTime( $expiry, $user ) + )->escaped(); } wfProfileOut( __METHOD__ ); @@ -112,6 +110,7 @@ class SpecialProtectedtitles extends SpecialPage { * @param $namespace Integer: * @param $type string * @param $level string + * @return string * @private */ function showOptions( $namespace, $type='edit', $level ) { @@ -121,11 +120,11 @@ class SpecialProtectedtitles extends SpecialPage { $special = htmlspecialchars( $title->getPrefixedDBkey() ); return "<form action=\"$action\" method=\"get\">\n" . '<fieldset>' . - Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) . + Xml::element( 'legend', array(), $this->msg( 'protectedtitles' )->text() ) . Html::hidden( 'title', $special ) . " \n" . $this->getNamespaceMenu( $namespace ) . " \n" . $this->getLevelMenu( $level ) . " \n" . - " " . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . + " " . Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n" . "</fieldset></form>"; } @@ -137,9 +136,17 @@ class SpecialProtectedtitles extends SpecialPage { * @return string */ function getNamespaceMenu( $namespace = null ) { - return Xml::label( wfMsg( 'namespace' ), 'namespace' ) - . ' ' - . Xml::namespaceSelector( $namespace, '' ); + return Html::namespaceSelector( + array( + 'selected' => $namespace, + 'all' => '', + 'label' => $this->msg( 'namespace' )->text() + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ); } /** @@ -149,13 +156,13 @@ class SpecialProtectedtitles extends SpecialPage { function getLevelMenu( $pr_level ) { global $wgRestrictionLevels; - $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array + $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); // Temporary array $options = array(); // First pass to load the log names foreach( $wgRestrictionLevels as $type ) { if ( $type !='' && $type !='*') { - $text = wfMsg("restriction-level-$type"); + $text = $this->msg( "restriction-level-$type" )->text(); $m[$text] = $type; } } @@ -170,7 +177,7 @@ class SpecialProtectedtitles extends SpecialPage { } return - Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . ' ' . + Xml::label( $this->msg( 'restriction-level' )->text(), $this->IdLevel ) . ' ' . Xml::tags( 'select', array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ), implode( "\n", $options ) ); @@ -212,7 +219,7 @@ class ProtectedTitlesPager extends AlphabeticPager { * @return Title */ function getTitle() { - return SpecialPage::getTitleFor( 'Protectedtitles' ); + return $this->mForm->getTitle(); } function formatRow( $row ) { diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php index 0b6239bb..307088ed 100644 --- a/includes/specials/SpecialRandompage.php +++ b/includes/specials/SpecialRandompage.php @@ -85,7 +85,7 @@ class RandomPage extends SpecialPage { $nsNames = array(); foreach( $this->namespaces as $n ) { if( $n === NS_MAIN ) { - $nsNames[] = wfMsgNoTrans( 'blanknamespace' ); + $nsNames[] = $this->msg( 'blanknamespace' )->plain(); } else { $nsNames[] = $wgContLang->getNsText( $n ); } diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index daf47f62..2bd8b0a9 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -190,8 +190,8 @@ class SpecialRecentChanges extends IncludableSpecialPage { public function getFeedObject( $feedFormat ){ $changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' ); $formatter = $changesFeed->getFeedObject( - wfMsgForContent( 'recentchanges' ), - wfMsgForContent( 'recentchanges-feed-description' ), + $this->msg( 'recentchanges' )->inContentLanguage()->text(), + $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(), $this->getTitle()->getFullURL() ); return array( $changesFeed, $formatter ); @@ -366,7 +366,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { * * @param $conds Array * @param $opts FormOptions - * @return database result or false (for Recentchangeslinked only) + * @return bool|ResultWrapper result or false (for Recentchangeslinked only) */ public function doMainQuery( $conds, $opts ) { $tables = array( 'recentchanges' ); @@ -396,14 +396,15 @@ class SpecialRecentChanges extends IncludableSpecialPage { $fields[] = 'page_latest'; $join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id'); } - if ( !$this->including() ) { - // Tag stuff. - // Doesn't work when transcluding. See bug 23293 - ChangeTags::modifyDisplayQuery( - $tables, $fields, $conds, $join_conds, $query_options, - $opts['tagfilter'] - ); - } + // Tag stuff. + ChangeTags::modifyDisplayQuery( + $tables, + $fields, + $conds, + $join_conds, + $query_options, + $opts['tagfilter'] + ); if ( !wfRunHooks( 'SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ) ) ) @@ -534,6 +535,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { /** * Get the query string to append to feed link URLs. * This is overridden by RCL to add the target parameter + * @return bool */ public function getFeedQuery() { return false; @@ -564,17 +566,17 @@ class SpecialRecentChanges extends IncludableSpecialPage { $extraOpts = $this->getExtraOptions( $opts ); $extraOptsCount = count( $extraOpts ); $count = 0; - $submit = ' ' . Xml::submitbutton( wfMsg( 'allpagessubmit' ) ); + $submit = ' ' . Xml::submitbutton( $this->msg( 'allpagessubmit' )->text() ); $out = Xml::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) ); - foreach( $extraOpts as $optionRow ) { + foreach( $extraOpts as $name => $optionRow ) { # Add submit button to the last row only ++$count; - $addSubmit = $count === $extraOptsCount ? $submit : ''; + $addSubmit = ( $count === $extraOptsCount ) ? $submit : ''; $out .= Xml::openElement( 'tr' ); if( is_array( $optionRow ) ) { - $out .= Xml::tags( 'td', array( 'class' => 'mw-label' ), $optionRow[0] ); + $out .= Xml::tags( 'td', array( 'class' => 'mw-label mw-' . $name . '-label' ), $optionRow[0] ); $out .= Xml::tags( 'td', array( 'class' => 'mw-input' ), $optionRow[1] . $addSubmit ); } else { $out .= Xml::tags( 'td', array( 'class' => 'mw-input', 'colspan' => 2 ), $optionRow . $addSubmit ); @@ -595,7 +597,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { $panelString = implode( "\n", $panel ); $this->getOutput()->addHTML( - Xml::fieldset( wfMsg( 'recentchanges-legend' ), $panelString, array( 'class' => 'rcoptions' ) ) + Xml::fieldset( $this->msg( 'recentchanges-legend' )->text(), $panelString, array( 'class' => 'rcoptions' ) ) ); $this->setBottomText( $opts ); @@ -632,14 +634,18 @@ class SpecialRecentChanges extends IncludableSpecialPage { */ function setTopText( FormOptions $opts ) { global $wgContLang; - $this->getOutput()->addWikiText( - Html::rawElement( 'p', - array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), - "\n" . wfMsgForContentNoTrans( 'recentchangestext' ) . "\n" - ), - /* $lineStart */ false, - /* $interface */ false - ); + + $message = $this->msg( 'recentchangestext' )->inContentLanguage(); + if ( !$message->isDisabled() ) { + $this->getOutput()->addWikiText( + Html::rawElement( 'p', + array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), + "\n" . $message->plain() . "\n" + ), + /* $lineStart */ false, + /* $interface */ false + ); + } } /** @@ -662,16 +668,16 @@ class SpecialRecentChanges extends IncludableSpecialPage { array( 'selected' => $opts['namespace'], 'all' => '' ), array( 'name' => 'namespace', 'id' => 'namespace' ) ); - $nsLabel = Xml::label( wfMsg( 'namespace' ), 'namespace' ); + $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ); $invert = Xml::checkLabel( - wfMsg( 'invert' ), 'invert', 'nsinvert', + $this->msg( 'invert' )->text(), 'invert', 'nsinvert', $opts['invert'], - array( 'title' => wfMsg( 'tooltip-invert' ) ) + array( 'title' => $this->msg( 'tooltip-invert' )->text() ) ); $associated = Xml::checkLabel( - wfMsg( 'namespace_association' ), 'associated', 'nsassociated', + $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated', $opts['associated'], - array( 'title' => wfMsg( 'tooltip-namespace_association' ) ) + array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() ) ); return array( $nsLabel, "$nsSelect $invert $associated" ); } @@ -683,10 +689,10 @@ class SpecialRecentChanges extends IncludableSpecialPage { * @return Array */ protected function categoryFilterForm( FormOptions $opts ) { - list( $label, $input ) = Xml::inputLabelSep( wfMsg( 'rc_categories' ), + list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(), 'categories', 'mw-categories', false, $opts['categories'] ); - $input .= ' ' . Xml::checkLabel( wfMsg( 'rc_categories_any' ), + $input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(), 'categories_any', 'mw-categories_any', $opts['categories_any'] ); return array( $label, $input ); @@ -763,9 +769,20 @@ class SpecialRecentChanges extends IncludableSpecialPage { * @param $override Array: options to override * @param $options Array: current options * @param $active Boolean: whether to show the link in bold + * @return string */ function makeOptionsLink( $title, $override, $options, $active = false ) { $params = $override + $options; + + // Bug 36524: false values have be converted to "0" otherwise + // wfArrayToCgi() will omit it them. + foreach ( $params as &$value ) { + if ( $value === false ) { + $value = '0'; + } + } + unset( $value ); + $text = htmlspecialchars( $title ); if ( $active ) { $text = '<strong>' . $text . '</strong>'; @@ -778,6 +795,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { * * @param $defaults Array * @param $nondefaults Array + * @return string */ function optionsPanel( $defaults, $nondefaults ) { global $wgRCLinkLimits, $wgRCLinkDays; @@ -785,16 +803,18 @@ class SpecialRecentChanges extends IncludableSpecialPage { $options = $nondefaults + $defaults; $note = ''; - if( !wfEmptyMsg( 'rclegend' ) ) { - $note .= '<div class="mw-rclegend">' . - wfMsgExt( 'rclegend', array( 'parseinline' ) ) . "</div>\n"; + $msg = $this->msg( 'rclegend' ); + if( !$msg->isDisabled() ) { + $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n"; } + + $lang = $this->getLanguage(); + $user = $this->getUser(); if( $options['from'] ) { - $note .= wfMsgExt( 'rcnotefrom', array( 'parseinline' ), - $this->getLanguage()->formatNum( $options['limit'] ), - $this->getLanguage()->timeanddate( $options['from'], true ), - $this->getLanguage()->date( $options['from'], true ), - $this->getLanguage()->time( $options['from'], true ) ) . '<br />'; + $note .= $this->msg( 'rcnotefrom' )->numParams( $options['limit'] )->params( + $lang->userTimeAndDate( $options['from'], $user ), + $lang->userDate( $options['from'], $user ), + $lang->userTime( $options['from'], $user ) )->parse() . '<br />'; } # Sort data for display and make sure it's unique after we've added user data. @@ -807,21 +827,21 @@ class SpecialRecentChanges extends IncludableSpecialPage { // limit links foreach( $wgRCLinkLimits as $value ) { - $cl[] = $this->makeOptionsLink( $this->getLanguage()->formatNum( $value ), + $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ), array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ); } - $cl = $this->getLanguage()->pipeList( $cl ); + $cl = $lang->pipeList( $cl ); // day links, reset 'from' to none foreach( $wgRCLinkDays as $value ) { - $dl[] = $this->makeOptionsLink( $this->getLanguage()->formatNum( $value ), + $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ), array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] ); } - $dl = $this->getLanguage()->pipeList( $dl ); + $dl = $lang->pipeList( $dl ); // show/hide links - $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) ); + $showhide = array( $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() ); $filters = array( 'hideminor' => 'rcshowhideminor', 'hidebots' => 'rcshowhidebots', @@ -834,7 +854,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { $filters[$key] = $params['msg']; } // Disable some if needed - if ( !$this->getUser()->useRCPatrol() ) { + if ( !$user->useRCPatrol() ) { unset( $filters['hidepatrolled'] ); } @@ -842,19 +862,18 @@ class SpecialRecentChanges extends IncludableSpecialPage { foreach ( $filters as $key => $msg ) { $link = $this->makeOptionsLink( $showhide[1 - $options[$key]], array( $key => 1-$options[$key] ), $nondefaults ); - $links[] = wfMsgHtml( $msg, $link ); + $links[] = $this->msg( $msg )->rawParams( $link )->escaped(); } // show from this onward link $timestamp = wfTimestampNow(); - $now = $this->getLanguage()->timeanddate( $timestamp, true ); + $now = $lang->userTimeAndDate( $timestamp, $user ); $tl = $this->makeOptionsLink( $now, array( 'from' => $timestamp ), $nondefaults ); - $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter' ), - $cl, $dl, $this->getLanguage()->pipeList( $links ) ); - $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter' ), $tl ); + $rclinks = $this->msg( 'rclinks' )->rawParams( $cl, $dl, $lang->pipeList( $links ) )->parse(); + $rclistfrom = $this->msg( 'rclistfrom' )->rawParams( $tl )->parse(); return "{$note}$rclinks<br />$rclistfrom"; } diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index 1f556f89..862736d3 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -54,8 +54,9 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { public function getFeedObject( $feedFormat ){ $feed = new ChangesFeed( $feedFormat, false ); $feedObj = $feed->getFeedObject( - wfMsgForContent( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() ), - wfMsgForContent( 'recentchangeslinked-feed' ), + $this->msg( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() ) + ->inContentLanguage()->text(), + $this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(), $this->getTitle()->getFullUrl() ); return array( $feed, $feedObj ); @@ -88,7 +89,7 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { */ $dbr = wfGetDB( DB_SLAVE, 'recentchangeslinked' ); - $id = $title->getArticleId(); + $id = $title->getArticleID(); $ns = $title->getNamespace(); $dbkey = $title->getDBkey(); @@ -109,10 +110,14 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { $join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id'); $select[] = 'page_latest'; } - if ( !$this->including() ) { // bug 23293 - ChangeTags::modifyDisplayQuery( $tables, $select, $conds, $join_conds, - $query_options, $opts['tagfilter'] ); - } + ChangeTags::modifyDisplayQuery( + $tables, + $select, + $conds, + $join_conds, + $query_options, + $opts['tagfilter'] + ); if ( !wfRunHooks( 'SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$select ) ) ) { return false; @@ -224,10 +229,10 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { $opts->consumeValues( array( 'showlinkedto', 'target', 'tagfilter' ) ); $extraOpts = array(); $extraOpts['namespace'] = $this->namespaceFilterForm( $opts ); - $extraOpts['target'] = array( wfMsgHtml( 'recentchangeslinked-page' ), + $extraOpts['target'] = array( $this->msg( 'recentchangeslinked-page' )->escaped(), Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) . Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' . - Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) ); + Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) ); $tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] ); if ($tagFilter) { $extraOpts['tagfilter'] = $tagFilter; diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index df60a26a..aba90cf8 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -66,6 +66,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { 'success' => 'revdelete-success', 'failure' => 'revdelete-failure', 'list-class' => 'RevDel_RevisionList', + 'permission' => 'deleterevision', ), 'archive' => array( 'check-label' => 'revdelete-hide-text', @@ -73,6 +74,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { 'success' => 'revdelete-success', 'failure' => 'revdelete-failure', 'list-class' => 'RevDel_ArchiveList', + 'permission' => 'deleterevision', ), 'oldimage'=> array( 'check-label' => 'revdelete-hide-image', @@ -80,6 +82,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { 'success' => 'revdelete-success', 'failure' => 'revdelete-failure', 'list-class' => 'RevDel_FileList', + 'permission' => 'deleterevision', ), 'filearchive' => array( 'check-label' => 'revdelete-hide-image', @@ -87,6 +90,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { 'success' => 'revdelete-success', 'failure' => 'revdelete-failure', 'list-class' => 'RevDel_ArchivedFileList', + 'permission' => 'deleterevision', ), 'logging' => array( 'check-label' => 'revdelete-hide-name', @@ -94,6 +98,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { 'success' => 'logdelete-success', 'failure' => 'logdelete-failure', 'list-class' => 'RevDel_LogList', + 'permission' => 'deletelogentry', ), ); @@ -117,7 +122,6 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $output = $this->getOutput(); $user = $this->getUser(); - $this->mIsAllowed = $user->isAllowed('deleterevision'); // for changes $this->setHeaders(); $this->outputHeader(); $request = $this->getRequest(); @@ -143,6 +147,24 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { } else { $this->typeName = $request->getVal( 'type' ); $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); + if ( $this->targetObj->isSpecial( 'Log' ) ) { + $result = wfGetDB( DB_SLAVE )->select( 'logging', + 'log_type', + array( 'log_id' => $this->ids ), + __METHOD__, + array( 'DISTINCT' ) + ); + + $logTypes = array(); + foreach ( $result as $row ) { + $logTypes[] = $row->log_type; + } + + if ( count( $logTypes ) == 1 ) { + // If there's only one type, the target can be set to include it. + $this->targetObj = SpecialPage::getTitleFor( 'Log', $logTypes[0] ); + } + } } # For reviewing deleted files... @@ -159,10 +181,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { # No targets? if( !isset( self::$allowedTypes[$this->typeName] ) || count( $this->ids ) == 0 ) { - $output->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); - return; + throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); } $this->typeInfo = self::$allowedTypes[$this->typeName]; + $this->mIsAllowed = $user->isAllowed( $this->typeInfo['permission'] ); # If we have revisions, get the title from the first one # since they should all be from the same page. This allows @@ -201,12 +223,14 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $qc = $this->getLogQueryCond(); # Show relevant lines from the deletion log - $output->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); + $deleteLogPage = new LogPage( 'delete' ); + $output->addHTML( "<h2>" . $deleteLogPage->getName()->escaped() . "</h2>\n" ); LogEventsList::showLogExtract( $output, 'delete', $this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) ); # Show relevant lines from the suppression log if( $user->isAllowed( 'suppressionlog' ) ) { - $output->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" ); + $suppressLogPage = new LogPage( 'suppress' ); + $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" ); LogEventsList::showLogExtract( $output, 'suppress', $this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) ); } @@ -221,7 +245,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $links = array(); $links[] = Linker::linkKnown( SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'viewpagelogs' ), + $this->msg( 'viewpagelogs' )->escaped(), array(), array( 'page' => $this->targetObj->getPrefixedText() ) ); @@ -229,7 +253,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { # Give a link to the page history $links[] = Linker::linkKnown( $this->targetObj, - wfMsgHtml( 'pagehist' ), + $this->msg( 'pagehist' )->escaped(), array(), array( 'action' => 'history' ) ); @@ -238,7 +262,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $undelete = SpecialPage::getTitleFor( 'Undelete' ); $links[] = Linker::linkKnown( $undelete, - wfMsgHtml( 'deletedhist' ), + $this->msg( 'deletedhist' )->escaped(), array(), array( 'target' => $this->targetObj->getPrefixedDBkey() ) ); @@ -251,6 +275,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { /** * Get the condition used for fetching log snippets + * @return array */ protected function getLogQueryCond() { $conds = array(); @@ -275,29 +300,30 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $this->getOutput()->addWikiMsg( 'revdelete-no-file' ); return; } - if( !$oimage->userCan( File::DELETED_FILE, $this->getUser() ) ) { + $user = $this->getUser(); + if( !$oimage->userCan( File::DELETED_FILE, $user ) ) { if( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) { - $this->getOutput()->permissionRequired( 'suppressrevision' ); + throw new PermissionsError( 'suppressrevision' ); } else { - $this->getOutput()->permissionRequired( 'deletedtext' ); + throw new PermissionsError( 'deletedtext' ); } - return; } - if ( !$this->getUser()->matchEditToken( $this->token, $archiveName ) ) { + if ( !$user->matchEditToken( $this->token, $archiveName ) ) { + $lang = $this->getLanguage(); $this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm', $this->targetObj->getText(), - $this->getLanguage()->date( $oimage->getTimestamp() ), - $this->getLanguage()->time( $oimage->getTimestamp() ) ); + $lang->userDate( $oimage->getTimestamp(), $user ), + $lang->userTime( $oimage->getTimestamp(), $user ) ); $this->getOutput()->addHTML( Xml::openElement( 'form', array( 'method' => 'POST', 'action' => $this->getTitle()->getLocalUrl( - 'target=' . urlencode( $oimage->getName() ) . + 'target=' . urlencode( $this->targetObj->getPrefixedDBkey() ) . '&file=' . urlencode( $archiveName ) . - '&token=' . urlencode( $this->getUser()->getEditToken( $archiveName ) ) ) + '&token=' . urlencode( $user->getEditToken( $archiveName ) ) ) ) ) . - Xml::submitButton( wfMsg( 'revdelete-show-file-submit' ) ) . + Xml::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) . '</form>' ); return; @@ -350,8 +376,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $item = $list->current(); if ( !$item->canView() ) { if( !$this->submitClicked ) { - $this->getOutput()->permissionRequired( 'suppressrevision' ); - return; + throw new PermissionsError( 'suppressrevision' ); } $UserAllowed = false; } @@ -360,8 +385,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { } if( !$numRevisions ) { - $this->getOutput()->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); - return; + throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); } $this->getOutput()->addHTML( "</ul>" ); @@ -376,22 +400,23 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $out = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ), 'id' => 'mw-revdel-form-revisions' ) ) . - Xml::fieldset( wfMsg( 'revdelete-legend' ) ) . + Xml::fieldset( $this->msg( 'revdelete-legend' )->text() ) . $this->buildCheckBoxes() . Xml::openElement( 'table' ) . "<tr>\n" . '<td class="mw-label">' . - Xml::label( wfMsg( 'revdelete-log' ), 'wpRevDeleteReasonList' ) . + Xml::label( $this->msg( 'revdelete-log' )->text(), 'wpRevDeleteReasonList' ) . '</td>' . '<td class="mw-input">' . Xml::listDropDown( 'wpRevDeleteReasonList', - wfMsgForContent( 'revdelete-reason-dropdown' ), - wfMsgForContent( 'revdelete-reasonotherlist' ), '', 'wpReasonDropDown', 1 + $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->text(), + $this->msg( 'revdelete-reasonotherlist' )->inContentLanguage()->text(), + '', 'wpReasonDropDown', 1 ) . '</td>' . "</tr><tr>\n" . '<td class="mw-label">' . - Xml::label( wfMsg( 'revdelete-otherreason' ), 'wpReason' ) . + Xml::label( $this->msg( 'revdelete-otherreason' )->text(), 'wpReason' ) . '</td>' . '<td class="mw-input">' . Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason', 'maxlength' => 100 ) ) . @@ -399,7 +424,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { "</tr><tr>\n" . '<td></td>' . '<td class="mw-submit">' . - Xml::submitButton( wfMsgExt('revdelete-submit','parsemag',$numRevisions), + Xml::submitButton( $this->msg( 'revdelete-submit', $numRevisions )->text(), array( 'name' => 'wpSubmit' ) ) . '</td>' . "</tr>\n" . @@ -416,10 +441,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $out .= Xml::closeElement( 'form' ) . "\n"; // Show link to edit the dropdown reasons if( $this->getUser()->isAllowed( 'editinterface' ) ) { - $title = Title::makeTitle( NS_MEDIAWIKI, 'revdelete-reason-dropdown' ); + $title = Title::makeTitle( NS_MEDIAWIKI, 'Revdelete-reason-dropdown' ); $link = Linker::link( $title, - wfMsgHtml( 'revdelete-edit-reasonlist' ), + $this->msg( 'revdelete-edit-reasonlist' )->escaped(), array(), array( 'action' => 'edit' ) ); @@ -458,7 +483,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { } foreach( $this->checks as $item ) { list( $message, $name, $field ) = $item; - $innerHTML = Xml::checkLabel( wfMsg($message), $name, $name, $bitfield & $field ); + $innerHTML = Xml::checkLabel( $this->msg( $message )->text(), $name, $name, $bitfield & $field ); if( $field == Revision::DELETED_RESTRICTED ) $innerHTML = "<b>$innerHTML</b>"; $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML ); @@ -467,9 +492,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { // Otherwise, use tri-state radios } else { $html .= '<tr>'; - $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-same').'</th>'; - $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-unset').'</th>'; - $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-set').'</th>'; + $html .= '<th class="mw-revdel-checkbox">' . $this->msg( 'revdelete-radio-same' )->escaped() . '</th>'; + $html .= '<th class="mw-revdel-checkbox">' . $this->msg( 'revdelete-radio-unset' )->escaped() . '</th>'; + $html .= '<th class="mw-revdel-checkbox">' . $this->msg( 'revdelete-radio-set' )->escaped() . '</th>'; $html .= "<th></th></tr>\n"; foreach( $this->checks as $item ) { list( $message, $name, $field ) = $item; @@ -482,7 +507,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $line = '<td class="mw-revdel-checkbox">' . Xml::radio( $name, -1, $selected == -1 ) . '</td>'; $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>'; $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>'; - $label = wfMsgHtml($message); + $label = $this->msg( $message )->escaped(); if( $field == Revision::DELETED_RESTRICTED ) { $label = "<b>$label</b>"; } @@ -497,6 +522,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { /** * UI entry point for form submission. + * @return bool */ protected function submit() { # Check edit token on submission @@ -510,14 +536,13 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $comment = $listReason; if( $comment != 'other' && $this->otherReason != '' ) { // Entry from drop down menu + additional comment - $comment .= wfMsgForContent( 'colon-separator' ) . $this->otherReason; + $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->otherReason; } elseif( $comment == 'other' ) { $comment = $this->otherReason; } # Can the user set this field? if( $bitParams[Revision::DELETED_RESTRICTED]==1 && !$this->getUser()->isAllowed('suppressrevision') ) { - $this->getOutput()->permissionRequired( 'suppressrevision' ); - return false; + throw new PermissionsError( 'suppressrevision' ); } # If the save went through, go to success message... $status = $this->save( $bitParams, $comment, $this->targetObj ); @@ -592,6 +617,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { /** * Do the write operations. Simple wrapper for RevDel_*List::setVisibility(). + * @param $bitfield + * @param $reason + * @param $title + * @return */ protected function save( $bitfield, $reason, $title ) { return $this->getList()->setVisibility( diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 3fa86875..5f5b6b4d 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -174,7 +174,8 @@ class SpecialSearch extends SpecialPage { $t = Title::newFromText( $term ); # If the string cannot be used to create a title if( is_null( $t ) ) { - return $this->showResults( $term ); + $this->showResults( $term ); + return; } # If there's an exact or very near match, jump right there. $t = SearchEngine::getNearMatch( $term ); @@ -201,7 +202,7 @@ class SpecialSearch extends SpecialPage { return; } } - return $this->showResults( $term ); + $this->showResults( $term ); } /** @@ -232,13 +233,13 @@ class SpecialSearch extends SpecialPage { } else { $out->addHTML( Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'search-external' ) ) . - Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) . - wfMsg( 'googlesearch', + Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) . + Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), $this->msg( 'searchdisabled' )->text() ) . + $this->msg( 'googlesearch' )->rawParams( htmlspecialchars( $term ), - htmlspecialchars( 'UTF-8' ), - htmlspecialchars( wfMsg( 'searchbutton' ) ) - ) . + 'UTF-8', + $this->msg( 'searchbutton' )->escaped() + )->text() . Xml::closeElement( 'fieldset' ) ); } @@ -285,7 +286,7 @@ class SpecialSearch extends SpecialPage { $stParams ); - $this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>'; + $this->didYouMeanHtml = '<div class="searchdidyoumean">' . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>'; } // start rendering the page $out->addHtml( @@ -299,7 +300,7 @@ class SpecialSearch extends SpecialPage { ) ); $out->addHtml( - Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . + Xml::openElement( 'table', array( 'id' => 'mw-search-top-table', 'cellpadding' => 0, 'cellspacing' => 0 ) ) . Xml::openElement( 'tr' ) . Xml::openElement( 'td' ) . "\n" . $this->shortDialog( $term ) . @@ -583,13 +584,8 @@ class SpecialSearch extends SpecialPage { $redirectText = null; $redirect = "<span class='searchalttitle'>" . - wfMsg( - 'search-redirect', - Linker::linkKnown( - $redirectTitle, - $redirectText - ) - ) . + $this->msg( 'search-redirect' )->rawParams( + Linker::linkKnown( $redirectTitle, $redirectText ) )->text() . "</span>"; } @@ -600,12 +596,8 @@ class SpecialSearch extends SpecialPage { $sectionText = null; $section = "<span class='searchalttitle'>" . - wfMsg( - 'search-section', Linker::linkKnown( - $sectionTitle, - $sectionText - ) - ) . + $this->msg( 'search-section' )->rawParams( + Linker::linkKnown( $sectionTitle, $sectionText ) )->text() . "</span>"; } @@ -620,7 +612,7 @@ class SpecialSearch extends SpecialPage { $score = ''; } else { $percent = sprintf( '%2.1f', $result->getScore() * 100 ); - $score = wfMsg( 'search-result-score', $lang->formatNum( $percent ) ) + $score = $this->msg( 'search-result-score' )->numParams( $percent )->text() . ' - '; } @@ -628,25 +620,17 @@ class SpecialSearch extends SpecialPage { $byteSize = $result->getByteSize(); $wordCount = $result->getWordCount(); $timestamp = $result->getTimestamp(); - $size = wfMsgExt( - 'search-result-size', - array( 'parsemag', 'escape' ), - $lang->formatSize( $byteSize ), - $lang->formatNum( $wordCount ) - ); + $size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) ) + ->numParams( $wordCount )->escaped(); if( $t->getNamespace() == NS_CATEGORY ) { $cat = Category::newFromTitle( $t ); - $size = wfMsgExt( - 'search-result-category-size', - array( 'parsemag', 'escape' ), - $lang->formatNum( $cat->getPageCount() ), - $lang->formatNum( $cat->getSubcatCount() ), - $lang->formatNum( $cat->getFileCount() ) - ); + $size = $this->msg( 'search-result-category-size' ) + ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() ) + ->escaped(); } - $date = $lang->timeanddate( $timestamp ); + $date = $lang->userTimeAndDate( $timestamp, $this->getUser() ); // link to related articles if supported $related = ''; @@ -655,14 +639,15 @@ class SpecialSearch extends SpecialPage { $stParams = array_merge( $this->powerSearchOptions(), array( - 'search' => wfMsgForContent( 'searchrelated' ) . ':' . $t->getPrefixedText(), - 'fulltext' => wfMsg( 'search' ) + 'search' => $this->msg( 'searchrelated' )->inContentLanguage()->text() . + ':' . $t->getPrefixedText(), + 'fulltext' => $this->msg( 'search' )->text() ) ); $related = ' -- ' . Linker::linkKnown( $st, - wfMsg('search-relatedarticle'), + $this->msg( 'search-relatedarticle' )->text(), array(), $stParams ); @@ -674,7 +659,7 @@ class SpecialSearch extends SpecialPage { if( $img ) { $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); if( $thumb ) { - $desc = wfMsg( 'parentheses', $img->getShortDesc() ); + $desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped(); wfProfileOut( __METHOD__ ); // Float doesn't seem to interact well with the bullets. // Table messes up vertical alignment of the bullets. @@ -682,10 +667,10 @@ class SpecialSearch extends SpecialPage { return "<li>" . '<table class="searchResultImage">' . '<tr>' . - '<td width="120" align="center" valign="top">' . + '<td width="120" style="text-align: center; vertical-align: top;">' . $thumb->toHtml( array( 'desc-link' => true ) ) . '</td>' . - '<td valign="top">' . + '<td style="vertical-align: top;">' . $link . $extract . "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" . @@ -718,12 +703,12 @@ class SpecialSearch extends SpecialPage { $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>". - wfMsg('search-interwiki-caption')."</div>\n"; + $this->msg( 'search-interwiki-caption' )->text() . "</div>\n"; $out .= "<ul class='mw-search-iwresults'>\n"; // work out custom project captions $customCaptions = array(); - $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption> + $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); // format per line <iwprefix>:<caption> foreach($customLines as $line) { $parts = explode(":",$line,2); if(count($parts) == 2) // validate line @@ -786,13 +771,8 @@ class SpecialSearch extends SpecialPage { $redirectText = null; $redirect = "<span class='searchalttitle'>" . - wfMsg( - 'search-redirect', - Linker::linkKnown( - $redirectTitle, - $redirectText - ) - ) . + $this->msg( 'search-redirect' )->rawParams( + Linker::linkKnown( $redirectTitle, $redirectText ) )->text() . "</span>"; } @@ -806,13 +786,13 @@ class SpecialSearch extends SpecialPage { // default is to show the hostname of the other wiki which might suck // if there are many wikis on one hostname $parsed = wfParseUrl( $t->getFullURL() ); - $caption = wfMsg('search-interwiki-default', $parsed['host']); + $caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text(); } // "more results" link (special page stuff could be localized, but we might not know target lang) $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search"); $searchLink = Linker::linkKnown( $searchTitle, - wfMsg('search-interwiki-more'), + $this->msg( 'search-interwiki-more' )->text(), array(), array( 'search' => $query, @@ -865,7 +845,7 @@ class SpecialSearch extends SpecialPage { } $name = str_replace( '_', ' ', $name ); if( $name == '' ) { - $name = wfMsg( 'blanknamespace' ); + $name = $this->msg( 'blanknamespace' )->text(); } $rows[$subject] .= Xml::openElement( @@ -888,7 +868,7 @@ class SpecialSearch extends SpecialPage { for( $i = 0; $i < $numRows; $i += 4 ) { $namespaceTables .= Xml::openElement( 'table', - array( 'cellpadding' => 0, 'cellspacing' => 0, 'border' => 0 ) + array( 'cellpadding' => 0, 'cellspacing' => 0 ) ); for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) { $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] ); @@ -901,7 +881,7 @@ class SpecialSearch extends SpecialPage { // Show redirects check only if backend supports it if( $this->getSearchEngine()->supports( 'list-redirects' ) ) { $showSections['redirects'] = - Xml::checkLabel( wfMsg( 'powersearch-redir' ), 'redirs', 'redirs', $this->searchRedirects ); + Xml::checkLabel( $this->msg( 'powersearch-redir' )->text(), 'redirs', 'redirs', $this->searchRedirects ); } wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) ); @@ -917,29 +897,9 @@ class SpecialSearch extends SpecialPage { 'fieldset', array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' ) ) . - Xml::element( 'legend', null, wfMsg('powersearch-legend') ) . - Xml::tags( 'h4', null, wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) ) . - Xml::tags( - 'div', - array( 'id' => 'mw-search-togglebox' ), - Xml::label( wfMsg( 'powersearch-togglelabel' ), 'mw-search-togglelabel' ) . - Xml::element( - 'input', - array( - 'type'=>'button', - 'id' => 'mw-search-toggleall', - 'value' => wfMsg( 'powersearch-toggleall' ) - ) - ) . - Xml::element( - 'input', - array( - 'type'=>'button', - 'id' => 'mw-search-togglenone', - 'value' => wfMsg( 'powersearch-togglenone' ) - ) - ) - ) . + Xml::element( 'legend', null, $this->msg('powersearch-legend' )->text() ) . + Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) . + Html::element( 'div', array( 'id' => 'mw-search-togglebox' ) ) . Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . implode( Xml::element( 'div', array( 'class' => 'divider' ), '', false ), $showSections ) . $hidden . @@ -1034,8 +994,8 @@ class SpecialSearch extends SpecialPage { $this->makeSearchLink( $bareterm, array(), - wfMsg( $profile['message'] ), - wfMsg( $profile['tooltip'], $tooltipParam ), + $this->msg( $profile['message'] )->text(), + $this->msg( $profile['tooltip'], $tooltipParam )->text(), $profile['parameters'] ) ); @@ -1046,24 +1006,19 @@ class SpecialSearch extends SpecialPage { // Results-info if ( $resultsShown > 0 ) { if ( $totalNum > 0 ){ - $top = wfMsgExt( 'showingresultsheader', array( 'parseinline' ), - $lang->formatNum( $this->offset + 1 ), - $lang->formatNum( $this->offset + $resultsShown ), - $lang->formatNum( $totalNum ), - wfEscapeWikiText( $term ), - $lang->formatNum( $resultsShown ) - ); + $top = $this->msg( 'showingresultsheader' ) + ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum ) + ->params( wfEscapeWikiText( $term ) ) + ->numParams( $resultsShown ) + ->parse(); } elseif ( $resultsShown >= $this->limit ) { - $top = wfMsgExt( 'showingresults', array( 'parseinline' ), - $lang->formatNum( $this->limit ), - $lang->formatNum( $this->offset + 1 ) - ); + $top = $this->msg( 'showingresults' ) + ->numParams( $this->limit, $this->offset + 1 ) + ->parse(); } else { - $top = wfMsgExt( 'showingresultsnum', array( 'parseinline' ), - $lang->formatNum( $this->limit ), - $lang->formatNum( $this->offset + 1 ), - $lang->formatNum( $resultsShown ) - ); + $top = $this->msg( 'showingresultsnum' ) + ->numParams( $this->limit, $this->offset + 1, $resultsShown ) + ->parse(); } $out .= Xml::tags( 'div', array( 'class' => 'results-info' ), Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) ) @@ -1090,7 +1045,7 @@ class SpecialSearch extends SpecialPage { 'autofocus' ) ) . "\n"; $out .= Html::hidden( 'fulltext', 'Search' ) . "\n"; - $out .= Xml::submitButton( wfMsg( 'searchbutton' ) ) . "\n"; + $out .= Xml::submitButton( $this->msg( 'searchbutton' )->text() ) . "\n"; return $out . $this->didYouMeanHtml; } @@ -1114,7 +1069,7 @@ class SpecialSearch extends SpecialPage { $stParams = array_merge( array( 'search' => $term, - 'fulltext' => wfMsg( 'search' ) + 'fulltext' => $this->msg( 'search' )->text() ), $opt ); @@ -1152,7 +1107,7 @@ class SpecialSearch extends SpecialPage { */ protected function startsWithAll( $term ) { - $allkeyword = wfMsgForContent('searchall'); + $allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text(); $p = explode( ':', $term ); if( count( $p ) > 1 ) { diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php index c176f913..5a4e8f03 100644 --- a/includes/specials/SpecialShortpages.php +++ b/includes/specials/SpecialShortpages.php @@ -40,10 +40,11 @@ class ShortPagesPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'page' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'page_len AS value' ), - 'conds' => array ( 'page_namespace' => NS_MAIN, + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_len' ), + 'conds' => array ( 'page_namespace' => + MWNamespace::getContentNamespaces(), 'page_is_redirect' => 0 ), 'options' => array ( 'USE INDEX' => 'page_redirect_namespace_len' ) ); @@ -61,16 +62,17 @@ class ShortPagesPage extends QueryPage { function preprocessResults( $db, $res ) { # There's no point doing a batch check if we aren't caching results; # the page must exist for it to have been pulled out of the table - if( $this->isCached() ) { - $batch = new LinkBatch(); - foreach ( $res as $row ) { - $batch->add( $row->namespace, $row->title ); - } - $batch->execute(); - if ( $db->numRows( $res ) > 0 ) { - $db->dataSeek( $res, 0 ); - } + if ( !$this->isCached() || !$res->numRows() ) { + return; } + + $batch = new LinkBatch(); + foreach ( $res as $row ) { + $batch->add( $row->namespace, $row->title ); + } + $batch->execute(); + + $res->seek( 0 ); } function sortDescending() { @@ -80,23 +82,32 @@ class ShortPagesPage extends QueryPage { function formatResult( $skin, $result ) { $dm = $this->getLanguage()->getDirMark(); - $title = Title::makeTitle( $result->namespace, $result->title ); + $title = Title::makeTitleSafe( $result->namespace, $result->title ); if ( !$title ) { - return '<!-- Invalid title ' . htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->'; + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); } + $hlink = Linker::linkKnown( $title, - wfMsgHtml( 'hist' ), + $this->msg( 'hist' )->escaped(), array(), array( 'action' => 'history' ) ); - $plink = $this->isCached() - ? Linker::link( $title ) - : Linker::linkKnown( $title ); + $hlinkInParentheses = $this->msg( 'parentheses' )->rawParams( $hlink )->escaped(); + + if ( $this->isCached() ) { + $plink = Linker::link( $title ); + $exists = $title->exists(); + } else { + $plink = Linker::linkKnown( $title ); + $exists = true; + } + $size = $this->msg( 'nbytes' )->numParams( $result->value )->escaped(); - return $title->exists() - ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]" - : "<del>({$hlink}) {$dm}{$plink} {$dm}[{$size}]</del>"; + return $exists + ? "${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]" + : "<del>${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]</del>"; } } diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php index b9c092b6..46881ec4 100644 --- a/includes/specials/SpecialStatistics.php +++ b/includes/specials/SpecialStatistics.php @@ -97,7 +97,7 @@ class SpecialStatistics extends SpecialPage { $text .= Xml::closeElement( 'table' ); # Customizable footer - $footer = wfMessage( 'statistics-footer' ); + $footer = $this->msg( 'statistics-footer' ); if ( !$footer->isBlank() ) { $text .= "\n" . $footer->parse(); } @@ -116,11 +116,11 @@ class SpecialStatistics extends SpecialPage { */ private function formatRow( $text, $number, $trExtraParams = array(), $descMsg = '', $descMsgParam = '' ) { if( $descMsg ) { - $msg = wfMessage( $descMsg, $descMsgParam ); + $msg = $this->msg( $descMsg, $descMsgParam ); if ( $msg->exists() ) { - $descriptionText = $msg->parse(); + $descriptionText = $this->msg( 'parentheses' )->rawParams( $msg->parse() )->escaped(); $text .= "<br />" . Xml::element( 'small', array( 'class' => 'mw-statistic-desc'), - " ($descriptionText)" ); + " $descriptionText" ); } } return Html::rawElement( 'tr', $trExtraParams, @@ -136,29 +136,29 @@ class SpecialStatistics extends SpecialPage { */ private function getPageStats() { return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-pages', array( 'parseinline' ) ) ) . + Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-pages' )->parse() ) . Xml::closeElement( 'tr' ) . $this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'Allpages' ), - wfMsgExt( 'statistics-articles', array( 'parseinline' ) ) ), + $this->msg( 'statistics-articles' )->parse() ), $this->getLanguage()->formatNum( $this->good ), array( 'class' => 'mw-statistics-articles' ) ) . - $this->formatRow( wfMsgExt( 'statistics-pages', array( 'parseinline' ) ), + $this->formatRow( $this->msg( 'statistics-pages' )->parse(), $this->getLanguage()->formatNum( $this->total ), array( 'class' => 'mw-statistics-pages' ), 'statistics-pages-desc' ) . $this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'Listfiles' ), - wfMsgExt( 'statistics-files', array( 'parseinline' ) ) ), + $this->msg( 'statistics-files' )->parse() ), $this->getLanguage()->formatNum( $this->images ), array( 'class' => 'mw-statistics-files' ) ); } private function getEditStats() { return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-edits', array( 'parseinline' ) ) ) . + Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-edits' )->parse() ) . Xml::closeElement( 'tr' ) . - $this->formatRow( wfMsgExt( 'statistics-edits', array( 'parseinline' ) ), + $this->formatRow( $this->msg( 'statistics-edits' )->parse(), $this->getLanguage()->formatNum( $this->edits ), array( 'class' => 'mw-statistics-edits' ) ) . - $this->formatRow( wfMsgExt( 'statistics-edits-average', array( 'parseinline' ) ), + $this->formatRow( $this->msg( 'statistics-edits-average' )->parse(), $this->getLanguage()->formatNum( sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) ), array( 'class' => 'mw-statistics-edits-average' ) ); } @@ -166,15 +166,15 @@ class SpecialStatistics extends SpecialPage { private function getUserStats() { global $wgActiveUserDays; return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-users', array( 'parseinline' ) ) ) . + Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-users' )->parse() ) . Xml::closeElement( 'tr' ) . - $this->formatRow( wfMsgExt( 'statistics-users', array( 'parseinline' ) ), + $this->formatRow( $this->msg( 'statistics-users' )->parse(), $this->getLanguage()->formatNum( $this->users ), array( 'class' => 'mw-statistics-users' ) ) . - $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ) . ' ' . + $this->formatRow( $this->msg( 'statistics-users-active' )->parse() . ' ' . Linker::linkKnown( SpecialPage::getTitleFor( 'Activeusers' ), - wfMsgHtml( 'listgrouprights-members' ) + $this->msg( 'listgrouprights-members' )->escaped() ), $this->getLanguage()->formatNum( $this->activeUsers ), array( 'class' => 'mw-statistics-users-active' ), @@ -191,13 +191,13 @@ class SpecialStatistics extends SpecialPage { continue; } $groupname = htmlspecialchars( $group ); - $msg = wfMessage( 'group-' . $groupname ); + $msg = $this->msg( 'group-' . $groupname ); if ( $msg->isBlank() ) { $groupnameLocalized = $groupname; } else { $groupnameLocalized = $msg->text(); } - $msg = wfMessage( 'grouppage-' . $groupname )->inContentLanguage(); + $msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage(); if ( $msg->isBlank() ) { $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname; } else { @@ -210,7 +210,7 @@ class SpecialStatistics extends SpecialPage { ); $grouplink = Linker::linkKnown( SpecialPage::getTitleFor( 'Listusers' ), - wfMsgHtml( 'listgrouprights-members' ), + $this->msg( 'listgrouprights-members' )->escaped(), array(), array( 'group' => $group ) ); @@ -229,12 +229,12 @@ class SpecialStatistics extends SpecialPage { private function getViewsStats() { return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-views', array( 'parseinline' ) ) ) . + Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-views' )->parse() ) . Xml::closeElement( 'tr' ) . - $this->formatRow( wfMsgExt( 'statistics-views-total', array( 'parseinline' ) ), + $this->formatRow( $this->msg( 'statistics-views-total' )->parse(), $this->getLanguage()->formatNum( $this->views ), array ( 'class' => 'mw-statistics-views-total' ), 'statistics-views-total-desc' ) . - $this->formatRow( wfMsgExt( 'statistics-views-peredit', array( 'parseinline' ) ), + $this->formatRow( $this->msg( 'statistics-views-peredit' )->parse(), $this->getLanguage()->formatNum( sprintf( '%.2f', $this->edits ? $this->views / $this->edits : 0 ) ), array ( 'class' => 'mw-statistics-views-peredit' ) ); @@ -262,7 +262,7 @@ class SpecialStatistics extends SpecialPage { ); if( $res->numRows() > 0 ) { $text .= Xml::openElement( 'tr' ); - $text .= Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-mostpopular', array( 'parseinline' ) ) ); + $text .= Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-mostpopular' )->parse() ); $text .= Xml::closeElement( 'tr' ); foreach ( $res as $row ) { $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); @@ -282,7 +282,7 @@ class SpecialStatistics extends SpecialPage { return ''; $return = Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-hooks', array( 'parseinline' ) ) ) . + Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-hooks' )->parse() ) . Xml::closeElement( 'tr' ); foreach( $stats as $name => $number ) { diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php index adfc7441..4036ebb2 100644 --- a/includes/specials/SpecialTags.php +++ b/includes/specials/SpecialTags.php @@ -21,9 +21,6 @@ * @ingroup SpecialPage */ -if (!defined('MEDIAWIKI')) - die; - /** * A special page that lists tags for edits * @@ -44,13 +41,13 @@ class SpecialTags extends SpecialPage { $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' ); // Write the headers - $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, wfMsgExt( 'tags-tag', 'parseinline' ) ) . - Xml::tags( 'th', null, wfMsgExt( 'tags-display-header', 'parseinline' ) ) . - Xml::tags( 'th', null, wfMsgExt( 'tags-description-header', 'parseinline' ) ) . - Xml::tags( 'th', null, wfMsgExt( 'tags-hitcount-header', 'parseinline' ) ) + $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) . + Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) . + Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) . + Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) ); $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'change_tag', array( 'ct_tag', 'count(*) AS hitcount' ), + $res = $dbr->select( 'change_tag', array( 'ct_tag', 'hitcount' => 'count(*)' ), array(), __METHOD__, array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ) ); foreach ( $res as $row ) { @@ -72,18 +69,22 @@ class SpecialTags extends SpecialPage { } $newRow = ''; - $newRow .= Xml::tags( 'td', null, Xml::element( 'tt', null, $tag ) ); + $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) ); $disp = ChangeTags::tagDescription( $tag ); - $disp .= ' (' . Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsgHtml( 'tags-edit' ) ) . ')'; + $disp .= ' '; + $editLink = Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), $this->msg( 'tags-edit' )->escaped() ); + $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped(); $newRow .= Xml::tags( 'td', null, $disp ); - $msg = wfMessage( "tag-$tag-description" ); + $msg = $this->msg( "tag-$tag-description" ); $desc = !$msg->exists() ? '' : $msg->parse(); - $desc .= ' (' . Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsgHtml( 'tags-edit' ) ) . ')'; + $desc .= ' '; + $editDescLink = Linker::link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), $this->msg( 'tags-edit' )->escaped() ); + $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped(); $newRow .= Xml::tags( 'td', null, $desc ); - $hitcount = wfMsgExt( 'tags-hitcount', array( 'parsemag' ), $this->getLanguage()->formatNum( $hitcount ) ); + $hitcount = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped(); $hitcount = Linker::link( SpecialPage::getTitleFor( 'Recentchanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) ); $newRow .= Xml::tags( 'td', null, $hitcount ); diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php index 47944309..fb2005b5 100644 --- a/includes/specials/SpecialUnblock.php +++ b/includes/specials/SpecialUnblock.php @@ -1,5 +1,7 @@ <?php /** + * Implements Special:Unblock + * * 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 @@ -49,23 +51,23 @@ class SpecialUnblock extends SpecialPage { $out->addModules( 'mediawiki.special' ); $form = new HTMLForm( $this->getFields(), $this->getContext() ); - $form->setWrapperLegend( wfMsg( 'unblockip' ) ); + $form->setWrapperLegendMsg( 'unblockip' ); $form->setSubmitCallback( array( __CLASS__, 'processUIUnblock' ) ); - $form->setSubmitText( wfMsg( 'ipusubmit' ) ); - $form->addPreText( wfMsgExt( 'unblockiptext', 'parse' ) ); + $form->setSubmitTextMsg( 'ipusubmit' ); + $form->addPreText( $this->msg( 'unblockiptext' )->parseAsBlock() ); if( $form->show() ){ switch( $this->type ){ case Block::TYPE_USER: case Block::TYPE_IP: - $out->addWikiMsg( 'unblocked', $this->target ); + $out->addWikiMsg( 'unblocked', wfEscapeWikiText( $this->target ) ); break; case Block::TYPE_RANGE: - $out->addWikiMsg( 'unblocked-range', $this->target ); + $out->addWikiMsg( 'unblocked-range', wfEscapeWikiText( $this->target ) ); break; case Block::TYPE_ID: case Block::TYPE_AUTO: - $out->addWikiMsg( 'unblocked-id', $this->target ); + $out->addWikiMsg( 'unblocked-id', wfEscapeWikiText( $this->target ) ); break; } } @@ -136,6 +138,7 @@ class SpecialUnblock extends SpecialPage { /** * Submit callback for an HTMLForm object + * @return Array( Array(message key, parameters) */ public static function processUIUnblock( array $data, HTMLForm $form ) { return self::processUnblock( $data, $form->getContext() ); diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php index 3efed747..5865bf62 100644 --- a/includes/specials/SpecialUncategorizedimages.php +++ b/includes/specials/SpecialUncategorizedimages.php @@ -49,9 +49,9 @@ class UncategorizedImagesPage extends ImageQueryPage { function getQueryInfo() { return array ( 'tables' => array( 'page', 'categorylinks' ), - 'fields' => array( 'page_namespace AS namespace', - 'page_title AS title', - 'page_title AS value' ), + 'fields' => array( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' ), 'conds' => array( 'cl_from IS NULL', 'page_namespace' => NS_FILE, 'page_is_redirect' => 0 ), diff --git a/includes/specials/SpecialUncategorizedpages.php b/includes/specials/SpecialUncategorizedpages.php index 08a69448..1226a6ca 100644 --- a/includes/specials/SpecialUncategorizedpages.php +++ b/includes/specials/SpecialUncategorizedpages.php @@ -46,9 +46,9 @@ class UncategorizedPagesPage extends PageQueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'page', 'categorylinks' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'page_title AS value' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' ), // default for page_namespace is all content namespaces (if requestedNamespace is false) // otherwise, page_namespace is requestedNamespace 'conds' => array ( 'cl_from IS NULL', diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index 5d8b17b7..d8e0b97c 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -92,13 +92,13 @@ class PageArchive { array( 'ar_namespace', 'ar_title', - 'COUNT(*) AS count' + 'count' => 'COUNT(*)' ), $condition, __METHOD__, array( - 'GROUP BY' => 'ar_namespace,ar_title', - 'ORDER BY' => 'ar_namespace,ar_title', + 'GROUP BY' => array( 'ar_namespace', 'ar_title' ), + 'ORDER BY' => array( 'ar_namespace', 'ar_title' ), 'LIMIT' => 100, ) ) @@ -120,7 +120,7 @@ class PageArchive { ), array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey() ), - 'PageArchive::listRevisions', + __METHOD__, array( 'ORDER BY' => 'ar_timestamp DESC' ) ); $ret = $dbr->resultObject( $res ); return $ret; @@ -195,7 +195,7 @@ class PageArchive { 'ar_timestamp' => $dbr->timestamp( $timestamp ) ), __METHOD__ ); if( $row ) { - return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleId() ) ); + return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleID() ) ); } else { return null; } @@ -308,7 +308,9 @@ class PageArchive { $dbr = wfGetDB( DB_SLAVE ); $n = $dbr->selectField( 'archive', 'COUNT(ar_title)', array( 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey() ) ); + 'ar_title' => $this->title->getDBkey() ), + __METHOD__ + ); return ( $n > 0 ); } @@ -321,11 +323,14 @@ class PageArchive { * @param $comment String * @param $fileVersions Array * @param $unsuppress Boolean + * @param $user User doing the action, or null to use $wgUser * * @return array(number of file revisions restored, number of image revisions restored, log message) * on success, false on failure */ - function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false ) { + function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false, User $user = null ) { + global $wgUser; + // If both the set of text revisions and file revisions are empty, // restore everything. Otherwise, just restore the requested items. $restoreAll = empty( $timestamps ) && empty( $fileVersions ); @@ -354,28 +359,35 @@ class PageArchive { } // Touch the log! - global $wgContLang; - $log = new LogPage( 'delete' ); if( $textRestored && $filesRestored ) { - $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ), - $wgContLang->formatNum( $textRestored ), - $wgContLang->formatNum( $filesRestored ) ); + $reason = wfMessage( 'undeletedrevisions-files' ) + ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text(); } elseif( $textRestored ) { - $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ), - $wgContLang->formatNum( $textRestored ) ); + $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored ) + ->inContentLanguage()->text(); } elseif( $filesRestored ) { - $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ), - $wgContLang->formatNum( $filesRestored ) ); + $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored ) + ->inContentLanguage()->text(); } else { wfDebug( "Undelete: nothing undeleted...\n" ); return false; } if( trim( $comment ) != '' ) { - $reason .= wfMsgForContent( 'colon-separator' ) . $comment; + $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment; } - $log->addEntry( 'restore', $this->title, $reason ); + + if ( $user === null ) { + $user = $wgUser; + } + + $logEntry = new ManualLogEntry( 'delete', 'restore' ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( $this->title ); + $logEntry->setComment( $reason ); + $logid = $logEntry->insert(); + $logEntry->publish( $logid ); return array( $textRestored, $filesRestored, $reason ); } @@ -745,14 +757,20 @@ class SpecialUndelete extends SpecialPage { $out->addHTML( "<ul>\n" ); foreach ( $result as $row ) { $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); - $link = Linker::linkKnown( - $undelete, - htmlspecialchars( $title->getPrefixedText() ), - array(), - array( 'target' => $title->getPrefixedText() ) - ); + if ( $title !== null ) { + $item = Linker::linkKnown( + $undelete, + htmlspecialchars( $title->getPrefixedText() ), + array(), + array( 'target' => $title->getPrefixedText() ) + ); + } else { + // The title is no longer valid, show as text + $item = Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $row->ar_namespace, $row->ar_title ) ); + } $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse(); - $out->addHTML( "<li>{$link} ({$revs})</li>\n" ); + $out->addHTML( "<li>{$item} ({$revs})</li>\n" ); } $result->free(); $out->addHTML( "</ul>\n" ); @@ -890,21 +908,22 @@ class SpecialUndelete extends SpecialPage { $diffEngine->showDiffStyle(); $this->getOutput()->addHTML( "<div>" . - "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" . + "<table width='98%' cellpadding='0' cellspacing='4' class='diff'>" . "<col class='diff-marker' />" . "<col class='diff-content' />" . "<col class='diff-marker' />" . "<col class='diff-content' />" . "<tr>" . - "<td colspan='2' width='50%' align='center' class='diff-otitle'>" . + "<td colspan='2' width='50%' style='text-align: center' class='diff-otitle'>" . $this->diffHeader( $previousRev, 'o' ) . "</td>\n" . - "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" . + "<td colspan='2' width='50%' style='text-align: center' class='diff-ntitle'>" . $this->diffHeader( $currentRev, 'n' ) . "</td>\n" . "</tr>" . $diffEngine->generateDiffBody( - $previousRev->getText(), $currentRev->getText() ) . + $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ), + $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) . "</table>" . "</div>\n" ); @@ -1009,7 +1028,7 @@ class SpecialUndelete extends SpecialPage { } $out->wrapWikiMsg( "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n", - array( 'undeletepagetitle', $this->mTargetObj->getPrefixedText() ) + array( 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ) ); $archive = new PageArchive( $this->mTargetObj ); @@ -1065,11 +1084,13 @@ class SpecialUndelete extends SpecialPage { } # Show relevant lines from the deletion log: - $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" ); + $deleteLogPage = new LogPage( 'delete' ); + $out->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) . "\n" ); LogEventsList::showLogExtract( $out, 'delete', $this->mTargetObj ); # Show relevant lines from the suppression log: + $suppressLogPage = new LogPage( 'suppress' ); if( $this->getUser()->isAllowed( 'suppressionlog' ) ) { - $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'suppress' ) ) . "\n" ); + $out->addHTML( Xml::element( 'h2', null, $suppressLogPage->getName()->text() ) . "\n" ); LogEventsList::showLogExtract( $out, 'suppress', $this->mTargetObj ); } @@ -1159,8 +1180,8 @@ class SpecialUndelete extends SpecialPage { private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) { $rev = Revision::newFromArchiveRow( $row, - array( 'page' => $this->mTargetObj->getArticleId() ) ); - $stxt = ''; + array( 'page' => $this->mTargetObj->getArticleID() ) ); + $revTextSize = ''; $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); // Build checkboxen... if( $this->mAllowed ) { @@ -1209,13 +1230,15 @@ class SpecialUndelete extends SpecialPage { // Revision text size $size = $row->ar_len; if( !is_null( $size ) ) { - $stxt = Linker::formatRevisionSize( $size ); + $revTextSize = Linker::formatRevisionSize( $size ); } // Edit summary $comment = Linker::revComment( $rev ); // Revision delete links $revdlink = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj ); - return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>"; + + $revisionRow = $this->msg( 'undelete-revisionrow' )->rawParams( $checkBox, $revdlink, $last, $pageLink , $userLink, $revTextSize, $comment )->escaped(); + return "<li>$revisionRow</li>"; } private function formatFileRow( $row ) { @@ -1232,9 +1255,9 @@ class SpecialUndelete extends SpecialPage { $pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user ); } $userLink = $this->getFileUser( $file ); - $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text() . - ' (' . $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() . ')'; - $data = htmlspecialchars( $data ); + $data = $this->msg( 'widthheight' )->numParams( $row->fa_width, $row->fa_height )->text(); + $bytes = $this->msg( 'parentheses' )->rawParams( $this->msg( 'nbytes' )->numParams( $row->fa_size )->text() )->plain(); + $data = htmlspecialchars( $data . ' ' . $bytes ); $comment = $this->getFileComment( $file ); // Add show/hide deletion links if available @@ -1263,7 +1286,7 @@ class SpecialUndelete extends SpecialPage { * * @param $rev Revision * @param $titleObj Title - * @param $ts Timestamp + * @param $ts string Timestamp * @return string */ function getPageLink( $rev, $titleObj, $ts ) { @@ -1294,7 +1317,7 @@ class SpecialUndelete extends SpecialPage { * * @param $file File * @param $titleObj Title - * @param $ts A timestamp + * @param $ts string A timestamp * @param $key String: a storage key * * @return String: HTML fragment @@ -1379,7 +1402,9 @@ class SpecialUndelete extends SpecialPage { $this->mTargetTimestamp, $this->mComment, $this->mFileVersions, - $this->mUnsuppress ); + $this->mUnsuppress, + $this->getUser() + ); if( is_array( $ok ) ) { if ( $ok[1] ) { // Undeleted file count @@ -1399,7 +1424,7 @@ class SpecialUndelete extends SpecialPage { // Show file deletion warnings and errors $status = $archive->getFileStatus(); if( $status && !$status->isGood() ) { - $out->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) ); + $out->addWikiText( '<div class="error">' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '</div>' ); } } } diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php index 48a93e8d..1bd38e17 100644 --- a/includes/specials/SpecialUnusedcategories.php +++ b/includes/specials/SpecialUnusedcategories.php @@ -39,9 +39,9 @@ class UnusedCategoriesPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'page', 'categorylinks' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'page_title AS value' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' ), 'conds' => array ( 'cl_from IS NULL', 'page_namespace' => NS_CATEGORY, 'page_is_redirect' => 0 ), @@ -52,6 +52,7 @@ class UnusedCategoriesPage extends QueryPage { /** * A should come before Z (bug 30907) + * @return bool */ function sortDescending() { return false; diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php index 6407de44..cdab557e 100644 --- a/includes/specials/SpecialUnusedimages.php +++ b/includes/specials/SpecialUnusedimages.php @@ -47,9 +47,9 @@ class UnusedimagesPage extends ImageQueryPage { global $wgCountCategorizedImagesAsUsed; $retval = array ( 'tables' => array ( 'image', 'imagelinks' ), - 'fields' => array ( "'" . NS_FILE . "' AS namespace", - 'img_name AS title', - 'img_timestamp AS value', + 'fields' => array ( 'namespace' => NS_FILE, + 'title' => 'img_name', + 'value' => 'img_timestamp', 'img_user', 'img_user_text', 'img_description' ), 'conds' => array ( 'il_to IS NULL' ), @@ -77,7 +77,7 @@ class UnusedimagesPage extends ImageQueryPage { } function getPageHeader() { - return wfMsgExt( 'unusedimagestext', array( 'parse' ) ); + return $this->msg( 'unusedimagestext' )->parseAsBlock(); } } diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php index e5c55b83..06077d1f 100644 --- a/includes/specials/SpecialUnusedtemplates.php +++ b/includes/specials/SpecialUnusedtemplates.php @@ -42,9 +42,9 @@ class UnusedtemplatesPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'page', 'templatelinks' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'page_title AS value' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' ), 'conds' => array ( 'page_namespace' => NS_TEMPLATE, 'tl_from IS NULL', 'page_is_redirect' => 0 ), @@ -68,17 +68,13 @@ class UnusedtemplatesPage extends QueryPage { array( 'redirect' => 'no' ) ); $wlhLink = Linker::linkKnown( - SpecialPage::getTitleFor( 'Whatlinkshere' ), - wfMsgHtml( 'unusedtemplateswlh' ), - array(), - array( 'target' => $title->getPrefixedText() ) + SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ), + $this->msg( 'unusedtemplateswlh' )->escaped() ); return $this->getLanguage()->specialList( $pageLink, $wlhLink ); } function getPageHeader() { - return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) ); + return $this->msg( 'unusedtemplatestext' )->parseAsBlock(); } - } - diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php index 22c64858..e5a79413 100644 --- a/includes/specials/SpecialUnwatchedpages.php +++ b/includes/specials/SpecialUnwatchedpages.php @@ -41,9 +41,9 @@ class UnwatchedpagesPage extends QueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'page', 'watchlist' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'page_namespace AS value' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_namespace' ), 'conds' => array ( 'wl_title IS NULL', 'page_is_redirect' => 0, "page_namespace != '" . NS_MEDIAWIKI . @@ -68,17 +68,19 @@ class UnwatchedpagesPage extends QueryPage { function formatResult( $skin, $result ) { global $wgContLang; - $nt = Title::makeTitle( $result->namespace, $result->title ); + $nt = Title::makeTitleSafe( $result->namespace, $result->title ); + if ( !$nt ) { + return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( $this->getContext(), $result->namespace, $result->title ) ); + } + $text = $wgContLang->convert( $nt->getPrefixedText() ); - $plink = Linker::linkKnown( - $nt, - htmlspecialchars( $text ) - ); + $plink = Linker::linkKnown( $nt, htmlspecialchars( $text ) ); $token = WatchAction::getWatchToken( $nt, $this->getUser() ); $wlink = Linker::linkKnown( $nt, - wfMsgHtml( 'watch' ), + $this->msg( 'watch' )->escaped(), array(), array( 'action' => 'watch', 'token' => $token ) ); diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index d6a76d02..43ea345b 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -150,7 +150,7 @@ class SpecialUpload extends SpecialPage { # Check blocks if( $user->isBlocked() ) { - throw new UserBlockedError( $user->mBlock ); + throw new UserBlockedError( $user->getBlock() ); } # Check whether we actually want to allow changing stuff @@ -235,7 +235,7 @@ class SpecialUpload extends SpecialPage { !$this->mTokenOk && !$this->mCancelUpload && ( $this->mUpload && $this->mUploadClicked ) ) { - $form->addPreText( wfMsgExt( 'session_fail_preview', 'parseinline' ) ); + $form->addPreText( $this->msg( 'session_fail_preview' )->parse() ); } # Give a notice if the user is uploading a file that has been deleted or moved @@ -255,16 +255,16 @@ class SpecialUpload extends SpecialPage { # Add text to form $form->addPreText( '<div id="uploadtext">' . - wfMsgExt( 'uploadtext', 'parse', array( $this->mDesiredDestName ) ) . + $this->msg( 'uploadtext', array( $this->mDesiredDestName ) )->parseAsBlock() . '</div>' ); # Add upload error message $form->addPreText( $message ); # Add footer to form - $uploadFooter = wfMessage( 'uploadfooter' ); + $uploadFooter = $this->msg( 'uploadfooter' ); if ( !$uploadFooter->isDisabled() ) { $form->addPostText( '<div id="mw-upload-footer-message">' - . $this->getOutput()->parse( $uploadFooter->plain() ) . "</div>\n" ); + . $uploadFooter->parseAsBlock() . "</div>\n" ); } return $form; @@ -280,14 +280,12 @@ class SpecialUpload extends SpecialPage { if( $title instanceof Title ) { $count = $title->isDeleted(); if ( $count > 0 && $user->isAllowed( 'deletedhistory' ) ) { - $link = wfMsgExt( - $user->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted', - array( 'parse', 'replaceafter' ), - Linker::linkKnown( - SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), - wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count ) - ) + $restorelink = Linker::linkKnown( + SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), + $this->msg( 'restorelink' )->numParams( $count )->escaped() ); + $link = $this->msg( $user->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted' ) + ->rawParams( $restorelink )->parseAsBlock(); $this->getOutput()->addHTML( "<div id=\"contentSub2\">{$link}</div>" ); } } @@ -306,11 +304,11 @@ class SpecialUpload extends SpecialPage { */ protected function showRecoverableUploadError( $message ) { $sessionKey = $this->mUpload->stashSession(); - $message = '<h2>' . wfMsgHtml( 'uploaderror' ) . "</h2>\n" . + $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . "</h2>\n" . '<div class="error">' . $message . "</div>\n"; $form = $this->getUploadForm( $message, $sessionKey ); - $form->setSubmitText( wfMsg( 'upload-tryagain' ) ); + $form->setSubmitText( $this->msg( 'upload-tryagain' )->escaped() ); $this->showUploadForm( $form ); } /** @@ -335,7 +333,7 @@ class SpecialUpload extends SpecialPage { $sessionKey = $this->mUpload->stashSession(); - $warningHtml = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" + $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" . '<ul class="warning">'; foreach( $warnings as $warning => $args ) { if( $warning == 'exists' ) { @@ -343,8 +341,8 @@ class SpecialUpload extends SpecialPage { } elseif( $warning == 'duplicate' ) { $msg = self::getDupeWarning( $args ); } elseif( $warning == 'duplicate-archive' ) { - $msg = "\t<li>" . wfMsgExt( 'file-deleted-duplicate', 'parseinline', - array( Title::makeTitle( NS_FILE, $args )->getPrefixedText() ) ) + $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate', + Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse() . "</li>\n"; } else { if ( $args === true ) { @@ -352,17 +350,17 @@ class SpecialUpload extends SpecialPage { } elseif ( !is_array( $args ) ) { $args = array( $args ); } - $msg = "\t<li>" . wfMsgExt( $warning, 'parseinline', $args ) . "</li>\n"; + $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n"; } $warningHtml .= $msg; } $warningHtml .= "</ul>\n"; - $warningHtml .= wfMsgExt( 'uploadwarning-text', 'parse' ); + $warningHtml .= $this->msg( 'uploadwarning-text' )->parseAsBlock(); $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true ); - $form->setSubmitText( wfMsg( 'upload-tryagain' ) ); - $form->addButton( 'wpUploadIgnoreWarning', wfMsg( 'ignorewarning' ) ); - $form->addButton( 'wpCancelUpload', wfMsg( 'reuploaddesc' ) ); + $form->setSubmitText( $this->msg( 'upload-tryagain' )->text() ); + $form->addButton( 'wpUploadIgnoreWarning', $this->msg( 'ignorewarning' )->text() ); + $form->addButton( 'wpCancelUpload', $this->msg( 'reuploaddesc' )->text() ); $this->showUploadForm( $form ); @@ -376,7 +374,7 @@ class SpecialUpload extends SpecialPage { * @param $message string HTML string */ protected function showUploadError( $message ) { - $message = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" . + $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" . '<div class="error">' . $message . "</div>\n"; $this->showUploadForm( $this->getUploadForm( $message ) ); } @@ -414,8 +412,7 @@ class SpecialUpload extends SpecialPage { $permErrors = $this->mUpload->verifyTitlePermissions( $this->getUser() ); if( $permErrors !== true ) { $code = array_shift( $permErrors[0] ); - $this->showRecoverableUploadError( wfMsgExt( $code, - 'parseinline', $permErrors[0] ) ); + $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() ); return; } @@ -469,7 +466,7 @@ class SpecialUpload extends SpecialPage { if ( in_array( $msgName, $wgForceUIMsgAsContentMsg ) ) { $msg[$msgName] = "{{int:$msgName}}"; } else { - $msg[$msgName] = wfMsgForContent( $msgName ); + $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text(); } } @@ -516,7 +513,7 @@ class SpecialUpload extends SpecialPage { if( $local && $local->exists() ) { // We're uploading a new version of an existing file. // No creation, so don't watch it if we're not already. - return $local->getTitle()->userIsWatching(); + return $this->getUser()->isWatched( $local->getTitle() ); } else { // New page should get watched if that's our option. return $this->getUser()->getOption( 'watchcreations' ); @@ -536,33 +533,31 @@ class SpecialUpload extends SpecialPage { /** Statuses that only require name changing **/ case UploadBase::MIN_LENGTH_PARTNAME: - $this->showRecoverableUploadError( wfMsgHtml( 'minlength1' ) ); + $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() ); break; case UploadBase::ILLEGAL_FILENAME: - $this->showRecoverableUploadError( wfMsgExt( 'illegalfilename', - 'parseinline', $details['filtered'] ) ); + $this->showRecoverableUploadError( $this->msg( 'illegalfilename', + $details['filtered'] )->parse() ); break; case UploadBase::FILENAME_TOO_LONG: - $this->showRecoverableUploadError( wfMsgHtml( 'filename-toolong' ) ); + $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() ); break; case UploadBase::FILETYPE_MISSING: - $this->showRecoverableUploadError( wfMsgExt( 'filetype-missing', - 'parseinline' ) ); + $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() ); break; case UploadBase::WINDOWS_NONASCII_FILENAME: - $this->showRecoverableUploadError( wfMsgExt( 'windows-nonascii-filename', - 'parseinline' ) ); + $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() ); break; /** Statuses that require reuploading **/ case UploadBase::EMPTY_FILE: - $this->showUploadError( wfMsgHtml( 'emptyfile' ) ); + $this->showUploadError( $this->msg( 'emptyfile' )->escaped() ); break; case UploadBase::FILE_TOO_LARGE: - $this->showUploadError( wfMsgHtml( 'largefileserver' ) ); + $this->showUploadError( $this->msg( 'largefileserver' )->escaped() ); break; case UploadBase::FILETYPE_BADTYPE: - $msg = wfMessage( 'filetype-banned-type' ); + $msg = $this->msg( 'filetype-banned-type' ); if ( isset( $details['blacklistedExt'] ) ) { $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) ); } else { @@ -585,7 +580,7 @@ class SpecialUpload extends SpecialPage { case UploadBase::VERIFICATION_ERROR: unset( $details['status'] ); $code = array_shift( $details['details'] ); - $this->showUploadError( wfMsgExt( $code, 'parseinline', $details['details'] ) ); + $this->showUploadError( $this->msg( $code, $details['details'] )->parse() ); break; case UploadBase::HOOK_ABORTED: if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array @@ -596,7 +591,7 @@ class SpecialUpload extends SpecialPage { $args = null; } - $this->showUploadError( wfMsgExt( $error, 'parseinline', $args ) ); + $this->showUploadError( $this->msg( $error, $args )->parse() ); break; default: throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" ); @@ -641,37 +636,37 @@ class SpecialUpload extends SpecialPage { if( $exists['warning'] == 'exists' ) { // Exact match - $warning = wfMsgExt( 'fileexists', 'parseinline', $filename ); + $warning = wfMessage( 'fileexists', $filename )->parse(); } elseif( $exists['warning'] == 'page-exists' ) { // Page exists but file does not - $warning = wfMsgExt( 'filepageexists', 'parseinline', $filename ); + $warning = wfMessage( 'filepageexists', $filename )->parse(); } elseif ( $exists['warning'] == 'exists-normalized' ) { - $warning = wfMsgExt( 'fileexists-extension', 'parseinline', $filename, - $exists['normalizedFile']->getTitle()->getPrefixedText() ); + $warning = wfMessage( 'fileexists-extension', $filename, + $exists['normalizedFile']->getTitle()->getPrefixedText() )->parse(); } elseif ( $exists['warning'] == 'thumb' ) { // Swapped argument order compared with other messages for backwards compatibility - $warning = wfMsgExt( 'fileexists-thumbnail-yes', 'parseinline', - $exists['thumbFile']->getTitle()->getPrefixedText(), $filename ); + $warning = wfMessage( 'fileexists-thumbnail-yes', + $exists['thumbFile']->getTitle()->getPrefixedText(), $filename )->parse(); } elseif ( $exists['warning'] == 'thumb-name' ) { // Image w/o '180px-' does not exists, but we do not like these filenames $name = $file->getName(); $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 ); - $warning = wfMsgExt( 'file-thumbnail-no', 'parseinline', $badPart ); + $warning = wfMessage( 'file-thumbnail-no', $badPart )->parse(); } elseif ( $exists['warning'] == 'bad-prefix' ) { - $warning = wfMsgExt( 'filename-bad-prefix', 'parseinline', $exists['prefix'] ); + $warning = wfMessage( 'filename-bad-prefix', $exists['prefix'] )->parse(); } elseif ( $exists['warning'] == 'was-deleted' ) { # If the file existed before and was deleted, warn the user of this $ltitle = SpecialPage::getTitleFor( 'Log' ); $llink = Linker::linkKnown( $ltitle, - wfMsgHtml( 'deletionlog' ), + wfMessage( 'deletionlog' )->escaped(), array(), array( 'type' => 'delete', 'page' => $filename ) ); - $warning = wfMsgExt( 'filewasdeleted', array( 'parse', 'replaceafter' ), $llink ); + $warning = wfMessage( 'filewasdeleted' )->rawParams( $llink )->parseAsBlock(); } return $warning; @@ -707,22 +702,18 @@ class SpecialUpload extends SpecialPage { * @return string */ public static function getDupeWarning( $dupes ) { - global $wgOut; - if( $dupes ) { - $msg = '<gallery>'; - foreach( $dupes as $file ) { - $title = $file->getTitle(); - $msg .= $title->getPrefixedText() . - '|' . $title->getText() . "\n"; - } - $msg .= '</gallery>'; - return '<li>' . - wfMsgExt( 'file-exists-duplicate', array( 'parse' ), count( $dupes ) ) . - $wgOut->parse( $msg ) . - "</li>\n"; - } else { + if ( !$dupes ) { return ''; } + + $gallery = new ImageGallery; + $gallery->setShowBytes( false ); + foreach( $dupes as $file ) { + $gallery->add( $file->getTitle() ); + } + return '<li>' . + wfMessage( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() . + $gallery->toHtml() . "</li>\n"; } } @@ -775,7 +766,7 @@ class UploadForm extends HTMLForm { parent::__construct( $descriptor, $context, 'upload' ); # Set some form properties - $this->setSubmitText( wfMsg( 'uploadbtn' ) ); + $this->setSubmitText( $this->msg( 'uploadbtn' )->text() ); $this->setSubmitName( 'wpUpload' ); # Used message keys: 'accesskey-upload', 'tooltip-upload' $this->setSubmitTooltip( 'upload' ); @@ -830,7 +821,9 @@ class UploadForm extends HTMLForm { # that setting doesn't exist if ( !wfIsHipHop() ) { $this->mMaxUploadSize['file'] = min( $this->mMaxUploadSize['file'], - wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ) ); + wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ), + wfShorthandToInteger( ini_get( 'post_max_size' ) ) + ); } $descriptor['UploadFile'] = array( @@ -841,10 +834,9 @@ class UploadForm extends HTMLForm { 'label-message' => 'sourcefilename', 'upload-type' => 'File', 'radio' => &$radio, - 'help' => wfMsgExt( 'upload-maxfilesize', - array( 'parseinline', 'escapenoentities' ), + 'help' => $this->msg( 'upload-maxfilesize', $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] ) - ) . ' ' . wfMsgHtml( 'upload_source_file' ), + )->parse() . ' ' . $this->msg( 'upload_source_file' )->escaped(), 'checked' => $selectedSourceType == 'file', ); if ( $canUploadByUrl ) { @@ -856,10 +848,9 @@ class UploadForm extends HTMLForm { 'label-message' => 'sourceurl', 'upload-type' => 'url', 'radio' => &$radio, - 'help' => wfMsgExt( 'upload-maxfilesize', - array( 'parseinline', 'escapenoentities' ), + 'help' => $this->msg( 'upload-maxfilesize', $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] ) - ) . ' ' . wfMsgHtml( 'upload_source_url' ), + )->parse() . ' ' . $this->msg( 'upload_source_url' )->escaped(), 'checked' => $selectedSourceType == 'url', ); } @@ -890,16 +881,16 @@ class UploadForm extends HTMLForm { # Everything not permitted is banned $extensionsList = '<div id="mw-upload-permitted">' . - wfMsgExt( 'upload-permitted', 'parse', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) ) . + $this->msg( 'upload-permitted', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) )->parseAsBlock() . "</div>\n"; } else { # We have to list both preferred and prohibited $extensionsList = '<div id="mw-upload-preferred">' . - wfMsgExt( 'upload-preferred', 'parse', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) ) . + $this->msg( 'upload-preferred', $this->getContext()->getLanguage()->commaList( $wgFileExtensions ) )->parseAsBlock() . "</div>\n" . '<div id="mw-upload-prohibited">' . - wfMsgExt( 'upload-prohibited', 'parse', $this->getContext()->getLanguage()->commaList( $wgFileBlacklist ) ) . + $this->msg( 'upload-prohibited', $this->getContext()->getLanguage()->commaList( $wgFileBlacklist ) )->parseAsBlock() . "</div>\n"; } } else { diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php index 121b6a44..1a00d731 100644 --- a/includes/specials/SpecialUploadStash.php +++ b/includes/specials/SpecialUploadStash.php @@ -1,7 +1,28 @@ <?php /** - * Implements Special:UploadStash + * Implements Special:UploadStash. * + * 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 SpecialPage + * @ingroup Upload + */ + +/** * Web access for files temporarily stored by UploadStash. * * For example -- files that were uploaded with the UploadWizard extension are stored temporarily @@ -10,12 +31,7 @@ * * Since this is based on the user's session, in effect this creates a private temporary file area. * However, the URLs for the files cannot be shared. - * - * @file - * @ingroup SpecialPage - * @ingroup Upload */ - class SpecialUploadStash extends UnlistedSpecialPage { // UploadStash private $stash; @@ -58,6 +74,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward. * * @param $key String: the key of a particular requested file + * @return bool */ public function showUpload( $key ) { // prevent callers from doing standard HTML output -- we'll take it from here @@ -241,6 +258,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { * Side effect: writes HTTP response to STDOUT. * * @param $file File object with a local path (e.g. UnregisteredLocalFile, LocalFile. Oddly these don't share an ancestor!) + * @return bool */ private function outputLocalFile( File $file ) { if ( $file->getSize() > self::MAX_SERVE_BYTES ) { @@ -255,8 +273,9 @@ class SpecialUploadStash extends UnlistedSpecialPage { /** * Output HTTP response of raw content * Side effect: writes HTTP response to STDOUT. - * @param String $content: content - * @param String $mimeType: mime type + * @param $content String content + * @param $contentType String mime type + * @return bool */ private function outputContents( $content, $contentType ) { $size = strlen( $content ); @@ -303,7 +322,8 @@ class SpecialUploadStash extends UnlistedSpecialPage { /** * Default action when we don't have a subpage -- just show links to the uploads we have, * Also show a button to clear stashed files - * @param Status : $status - the result of processRequest + * @param $status [optional] Status: the result of processRequest + * @return bool */ private function showUploads( $status = null ) { if ( $status === null ) { @@ -326,7 +346,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { ), $this->getContext(), 'clearStashedUploads' ); $form->setSubmitCallback( array( __CLASS__ , 'tryClearStashedUploads' ) ); $form->setTitle( $this->getTitle() ); - $form->setSubmitText( wfMsg( 'uploadstash-clear' ) ); + $form->setSubmitTextMsg( 'uploadstash-clear' ); $form->prepareForm(); $formResult = $form->tryAuthorizedSubmit(); @@ -334,7 +354,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { // show the files + form, if there are any, or just say there are none $refreshHtml = Html::element( 'a', array( 'href' => $this->getTitle()->getLocalURL() ), - wfMsg( 'uploadstash-refresh' ) ); + $this->msg( 'uploadstash-refresh' )->text() ); $files = $this->stash->listFiles(); if ( $files && count( $files ) ) { sort( $files ); @@ -351,7 +371,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { $this->getOutput()->addHtml( Html::rawElement( 'p', array(), $refreshHtml ) ); } else { $this->getOutput()->addHtml( Html::rawElement( 'p', array(), - Html::element( 'span', array(), wfMsg( 'uploadstash-nofiles' ) ) + Html::element( 'span', array(), $this->msg( 'uploadstash-nofiles' )->text() ) . ' ' . $refreshHtml ) ); diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 4c5a2376..58da77da 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -74,7 +74,7 @@ class LoginForm extends SpecialPage { * Loader */ function load() { - global $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin; + global $wgAuth, $wgHiddenPrefs, $wgEnableEmail; if ( $this->mLoaded ) { return; @@ -93,8 +93,6 @@ class LoginForm extends SpecialPage { $this->mRetype = $request->getText( 'wpRetype' ); $this->mDomain = $request->getText( 'wpDomain' ); $this->mReason = $request->getText( 'wpReason' ); - $this->mReturnTo = $request->getVal( 'returnto' ); - $this->mReturnToQuery = $request->getVal( 'returntoquery' ); $this->mCookieCheck = $request->getVal( 'wpCookieCheck' ); $this->mPosted = $request->wasPosted(); $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' ); @@ -107,11 +105,8 @@ class LoginForm extends SpecialPage { $this->mLanguage = $request->getText( 'uselang' ); $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' ); $this->mToken = ( $this->mType == 'signup' ) ? $request->getVal( 'wpCreateaccountToken' ) : $request->getVal( 'wpLoginToken' ); - - if ( $wgRedirectOnLogin ) { - $this->mReturnTo = $wgRedirectOnLogin; - $this->mReturnToQuery = ''; - } + $this->mReturnTo = $request->getVal( 'returnto', '' ); + $this->mReturnToQuery = $request->getVal( 'returntoquery', '' ); if( $wgEnableEmail ) { $this->mEmail = $request->getText( 'wpEmail' ); @@ -125,17 +120,17 @@ class LoginForm extends SpecialPage { } if( !$wgAuth->validDomain( $this->mDomain ) ) { - if ( isset( $_SESSION['wsDomain'] ) ) { - $this->mDomain = $_SESSION['wsDomain']; - } else { - $this->mDomain = 'invaliddomain'; - } + $this->mDomain = $wgAuth->getDomain(); } $wgAuth->setDomain( $this->mDomain ); - # When switching accounts, it sucks to get automatically logged out + # 1. When switching accounts, it sucks to get automatically logged out + # 2. Do not return to PasswordReset after a successful password change + # but goto Wiki start page (Main_Page) instead ( bug 33997 ) $returnToTitle = Title::newFromText( $this->mReturnTo ); - if( is_object( $returnToTitle ) && $returnToTitle->isSpecial( 'Userlogout' ) ) { + if( is_object( $returnToTitle ) && ( + $returnToTitle->isSpecial( 'Userlogout' ) + || $returnToTitle->isSpecial( 'PasswordReset' ) ) ) { $this->mReturnTo = ''; $this->mReturnToQuery = ''; } @@ -163,11 +158,14 @@ class LoginForm extends SpecialPage { return; } elseif( $this->mPosted ) { if( $this->mCreateaccount ) { - return $this->addNewAccount(); + $this->addNewAccount(); + return; } elseif ( $this->mCreateaccountMail ) { - return $this->addNewAccountMailPassword(); + $this->addNewAccountMailPassword(); + return; } elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) { - return $this->processLogin(); + $this->processLogin(); + return; } } $this->mainLoginForm( '' ); @@ -203,12 +201,13 @@ class LoginForm extends SpecialPage { $this->mainLoginForm( $this->msg( 'mailerror', $result->getWikiText() )->text() ); } else { $out->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() ); - $out->returnToMain( false ); + $this->executeReturnTo( 'success' ); } } /** * @private + * @return bool */ function addNewAccount() { global $wgUser, $wgEmailAuthentication, $wgLoginLanguageSelector; @@ -216,7 +215,7 @@ class LoginForm extends SpecialPage { # Create the account and abort if there's a problem doing so $u = $this->addNewAccountInternal(); if( $u == null ) { - return; + return false; } # If we showed up language selection links, and one was in use, be @@ -253,23 +252,24 @@ class LoginForm extends SpecialPage { wfRunHooks( 'AddNewAccount', array( $u, false ) ); $u->addNewUserLogEntry(); if( $this->hasSessionCookie() ) { - return $this->successfulCreation(); + $this->successfulCreation(); } else { - return $this->cookieRedirectCheck( 'new' ); + $this->cookieRedirectCheck( 'new' ); } } else { # Confirm that the account was created $out->setPageTitle( $this->msg( 'accountcreated' ) ); $out->addWikiMsg( 'accountcreatedtext', $u->getName() ); - $out->returnToMain( false, $this->getTitle() ); + $out->addReturnTo( $this->getTitle() ); wfRunHooks( 'AddNewAccount', array( $u, false ) ); $u->addNewUserLogEntry( false, $this->mReason ); - return true; } + return true; } /** * @private + * @return bool|User */ function addNewAccountInternal() { global $wgAuth, $wgMemc, $wgAccountCreationThrottle, @@ -334,7 +334,7 @@ class LoginForm extends SpecialPage { $ip = $this->getRequest()->getIP(); if ( $currentUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) { - $this->mainLoginForm( $this->msg( 'sorbs_create_account_reason' )->text() . ' (' . htmlspecialchars( $ip ) . ')' ); + $this->mainLoginForm( $this->msg( 'sorbs_create_account_reason' )->text() . ' ' . $this->msg( 'parentheses', $ip )->escaped() ); return false; } @@ -476,6 +476,7 @@ class LoginForm extends SpecialPage { * This may create a local account as a side effect if the * authentication plugin allows transparent local account * creation. + * @return int */ public function authenticateUserData() { global $wgUser, $wgAuth; @@ -747,9 +748,9 @@ class LoginForm extends SpecialPage { $this->getContext()->setLanguage( $userLang ); // Reset SessionID on Successful login (bug 40995) $this->renewSessionId(); - return $this->successfulLogin(); + $this->successfulLogin(); } else { - return $this->cookieRedirectCheck( 'login' ); + $this->cookieRedirectCheck( 'login' ); } break; @@ -769,7 +770,7 @@ class LoginForm extends SpecialPage { case self::NOT_EXISTS: if( $this->getUser()->isAllowed( 'createaccount' ) ) { $this->mainLoginForm( $this->msg( 'nosuchuser', - wfEscapeWikiText( $this->mUsername ) )->parse() ); + wfEscapeWikiText( $this->mUsername ) )->parse() ); } else { $this->mainLoginForm( $this->msg( 'nosuchusershort', wfEscapeWikiText( $this->mUsername ) )->text() ); @@ -861,16 +862,7 @@ class LoginForm extends SpecialPage { if( $injected_html !== '' ) { $this->displaySuccessfulLogin( 'loginsuccess', $injected_html ); } else { - $titleObj = Title::newFromText( $this->mReturnTo ); - if ( !$titleObj instanceof Title ) { - $titleObj = Title::newMainPage(); - } - $redirectUrl = $titleObj->getFullURL( $this->mReturnToQuery ); - global $wgSecureLogin; - if( $wgSecureLogin && !$this->mStickHTTPS ) { - $redirectUrl = preg_replace( '/^https:/', 'http:', $redirectUrl ); - } - $this->getOutput()->redirect( $redirectUrl ); + $this->executeReturnTo( 'successredirect' ); } } @@ -900,6 +892,8 @@ class LoginForm extends SpecialPage { /** * Display a "login successful" page. + * @param $msgname string + * @param $injected_html string */ private function displaySuccessfulLogin( $msgname, $injected_html ) { $out = $this->getOutput(); @@ -910,11 +904,7 @@ class LoginForm extends SpecialPage { $out->addHTML( $injected_html ); - if ( !empty( $this->mReturnTo ) ) { - $out->returnToMain( null, $this->mReturnTo, $this->mReturnToQuery ); - } else { - $out->returnToMain( null ); - } + $this->executeReturnTo( 'success' ); } /** @@ -948,7 +938,42 @@ class LoginForm extends SpecialPage { $block->getByName() ); - $out->returnToMain( false ); + $this->executeReturnTo( 'error' ); + } + + /** + * Add a "return to" link or redirect to it. + * + * @param $type string, one of the following: + * - error: display a return to link ignoring $wgRedirectOnLogin + * - success: display a return to link using $wgRedirectOnLogin if needed + * - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed + */ + private function executeReturnTo( $type ) { + global $wgRedirectOnLogin, $wgSecureLogin; + + if ( $type != 'error' && $wgRedirectOnLogin !== null ) { + $returnTo = $wgRedirectOnLogin; + $returnToQuery = array(); + } else { + $returnTo = $this->mReturnTo; + $returnToQuery = wfCgiToArray( $this->mReturnToQuery ); + } + + $returnToTitle = Title::newFromText( $returnTo ); + if ( !$returnToTitle ) { + $returnToTitle = Title::newMainPage(); + } + + if ( $type == 'successredirect' ) { + $redirectUrl = $returnToTitle->getFullURL( $returnToQuery ); + if( $wgSecureLogin && !$this->mStickHTTPS ) { + $redirectUrl = preg_replace( '/^https:/', 'http:', $redirectUrl ); + } + $this->getOutput()->redirect( $redirectUrl ); + } else { + $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery ); + } } /** @@ -998,9 +1023,9 @@ class LoginForm extends SpecialPage { $linkmsg = 'nologin'; } - if ( !empty( $this->mReturnTo ) ) { + if ( $this->mReturnTo !== '' ) { $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo ); - if ( !empty( $this->mReturnToQuery ) ) { + if ( $this->mReturnToQuery !== '' ) { $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery ); } @@ -1125,6 +1150,7 @@ class LoginForm extends SpecialPage { * previous pass through the system. * * @private + * @return bool */ function hasSessionCookie() { global $wgDisableCookieCheck; @@ -1133,6 +1159,7 @@ class LoginForm extends SpecialPage { /** * Get the login token from the current session + * @return Mixed */ public static function getLoginToken() { global $wgRequest; @@ -1159,6 +1186,7 @@ class LoginForm extends SpecialPage { /** * Get the createaccount token from the current session + * @return Mixed */ public static function getCreateaccountToken() { global $wgRequest; @@ -1181,7 +1209,7 @@ class LoginForm extends SpecialPage { $wgRequest->setSessionData( 'wsCreateaccountToken', null ); } - /** + /** * Renew the user's session id, using strong entropy */ private function renewSessionId() { @@ -1204,12 +1232,13 @@ class LoginForm extends SpecialPage { function cookieRedirectCheck( $type ) { $titleObj = SpecialPage::getTitleFor( 'Userlogin' ); $query = array( 'wpCookieCheck' => $type ); - if ( $this->mReturnTo ) { + if ( $this->mReturnTo !== '' ) { $query['returnto'] = $this->mReturnTo; + $query['returntoquery'] = $this->mReturnToQuery; } $check = $titleObj->getFullURL( $query ); - return $this->getOutput()->redirect( $check ); + $this->getOutput()->redirect( $check ); } /** @@ -1218,15 +1247,15 @@ class LoginForm extends SpecialPage { function onCookieRedirectCheck( $type ) { if ( !$this->hasSessionCookie() ) { if ( $type == 'new' ) { - return $this->mainLoginForm( $this->msg( 'nocookiesnew' )->parse() ); + $this->mainLoginForm( $this->msg( 'nocookiesnew' )->parse() ); } elseif ( $type == 'login' ) { - return $this->mainLoginForm( $this->msg( 'nocookieslogin' )->parse() ); + $this->mainLoginForm( $this->msg( 'nocookieslogin' )->parse() ); } else { # shouldn't happen - return $this->mainLoginForm( $this->msg( 'error' )->text() ); + $this->mainLoginForm( $this->msg( 'error' )->text() ); } } else { - return $this->successfulLogin(); + $this->successfulLogin(); } } @@ -1268,20 +1297,31 @@ class LoginForm extends SpecialPage { * * @param $text Link text * @param $lang Language code + * @return string */ function makeLanguageSelectorLink( $text, $lang ) { - $attr = array( 'uselang' => $lang ); + if( $this->getLanguage()->getCode() == $lang ) { + // no link for currently used language + return htmlspecialchars( $text ); + } + $query = array( 'uselang' => $lang ); if( $this->mType == 'signup' ) { - $attr['type'] = 'signup'; + $query['type'] = 'signup'; } - if( $this->mReturnTo ) { - $attr['returnto'] = $this->mReturnTo; + if( $this->mReturnTo !== '' ) { + $query['returnto'] = $this->mReturnTo; + $query['returntoquery'] = $this->mReturnToQuery; } + + $attr = array(); + $targetLanguage = Language::factory( $lang ); + $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode(); + return Linker::linkKnown( $this->getTitle(), htmlspecialchars( $text ), - array(), - $attr + $attr, + $query ); } } diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php index d747448f..ab2bf0ac 100644 --- a/includes/specials/SpecialUserlogout.php +++ b/includes/specials/SpecialUserlogout.php @@ -39,7 +39,7 @@ class SpecialUserlogout extends UnlistedSpecialPage { */ if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&' ) !== false ) { wfDebug( "Special:Userlogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" ); - throw new HttpError( 400, wfMessage( 'suspicious-userlogout' ), wfMessage( 'loginerror' ) ); + throw new HttpError( 400, $this->msg( 'suspicious-userlogout' ), $this->msg( 'loginerror' ) ); } $this->setHeaders(); diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index e2e0f38b..9f5a48a5 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -47,6 +47,9 @@ class UserrightsPage extends SpecialPage { public function userCanChangeRights( $user, $checkIfSelf = true ) { $available = $this->changeableGroups(); + if ( $user->getId() == 0 ) { + return false; + } return !empty( $available['add'] ) || !empty( $available['remove'] ) || ( ( $this->isself || !$checkIfSelf ) && @@ -72,7 +75,7 @@ class UserrightsPage extends SpecialPage { * allow them to use Special:UserRights. */ if( $user->isBlocked() && !$user->isAllowed( 'userrights' ) ) { - throw new UserBlockedError( $user->mBlock ); + throw new UserBlockedError( $user->getBlock() ); } $request = $this->getRequest(); @@ -345,7 +348,7 @@ class UserrightsPage extends SpecialPage { function makeGroupNameList( $ids ) { if( empty( $ids ) ) { - return wfMsgForContent( 'rightsnone' ); + return $this->msg( 'rightsnone' )->inContentLanguage()->text(); } else { return implode( ', ', $ids ); } @@ -367,9 +370,9 @@ class UserrightsPage extends SpecialPage { $this->getOutput()->addHTML( Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) . Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::fieldset( wfMsg( 'userrights-lookup-user' ) ) . - Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ) ) . ' ' . - Xml::submitButton( wfMsg( 'editusergroup' ) ) . + Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) . + Xml::inputLabel( $this->msg( 'userrights-user-editname' )->text(), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ) ) . ' ' . + Xml::submitButton( $this->msg( 'editusergroup' )->text() ) . Html::closeElement( 'fieldset' ) . Html::closeElement( 'form' ) . "\n" ); @@ -420,12 +423,12 @@ class UserrightsPage extends SpecialPage { $grouplist = ''; $count = count( $list ); if( $count > 0 ) { - $grouplist = wfMessage( 'userrights-groupsmember', $count)->parse(); + $grouplist = $this->msg( 'userrights-groupsmember', $count, $user->getName() )->parse(); $grouplist = '<p>' . $grouplist . ' ' . $this->getLanguage()->listToText( $list ) . "</p>\n"; } $count = count( $autolist ); if( $count > 0 ) { - $autogrouplistintro = wfMessage( 'userrights-groupsmember-auto', $count)->parse(); + $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto', $count, $user->getName() )->parse(); $grouplist .= '<p>' . $autogrouplistintro . ' ' . $this->getLanguage()->listToText( $autolist ) . "</p>\n"; } @@ -441,15 +444,15 @@ class UserrightsPage extends SpecialPage { Html::hidden( 'user', $this->mTarget ) . Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) . Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', array(), wfMessage( 'userrights-editusergroup', $user->getName() )->text() ) . - wfMessage( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )->rawParams( $userToolLinks )->parse() . - wfMessage( 'userrights-groups-help', $user->getName() )->parse() . + Xml::element( 'legend', array(), $this->msg( 'userrights-editusergroup', $user->getName() )->text() ) . + $this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )->rawParams( $userToolLinks )->parse() . + $this->msg( 'userrights-groups-help', $user->getName() )->parse() . $grouplist . Xml::tags( 'p', null, $this->groupCheckboxes( $groups, $user ) ) . - Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-userrights-table-outer' ) ) . + Xml::openElement( 'table', array( 'id' => 'mw-userrights-table-outer' ) ) . "<tr> <td class='mw-label'>" . - Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) . + Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) . "</td> <td class='mw-input'>" . Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ), @@ -459,7 +462,7 @@ class UserrightsPage extends SpecialPage { <tr> <td></td> <td class='mw-submit'>" . - Xml::submitButton( wfMsg( 'saveusergroups' ), + Xml::submitButton( $this->msg( 'saveusergroups' )->text(), array( 'name' => 'saveusergroups' ) + Linker::tooltipAndAccesskeyAttribs( 'userrights-set' ) ) . "</td> </tr>" . @@ -531,12 +534,12 @@ class UserrightsPage extends SpecialPage { } # Build the HTML table - $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) . + $ret .= Xml::openElement( 'table', array( 'class' => 'mw-userrights-groups' ) ) . "<tr>\n"; foreach( $columns as $name => $column ) { if( $column === array() ) continue; - $ret .= Xml::element( 'th', null, wfMessage( 'userrights-' . $name . '-col', count( $column ) )->text() ); + $ret .= Xml::element( 'th', null, $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text() ); } $ret.= "</tr>\n<tr>\n"; foreach( $columns as $column ) { @@ -548,7 +551,7 @@ class UserrightsPage extends SpecialPage { $member = User::getGroupMember( $group, $user->getName() ); if ( $checkbox['irreversible'] ) { - $text = wfMessage( 'userrights-irreversible-marker', $member )->escaped(); + $text = $this->msg( 'userrights-irreversible-marker', $member )->escaped(); } else { $text = htmlspecialchars( $member ); } @@ -602,7 +605,8 @@ class UserrightsPage extends SpecialPage { * @param $output OutputPage to use */ protected function showLogFragment( $user, $output ) { - $output->addHTML( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) ); + $rightsLogPage = new LogPage( 'rights' ); + $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) ); LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() ); } } diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index 8185fe88..4e5b6bf5 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -37,7 +37,7 @@ class SpecialVersion extends SpecialPage { protected static $viewvcUrls = array( 'svn+ssh://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki', 'http://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki', - 'https://svn.wikimedia.org/viewvc/mediawiki' => 'https://svn.wikimedia.org/viewvc/mediawiki', + 'https://svn.wikimedia.org/svnroot/mediawiki' => 'https://svn.wikimedia.org/viewvc/mediawiki', ); public function __construct(){ @@ -58,6 +58,7 @@ class SpecialVersion extends SpecialPage { $text = $this->getMediaWikiCredits() . $this->softwareInformation() . + $this->getEntryPointInfo() . $this->getExtensionCredits(); if ( $wgSpecialVersionShowHooks ) { $text .= $this->getWgHooks(); @@ -79,13 +80,13 @@ class SpecialVersion extends SpecialPage { * @return string */ private static function getMediaWikiCredits() { - $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ); + $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMessage( 'version-license' )->text() ); // This text is always left-to-right. - $ret .= '<div>'; + $ret .= '<div class="plainlinks">'; $ret .= "__NOTOC__ " . self::getCopyrightAndAuthorList() . "\n - " . wfMsg( 'version-license-info' ); + " . wfMessage( 'version-license-info' )->text(); $ret .= '</div>'; return str_replace( "\t\t", '', $ret ) . "\n"; @@ -107,11 +108,14 @@ class SpecialVersion extends SpecialPage { 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe', 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed', 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso', - wfMsg( 'version-poweredby-others' ) + 'Timo Tijhof', + '[{{SERVER}}{{SCRIPTPATH}}/CREDITS ' . + wfMessage( 'version-poweredby-others' )->text() . + ']' ); - return wfMsg( 'version-poweredby-credits', date( 'Y' ), - $wgLang->listToText( $authorList ) ); + return wfMessage( 'version-poweredby-credits', date( 'Y' ), + $wgLang->listToText( $authorList ) )->text(); } /** @@ -123,8 +127,8 @@ class SpecialVersion extends SpecialPage { $dbr = wfGetDB( DB_SLAVE ); // Put the software in an array of form 'name' => 'version'. All messages should - // be loaded here, so feel free to use wfMsg*() in the 'name'. Raw HTML or wikimarkup - // can be used. + // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or + // wikimarkup can be used. $software = array(); $software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked(); $software['[http://www.php.net/ PHP]'] = phpversion() . " (" . php_sapi_name() . ")"; @@ -133,11 +137,11 @@ class SpecialVersion extends SpecialPage { // Allow a hook to add/remove items. wfRunHooks( 'SoftwareInfo', array( &$software ) ); - $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) . - Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-software' ) ) . + $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMessage( 'version-software' )->text() ) . + Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ) ) . "<tr> - <th>" . wfMsg( 'version-software-product' ) . "</th> - <th>" . wfMsg( 'version-software-version' ) . "</th> + <th>" . wfMessage( 'version-software-product' )->text() . "</th> + <th>" . wfMessage( 'version-software-version' )->text() . "</th> </tr>\n"; foreach( $software as $name => $version ) { @@ -160,18 +164,26 @@ class SpecialVersion extends SpecialPage { global $wgVersion, $IP; wfProfileIn( __METHOD__ ); - $info = self::getSvnInfo( $IP ); - if ( !$info ) { + $gitInfo = self::getGitHeadSha1( $IP ); + $svnInfo = self::getSvnInfo( $IP ); + if ( !$svnInfo && !$gitInfo ) { $version = $wgVersion; - } elseif( $flags === 'nodb' ) { - $version = "$wgVersion (r{$info['checkout-rev']})"; + } elseif ( $gitInfo && $flags === 'nodb' ) { + $shortSha1 = substr( $gitInfo, 0, 7 ); + $version = "$wgVersion ($shortSha1)"; + } elseif ( $gitInfo ) { + $shortSha1 = substr( $gitInfo, 0, 7 ); + $shortSha1 = wfMessage( 'parentheses' )->params( $shortSha1 )->escaped(); + $version = "$wgVersion $shortSha1"; + } elseif ( $flags === 'nodb' ) { + $version = "$wgVersion (r{$svnInfo['checkout-rev']})"; } else { $version = $wgVersion . ' ' . - wfMsg( + wfMessage( 'version-svn-revision', isset( $info['directory-rev'] ) ? $info['directory-rev'] : '', $info['checkout-rev'] - ); + )->text(); } wfProfileOut( __METHOD__ ); @@ -180,37 +192,79 @@ class SpecialVersion extends SpecialPage { /** * Return a wikitext-formatted string of the MediaWiki version with a link to - * the SVN revision if available. + * the SVN revision or the git SHA1 of head if available. + * Git is prefered over Svn + * The fallback is just $wgVersion * * @return mixed */ public static function getVersionLinked() { - global $wgVersion, $IP; + global $wgVersion; wfProfileIn( __METHOD__ ); + $gitVersion = self::getVersionLinkedGit(); + if( $gitVersion ) { + $v = $gitVersion; + } else { + $svnVersion = self::getVersionLinkedSvn(); + if( $svnVersion ) { + $v = $svnVersion; + } else { + $v = $wgVersion; // fallback + } + } + + wfProfileOut( __METHOD__ ); + return $v; + } + + /** + * @return string wgVersion + a link to subversion revision of svn BASE + */ + private static function getVersionLinkedSvn() { + global $wgVersion, $IP; + $info = self::getSvnInfo( $IP ); + if( !isset( $info['checkout-rev'] ) ) { + return false; + } - if ( isset( $info['checkout-rev'] ) ) { - $linkText = wfMsg( - 'version-svn-revision', - isset( $info['directory-rev'] ) ? $info['directory-rev'] : '', - $info['checkout-rev'] - ); + $linkText = wfMessage( + 'version-svn-revision', + isset( $info['directory-rev'] ) ? $info['directory-rev'] : '', + $info['checkout-rev'] + )->text(); - if ( isset( $info['viewvc-url'] ) ) { - $version = "$wgVersion [{$info['viewvc-url']} $linkText]"; - } else { - $version = "$wgVersion $linkText"; - } + if ( isset( $info['viewvc-url'] ) ) { + $version = "$wgVersion [{$info['viewvc-url']} $linkText]"; } else { - $version = $wgVersion; + $version = "$wgVersion $linkText"; } - wfProfileOut( __METHOD__ ); return $version; } /** + * @return bool|string wgVersion + HEAD sha1 stripped to the first 7 chars. False on failure + */ + private static function getVersionLinkedGit() { + global $wgVersion, $IP; + + $gitInfo = new GitInfo( $IP ); + $headSHA1 = $gitInfo->getHeadSHA1(); + if( !$headSHA1 ) { + return false; + } + + $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')'; + $viewerUrl = $gitInfo->getHeadViewUrl(); + if ( $viewerUrl !== false ) { + $shortSHA1 = "[$viewerUrl $shortSHA1]"; + } + return "$wgVersion $shortSHA1"; + } + + /** * Returns an array with the base extension types. * Type is stored as array key, the message as array value. * @@ -225,14 +279,14 @@ class SpecialVersion extends SpecialPage { public static function getExtensionTypes() { if ( self::$extensionTypes === false ) { self::$extensionTypes = array( - 'specialpage' => wfMsg( 'version-specialpages' ), - 'parserhook' => wfMsg( 'version-parserhooks' ), - 'variable' => wfMsg( 'version-variables' ), - 'media' => wfMsg( 'version-mediahandlers' ), - 'antispam' => wfMsg( 'version-antispam' ), - 'skin' => wfMsg( 'version-skins' ), - 'api' => wfMsg( 'version-api' ), - 'other' => wfMsg( 'version-other' ), + 'specialpage' => wfMessage( 'version-specialpages' )->text(), + 'parserhook' => wfMessage( 'version-parserhooks' )->text(), + 'variable' => wfMessage( 'version-variables' )->text(), + 'media' => wfMessage( 'version-mediahandlers' )->text(), + 'antispam' => wfMessage( 'version-antispam' )->text(), + 'skin' => wfMessage( 'version-skins' )->text(), + 'api' => wfMessage( 'version-api' )->text(), + 'other' => wfMessage( 'version-other' )->text(), ); wfRunHooks( 'ExtensionTypes', array( &self::$extensionTypes ) ); @@ -274,8 +328,8 @@ class SpecialVersion extends SpecialPage { */ wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) ); - $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) . - Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-ext' ) ); + $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), $this->msg( 'version-extensions' )->text() ) . + Xml::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ) ); // Make sure the 'other' type is set to an array. if ( !array_key_exists( 'other', $wgExtensionCredits ) ) { @@ -300,7 +354,7 @@ class SpecialVersion extends SpecialPage { $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'] ); if ( count( $wgExtensionFunctions ) ) { - $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' ); + $out .= $this->openExtType( $this->msg( 'version-extension-functions' )->text(), 'extension-functions' ); $out .= '<tr><td colspan="4">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n"; } @@ -311,13 +365,13 @@ class SpecialVersion extends SpecialPage { for ( $i = 0; $i < $cnt; ++$i ) { $tags[$i] = "<{$tags[$i]}>"; } - $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' ); + $out .= $this->openExtType( $this->msg( 'version-parser-extensiontags' )->text(), 'parser-tags' ); $out .= '<tr><td colspan="4">' . $this->listToText( $tags ). "</td></tr>\n"; } $fhooks = $wgParser->getFunctionHooks(); if( count( $fhooks ) ) { - $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' ); + $out .= $this->openExtType( $this->msg( 'version-parser-function-hooks' )->text(), 'parser-function-hooks' ); $out .= '<tr><td colspan="4">' . $this->listToText( $fhooks ) . "</td></tr>\n"; } @@ -356,6 +410,9 @@ class SpecialVersion extends SpecialPage { /** * Callback to sort extensions by type. + * @param $a array + * @param $b array + * @return int */ function compare( $a, $b ) { if( $a['name'] === $b['name'] ) { @@ -377,15 +434,26 @@ class SpecialVersion extends SpecialPage { function getCreditsForExtension( array $extension ) { $name = isset( $extension['name'] ) ? $extension['name'] : '[no name]'; + $vcsText = false; + if ( isset( $extension['path'] ) ) { - $svnInfo = self::getSvnInfo( dirname($extension['path']) ); - $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null; - $checkoutRev = isset( $svnInfo['checkout-rev'] ) ? $svnInfo['checkout-rev'] : null; - $viewvcUrl = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : null; - } else { - $directoryRev = null; - $checkoutRev = null; - $viewvcUrl = null; + $gitInfo = new GitInfo( dirname( $extension['path'] ) ); + $gitHeadSHA1 = $gitInfo->getHeadSHA1(); + if ( $gitHeadSHA1 !== false ) { + $vcsText = '(' . substr( $gitHeadSHA1, 0, 7 ) . ')'; + $gitViewerUrl = $gitInfo->getHeadViewUrl(); + if ( $gitViewerUrl !== false ) { + $vcsText = "[$gitViewerUrl $vcsText]"; + } + } else { + $svnInfo = self::getSvnInfo( dirname( $extension['path'] ) ); + # Make subversion text/link. + if ( $svnInfo !== false ) { + $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null; + $vcsText = $this->msg( 'version-svn-revision', $directoryRev, $svnInfo['checkout-rev'] )->text(); + $vcsText = isset( $svnInfo['viewvc-url'] ) ? '[' . $svnInfo['viewvc-url'] . " $vcsText]" : $vcsText; + } + } } # Make main link (or just the name if there is no URL). @@ -397,20 +465,12 @@ class SpecialVersion extends SpecialPage { if ( isset( $extension['version'] ) ) { $versionText = '<span class="mw-version-ext-version">' . - wfMsg( 'version-version', $extension['version'] ) . + $this->msg( 'version-version', $extension['version'] )->text() . '</span>'; } else { $versionText = ''; } - # Make subversion text/link. - if ( $checkoutRev ) { - $svnText = wfMsg( 'version-svn-revision', $directoryRev, $checkoutRev ); - $svnText = isset( $viewvcUrl ) ? "[$viewvcUrl $svnText]" : $svnText; - } else { - $svnText = false; - } - # Make description text. $description = isset ( $extension['description'] ) ? $extension['description'] : ''; @@ -422,16 +482,16 @@ class SpecialVersion extends SpecialPage { $descriptionMsgKey = $descriptionMsg[0]; // Get the message key array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only array_map( "htmlspecialchars", $descriptionMsg ); // For sanity - $description = wfMsg( $descriptionMsgKey, $descriptionMsg ); + $description = $this->msg( $descriptionMsgKey, $descriptionMsg )->text(); } else { - $description = wfMsg( $descriptionMsg ); + $description = $this->msg( $descriptionMsg )->text(); } } - if ( $svnText !== false ) { + if ( $vcsText !== false ) { $extNameVer = "<tr> <td><em>$mainLink $versionText</em></td> - <td><em>$svnText</em></td>"; + <td><em>$vcsText</em></td>"; } else { $extNameVer = "<tr> <td colspan=\"2\"><em>$mainLink $versionText</em></td>"; @@ -457,11 +517,11 @@ class SpecialVersion extends SpecialPage { $myWgHooks = $wgHooks; ksort( $myWgHooks ); - $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) . + $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), $this->msg( 'version-hooks' )->text() ) . Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) ) . "<tr> - <th>" . wfMsg( 'version-hook-name' ) . "</th> - <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th> + <th>" . $this->msg( 'version-hook-name' )->text() . "</th> + <th>" . $this->msg( 'version-hook-subscribedby' )->text() . "</th> </tr>\n"; foreach ( $myWgHooks as $hook => $hooks ) { @@ -517,7 +577,7 @@ class SpecialVersion extends SpecialPage { $list = array(); foreach( (array)$authors as $item ) { if( $item == '...' ) { - $list[] = wfMsg( 'version-poweredby-others' ); + $list[] = $this->msg( 'version-poweredby-others' )->text(); } else { $list[] = $item; } @@ -562,8 +622,8 @@ class SpecialVersion extends SpecialPage { $list = $list[0]; } if( is_object( $list ) ) { - $class = get_class( $list ); - return "($class)"; + $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped(); + return $class; } elseif ( !is_array( $list ) ) { return $list; } else { @@ -572,7 +632,7 @@ class SpecialVersion extends SpecialPage { } else { $class = $list[0]; } - return "($class, {$list[1]})"; + return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped(); } } @@ -589,6 +649,8 @@ class SpecialVersion extends SpecialPage { * url The subversion URL of the directory * repo-url The base URL of the repository * viewvc-url A ViewVC URL pointing to the checked-out revision + * @param $dir string + * @return array|bool */ public static function getSvnInfo( $dir ) { // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html @@ -676,6 +738,50 @@ class SpecialVersion extends SpecialPage { } } + /** + * @param $dir String: directory of the git checkout + * @return bool|String sha1 of commit HEAD points to + */ + public static function getGitHeadSha1( $dir ) { + $repo = new GitInfo( $dir ); + return $repo->getHeadSHA1(); + } + + /** + * Get the list of entry points and their URLs + * @return string Wikitext + */ + public function getEntryPointInfo() { + global $wgArticlePath, $wgScriptPath; + $entryPoints = array( + 'version-entrypoints-articlepath' => $wgArticlePath, + 'version-entrypoints-scriptpath' => $wgScriptPath, + 'version-entrypoints-index-php' => wfScript( 'index' ), + 'version-entrypoints-api-php' => wfScript( 'api' ), + 'version-entrypoints-load-php' => wfScript( 'load' ), + ); + + $out = Html::element( 'h2', array( 'id' => 'mw-version-entrypoints' ), $this->msg( 'version-entrypoints' )->text() ) . + Html::openElement( 'table', array( 'class' => 'wikitable plainlinks', 'id' => 'mw-version-entrypoints-table' ) ) . + Html::openElement( 'tr' ) . + Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-entrypoint' )->text() ) . + Html::element( 'th', array(), $this->msg( 'version-entrypoints-header-url' )->text() ) . + Html::closeElement( 'tr' ); + + foreach ( $entryPoints as $message => $value ) { + $url = wfExpandUrl( $value, PROTO_RELATIVE ); + $out .= Html::openElement( 'tr' ) . + // ->text() looks like it should be ->parse(), but this function + // returns wikitext, not HTML, boo + Html::rawElement( 'td', array(), $this->msg( $message )->text() ) . + Html::rawElement( 'td', array(), Html::rawElement( 'code', array(), "[$url $value]" ) ) . + Html::closeElement( 'tr' ); + } + + $out .= Html::closeElement( 'table' ); + return $out; + } + function showEasterEgg() { $rx = $rp = $xe = ''; $alpha = array("", "kbQW", "\$\n()"); diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php index f497e4e2..0b1fb251 100644 --- a/includes/specials/SpecialWantedcategories.php +++ b/includes/specials/SpecialWantedcategories.php @@ -37,9 +37,9 @@ class WantedCategoriesPage extends WantedQueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'categorylinks', 'page' ), - 'fields' => array ( "'" . NS_CATEGORY . "' AS namespace", - 'cl_to AS title', - 'COUNT(*) AS value' ), + 'fields' => array ( 'namespace' => NS_CATEGORY, + 'title' => 'cl_to', + 'value' => 'COUNT(*)' ), 'conds' => array ( 'page_title IS NULL' ), 'options' => array ( 'GROUP BY' => 'cl_to' ), 'join_conds' => array ( 'page' => array ( 'LEFT JOIN', diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php index ec0912df..f52f7bb9 100644 --- a/includes/specials/SpecialWantedfiles.php +++ b/includes/specials/SpecialWantedfiles.php @@ -39,7 +39,7 @@ class WantedFilesPage extends WantedQueryPage { # Specifically setting to use "Wanted Files" (NS_MAIN) as title, so as to get what # category would be used on main namespace pages, for those tricky wikipedia # admins who like to do {{#ifeq:{{NAMESPACE}}|foo|bar|....}}. - $catMessage = wfMessage( 'broken-file-category' ) + $catMessage = $this->msg( 'broken-file-category' ) ->title( Title::newFromText( "Wanted Files", NS_MAIN ) ) ->inContentLanguage(); @@ -66,6 +66,7 @@ class WantedFilesPage extends WantedQueryPage { * that exist e.g. in a shared repo. Setting this at least * keeps them from showing up as redlinks in the output, even * if it doesn't fix the real problem (bug 6220). + * @return bool */ function forceExistenceCheck() { return true; @@ -74,9 +75,9 @@ class WantedFilesPage extends WantedQueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'imagelinks', 'image' ), - 'fields' => array ( "'" . NS_FILE . "' AS namespace", - 'il_to AS title', - 'COUNT(*) AS value' ), + 'fields' => array ( 'namespace' => NS_FILE, + 'title' => 'il_to', + 'value' => 'COUNT(*)' ), 'conds' => array ( 'img_name IS NULL' ), 'options' => array ( 'GROUP BY' => 'il_to' ), 'join_conds' => array ( 'image' => diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php index 4624b355..7673305d 100644 --- a/includes/specials/SpecialWantedpages.php +++ b/includes/specials/SpecialWantedpages.php @@ -60,9 +60,9 @@ class WantedPagesPage extends WantedQueryPage { 'pg2' => 'page' ), 'fields' => array( - 'pl_namespace AS namespace', - 'pl_title AS title', - 'COUNT(*) AS value' + 'namespace' => 'pl_namespace', + 'title' => 'pl_title', + 'value' => 'COUNT(*)' ), 'conds' => array( 'pg1.page_namespace IS NULL', @@ -72,7 +72,7 @@ class WantedPagesPage extends WantedQueryPage { ), 'options' => array( 'HAVING' => "COUNT(*) > $count", - 'GROUP BY' => 'pl_namespace, pl_title' + 'GROUP BY' => array( 'pl_namespace', 'pl_title' ) ), 'join_conds' => array( 'pg1' => array( diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php index ab9d6046..f3e33698 100644 --- a/includes/specials/SpecialWantedtemplates.php +++ b/includes/specials/SpecialWantedtemplates.php @@ -40,13 +40,13 @@ class WantedTemplatesPage extends WantedQueryPage { function getQueryInfo() { return array ( 'tables' => array ( 'templatelinks', 'page' ), - 'fields' => array ( 'tl_namespace AS namespace', - 'tl_title AS title', - 'COUNT(*) AS value' ), + 'fields' => array ( 'namespace' => 'tl_namespace', + 'title' => 'tl_title', + 'value' => 'COUNT(*)' ), 'conds' => array ( 'page_title IS NULL', 'tl_namespace' => NS_TEMPLATE ), 'options' => array ( - 'GROUP BY' => 'tl_namespace, tl_title' ), + 'GROUP BY' => array( 'tl_namespace', 'tl_title' ) ), 'join_conds' => array ( 'page' => array ( 'LEFT JOIN', array ( 'page_namespace = tl_namespace', 'page_title = tl_title' ) ) ) diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index fef54911..5dfc1133 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -91,14 +91,6 @@ class SpecialWatchlist extends SpecialPage { return; } - if( ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) && $request->getVal( 'reset' ) && - $request->wasPosted() ) - { - $user->clearAllNotifications(); - $output->redirect( $this->getTitle()->getFullUrl() ); - return; - } - $nitems = $this->countItems(); if ( $nitems == 0 ) { $output->addWikiMsg( 'nowatchlist' ); @@ -116,6 +108,7 @@ class SpecialWatchlist extends SpecialPage { /* bool */ 'hideOwn' => (int)$user->getBoolOption( 'watchlisthideown' ), /* ? */ 'namespace' => 'all', /* ? */ 'invert' => false, + /* bool */ 'associated' => false, ); $this->customFilters = array(); wfRunHooks( 'SpecialWatchlistFilters', array( $this, &$this->customFilters ) ); @@ -148,13 +141,20 @@ class SpecialWatchlist extends SpecialPage { # Get namespace value, if supplied, and prepare a WHERE fragment $nameSpace = $request->getIntOrNull( 'namespace' ); - $invert = $request->getIntOrNull( 'invert' ); + $invert = $request->getBool( 'invert' ); + $associated = $request->getBool( 'associated' ); if ( !is_null( $nameSpace ) ) { + $eq_op = $invert ? '!=' : '='; + $bool_op = $invert ? 'AND' : 'OR'; $nameSpace = intval( $nameSpace ); // paranioa - if ( $invert ) { - $nameSpaceClause = "rc_namespace != $nameSpace"; + if ( !$associated ) { + $nameSpaceClause = "rc_namespace $eq_op $nameSpace"; } else { - $nameSpaceClause = "rc_namespace = $nameSpace"; + $associatedNS = MWNamespace::getAssociated( $nameSpace ); + $nameSpaceClause = + "rc_namespace $eq_op $nameSpace " . + $bool_op . + " rc_namespace $eq_op $associatedNS"; } } else { $nameSpace = ''; @@ -162,6 +162,7 @@ class SpecialWatchlist extends SpecialPage { } $values['namespace'] = $nameSpace; $values['invert'] = $invert; + $values['associated'] = $associated; if( is_null( $values['days'] ) || !is_numeric( $values['days'] ) ) { $big = 1000; /* The magical big */ @@ -181,6 +182,14 @@ class SpecialWatchlist extends SpecialPage { wfAppendToArrayIfNotDefault( $name, $values[$name], $defaults, $nondefaults ); } + if( ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) && $request->getVal( 'reset' ) && + $request->wasPosted() ) + { + $user->clearAllNotifications(); + $output->redirect( $this->getTitle()->getFullUrl( $nondefaults ) ); + return; + } + $dbr = wfGetDB( DB_SLAVE, 'watchlist' ); # Possible where conditions @@ -254,15 +263,25 @@ class SpecialWatchlist extends SpecialPage { 'id' => 'mw-watchlist-resetbutton' ) ) . $this->msg( 'wlheader-showupdated' )->parse() . ' ' . Xml::submitButton( $this->msg( 'enotif_reset' )->text(), array( 'name' => 'dummy' ) ) . - Html::hidden( 'reset', 'all' ) . - Xml::closeElement( 'form' ); + Html::hidden( 'reset', 'all' ); + foreach ( $nondefaults as $key => $value ) { + $form .= Html::hidden( $key, $value ); + } + $form .= Xml::closeElement( 'form' ); } $form .= '<hr />'; $tables = array( 'recentchanges', 'watchlist' ); $fields = array( $dbr->tableName( 'recentchanges' ) . '.*' ); $join_conds = array( - 'watchlist' => array('INNER JOIN',"wl_user='{$user->getId()}' AND wl_namespace=rc_namespace AND wl_title=rc_title"), + 'watchlist' => array( + 'INNER JOIN', + array( + 'wl_user' => $user->getId(), + 'wl_namespace=rc_namespace', + 'wl_title=rc_title' + ), + ), ); $options = array( 'ORDER BY' => 'rc_timestamp DESC' ); if( $wgShowUpdatedMarker ) { @@ -285,7 +304,7 @@ class SpecialWatchlist extends SpecialPage { wfRunHooks('SpecialWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) ); $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds ); - $numRows = $dbr->numRows( $res ); + $numRows = $res->numRows(); /* Start bottom header */ @@ -327,9 +346,31 @@ class SpecialWatchlist extends SpecialPage { $form .= $lang->pipeList( $links ); $form .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl(), 'id' => 'mw-watchlist-form-namespaceselector' ) ); $form .= '<hr /><p>'; - $form .= Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . ' '; - $form .= Xml::namespaceSelector( $nameSpace, '' ) . ' '; - $form .= Xml::checkLabel( $this->msg( 'invert' )->text(), 'invert', 'nsinvert', $invert ) . ' '; + $form .= Html::namespaceSelector( + array( + 'selected' => $nameSpace, + 'all' => '', + 'label' => $this->msg( 'namespace' )->text() + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ) . ' '; + $form .= Xml::checkLabel( + $this->msg( 'invert' )->text(), + 'invert', + 'nsinvert', + $invert, + array( 'title' => $this->msg( 'tooltip-invert' )->text() ) + ) . ' '; + $form .= Xml::checkLabel( + $this->msg( 'namespace_association' )->text(), + 'associated', + 'associated', + $associated, + array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() ) + ) . ' '; $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . '</p>'; $form .= Html::hidden( 'days', $values['days'] ); foreach ( $filters as $key => $msg ) { @@ -459,7 +500,7 @@ class SpecialWatchlist extends SpecialPage { $dbr = wfGetDB( DB_SLAVE, 'watchlist' ); # Fetch the raw count - $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', + $res = $dbr->select( 'watchlist', array( 'count' => 'COUNT(*)' ), array( 'wl_user' => $this->getUser()->getId() ), __METHOD__ ); $row = $dbr->fetchObject( $res ); $count = $row->count; diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php index d5129bf6..f1356493 100644 --- a/includes/specials/SpecialWhatlinkshere.php +++ b/includes/specials/SpecialWhatlinkshere.php @@ -163,7 +163,7 @@ class SpecialWhatLinksHere extends SpecialPage { 'rd_from = page_id', 'rd_namespace' => $target->getNamespace(), 'rd_title' => $target->getDBkey(), - '(rd_interwiki is NULL) or (rd_interwiki = \'\')' + 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL' ))); if( $fetchlinks ) { @@ -288,7 +288,7 @@ class SpecialWhatLinksHere extends SpecialPage { 'whatlinkshere-links', 'isimage' ); $msgcache = array(); foreach ( $msgs as $msg ) { - $msgcache[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); + $msgcache[$msg] = $this->msg( $msg )->escaped(); } } @@ -316,12 +316,12 @@ class SpecialWhatLinksHere extends SpecialPage { $props[] = $msgcache['isimage']; if ( count( $props ) ) { - $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')'; + $propsText = $this->msg( 'parentheses' )->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped(); } # Space for utilities links, with a what-links-here link provided $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] ); - $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' ); + $wlh = Xml::wrapClass( $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(), 'mw-whatlinkshere-tools' ); return $notClose ? Xml::openElement( 'li' ) . "$link $propsText $dirmark $wlh\n" : @@ -356,8 +356,8 @@ class SpecialWhatLinksHere extends SpecialPage { function getPrevNext( $prevId, $nextId ) { $currentLimit = $this->opts->getValue( 'limit' ); - $prev = wfMessage( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped(); - $next = wfMessage( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped(); + $prev = $this->msg( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped(); + $next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped(); $changed = $this->opts->getChangedValues(); unset($changed['target']); // Already in the request title @@ -381,7 +381,7 @@ class SpecialWhatLinksHere extends SpecialPage { $nums = $lang->pipeList( $limitLinks ); - return wfMsgHtml( 'viewprevnext', $prev, $next, $nums ); + return $this->msg( 'viewprevnext' )->rawParams( $prev, $next, $nums )->escaped(); } function whatlinkshereForm() { @@ -404,22 +404,31 @@ class SpecialWhatLinksHere extends SpecialPage { $f .= Html::hidden( $name, $value ); } - $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) ); + $f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() ); # Target input - $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target', + $f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target', 'mw-whatlinkshere-target', 40, $target ); $f .= ' '; # Namespace selector - $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . - Xml::namespaceSelector( $namespace, '' ); + $f .= Html::namespaceSelector( + array( + 'selected' => $namespace, + 'all' => '', + 'label' => $this->msg( 'namespace' )->text() + ), array( + 'name' => 'namespace', + 'id' => 'namespace', + 'class' => 'namespaceselector', + ) + ); $f .= ' '; # Submit - $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ); + $f .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ); # Close $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n"; @@ -433,8 +442,8 @@ class SpecialWhatLinksHere extends SpecialPage { * @return string HTML fieldset and filter panel with the show/hide links */ function getFilterPanel() { - $show = wfMsgHtml( 'show' ); - $hide = wfMsgHtml( 'hide' ); + $show = $this->msg( 'show' )->escaped(); + $hide = $this->msg( 'hide' )->escaped(); $changed = $this->opts->getChangedValues(); unset($changed['target']); // Already in the request title @@ -445,13 +454,14 @@ class SpecialWhatLinksHere extends SpecialPage { $types[] = 'hideimages'; // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans', 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages' - // To be sure they will be find by grep + // To be sure they will be found by grep foreach( $types as $type ) { $chosen = $this->opts->getValue( $type ); $msg = $chosen ? $show : $hide; $overrides = array( $type => !$chosen ); - $links[] = wfMsgHtml( "whatlinkshere-{$type}", $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) ); + $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams( + $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped(); } - return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), $this->getLanguage()->pipeList( $links ) ); + return Xml::fieldset( $this->msg( 'whatlinkshere-filters' )->text(), $this->getLanguage()->pipeList( $links ) ); } } diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php index 89dae203..2988b04f 100644 --- a/includes/specials/SpecialWithoutinterwiki.php +++ b/includes/specials/SpecialWithoutinterwiki.php @@ -41,10 +41,10 @@ class WithoutInterwikiPage extends PageQueryPage { } function getPageHeader() { - global $wgScript, $wgMiserMode; + global $wgScript; - # Do not show useless input form if wiki is running in misermode - if( $wgMiserMode ) { + # Do not show useless input form if special page is cached + if( $this->isCached() ) { return ''; } @@ -53,10 +53,10 @@ class WithoutInterwikiPage extends PageQueryPage { return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'withoutinterwiki-legend' ) ) . + Xml::element( 'legend', null, $this->msg( 'withoutinterwiki-legend' )->text() ) . Html::hidden( 'title', $t->getPrefixedText() ) . - Xml::inputLabel( wfMsg( 'allpagesprefix' ), 'prefix', 'wiprefix', 20, $prefix ) . ' ' . - Xml::submitButton( wfMsg( 'withoutinterwiki-submit' ) ) . + Xml::inputLabel( $this->msg( 'allpagesprefix' )->text(), 'prefix', 'wiprefix', 20, $prefix ) . ' ' . + Xml::submitButton( $this->msg( 'withoutinterwiki-submit' )->text() ) . Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); } @@ -80,9 +80,9 @@ class WithoutInterwikiPage extends PageQueryPage { function getQueryInfo() { $query = array ( 'tables' => array ( 'page', 'langlinks' ), - 'fields' => array ( 'page_namespace AS namespace', - 'page_title AS title', - 'page_title AS value' ), + 'fields' => array ( 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' ), 'conds' => array ( 'll_title IS NULL', 'page_namespace' => MWNamespace::getContentNamespaces(), 'page_is_redirect' => 0 ), diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php index 59284af0..bf5c487a 100644 --- a/includes/templates/NoLocalSettings.php +++ b/includes/templates/NoLocalSettings.php @@ -1,6 +1,21 @@ <?php /** - * Template used when there is no LocalSettings.php file + * Template used when there is no LocalSettings.php file. + * + * 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 Templates diff --git a/includes/templates/Usercreate.php b/includes/templates/Usercreate.php index c93b02cc..98727f17 100644 --- a/includes/templates/Usercreate.php +++ b/includes/templates/Usercreate.php @@ -1,6 +1,21 @@ <?php /** - * Html form for account creation + * Html form for account creation. + * + * 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 Templates @@ -154,9 +169,10 @@ class UsercreateTemplate extends QuickTemplate { <td></td> <td class="mw-input"> <?php - global $wgCookieExpiration, $wgLang; + global $wgCookieExpiration; + $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) ); echo Xml::checkLabel( - wfMsgExt( 'remembermypassword', 'parsemag', $wgLang->formatNum( ceil( $wgCookieExpiration / ( 3600 * 24 ) ) ) ), + wfMessage( 'remembermypassword' )->numParams( $expirationDays )->text(), 'wpRemember', 'wpRemember', $this->data['remember'], diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index efe826f4..a3f6a38b 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -1,6 +1,21 @@ <?php /** - * Html form for user login + * Html form for user login. + * + * 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 Templates @@ -93,9 +108,10 @@ class UserloginTemplate extends QuickTemplate { <td></td> <td class="mw-input"> <?php - global $wgCookieExpiration, $wgLang; + global $wgCookieExpiration; + $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) ); echo Xml::checkLabel( - wfMsgExt( 'remembermypassword', 'parsemag', $wgLang->formatNum( ceil( $wgCookieExpiration / ( 3600 * 24 ) ) ) ), + wfMessage( 'remembermypassword' )->numParams( $expirationDays )->text(), 'wpRemember', 'wpRemember', $this->data['remember'], @@ -111,7 +127,7 @@ class UserloginTemplate extends QuickTemplate { <td class="mw-input"> <?php echo Xml::checkLabel( - wfMsg( 'securelogin-stick-https' ), + wfMessage( 'securelogin-stick-https' )->text(), 'wpStickHTTPS', 'wpStickHTTPS', $this->data['stickHTTPS'], @@ -125,7 +141,7 @@ class UserloginTemplate extends QuickTemplate { <td></td> <td class="mw-submit"> <?php - echo Html::input( 'wpLoginAttempt', wfMsg( 'login' ), 'submit', array( + echo Html::input( 'wpLoginAttempt', wfMessage( 'login' )->text(), 'submit', array( 'id' => 'wpLoginAttempt', 'tabindex' => '9' ) ); @@ -138,10 +154,14 @@ class UserloginTemplate extends QuickTemplate { ); } elseif( $this->data['resetlink'] === null ) { echo ' '; - echo Html::input( 'wpMailmypassword', wfMsg( 'mailmypassword' ), 'submit', array( - 'id' => 'wpMailmypassword', - 'tabindex' => '10' - ) ); + echo Html::input( + 'wpMailmypassword', + wfMessage( 'mailmypassword' )->text(), + 'submit', array( + 'id' => 'wpMailmypassword', + 'tabindex' => '10' + ) + ); } } ?> diff --git a/includes/tidy.conf b/includes/tidy.conf index 09412f05..aa333fcb 100644 --- a/includes/tidy.conf +++ b/includes/tidy.conf @@ -16,3 +16,4 @@ quiet: yes quote-nbsp: yes fix-backslash: no fix-uri: no +new-inline-tags: video,audio,source,track,bdi diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index 6cc2287a..d40b53d3 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -1,6 +1,28 @@ <?php /** - * @defgroup Upload + * Base class for the backend of file upload. + * + * 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 Upload + */ + +/** + * @defgroup Upload Upload related */ /** @@ -41,6 +63,10 @@ abstract class UploadBase { const WINDOWS_NONASCII_FILENAME = 13; const FILENAME_TOO_LONG = 14; + /** + * @param $error int + * @return string + */ public function getVerificationErrorCode( $error ) { $code_to_status = array(self::EMPTY_FILE => 'empty-file', self::FILE_TOO_LARGE => 'file-too-large', @@ -64,6 +90,7 @@ abstract class UploadBase { /** * Returns true if uploads are enabled. * Can be override by subclasses. + * @return bool */ public static function isEnabled() { global $wgEnableUploads; @@ -82,6 +109,7 @@ abstract class UploadBase { * Can be overriden by subclasses. * * @param $user User + * @return bool */ public static function isAllowed( $user ) { foreach ( array( 'upload', 'edit' ) as $permission ) { @@ -100,6 +128,7 @@ abstract class UploadBase { * * @param $request WebRequest * @param $type + * @return null */ public static function createFromRequest( &$request, $type = null ) { $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' ); @@ -140,6 +169,8 @@ abstract class UploadBase { /** * Check whether a request if valid for this handler + * @param $request + * @return bool */ public static function isValidRequest( $request ) { return false; @@ -161,7 +192,7 @@ abstract class UploadBase { * @param $tempPath string the temporary path * @param $fileSize int the file size * @param $removeTempFile bool (false) remove the temporary file? - * @return null + * @throws MWException */ public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) { $this->mDesiredDestName = $name; @@ -180,6 +211,7 @@ abstract class UploadBase { /** * Fetch the file. Usually a no-op + * @return Status */ public function fetchFile() { return Status::newGood(); @@ -203,17 +235,20 @@ abstract class UploadBase { /** * @param $srcPath String: the source path - * @return the real path if it was a virtual URL + * @return string the real path if it was a virtual URL */ function getRealPath( $srcPath ) { + wfProfileIn( __METHOD__ ); $repo = RepoGroup::singleton()->getLocalRepo(); if ( $repo->isVirtualUrl( $srcPath ) ) { // @TODO: just make uploads work with storage paths // UploadFromStash loads files via virtuals URLs $tmpFile = $repo->getLocalCopy( $srcPath ); $tmpFile->bind( $this ); // keep alive with $thumb + wfProfileOut( __METHOD__ ); return $tmpFile->getPath(); } + wfProfileOut( __METHOD__ ); return $srcPath; } @@ -222,10 +257,13 @@ abstract class UploadBase { * @return mixed self::OK or else an array with error information */ public function verifyUpload() { + wfProfileIn( __METHOD__ ); + /** * If there was no filename or a zero size given, give up quick. */ if( $this->isEmptyFile() ) { + wfProfileOut( __METHOD__ ); return array( 'status' => self::EMPTY_FILE ); } @@ -234,6 +272,7 @@ abstract class UploadBase { */ $maxSize = self::getMaxUploadSize( $this->getSourceType() ); if( $this->mFileSize > $maxSize ) { + wfProfileOut( __METHOD__ ); return array( 'status' => self::FILE_TOO_LARGE, 'max' => $maxSize, @@ -247,6 +286,7 @@ abstract class UploadBase { */ $verification = $this->verifyFile(); if( $verification !== true ) { + wfProfileOut( __METHOD__ ); return array( 'status' => self::VERIFICATION_ERROR, 'details' => $verification @@ -258,15 +298,19 @@ abstract class UploadBase { */ $result = $this->validateName(); if( $result !== true ) { + wfProfileOut( __METHOD__ ); return $result; } $error = ''; if( !wfRunHooks( 'UploadVerification', - array( $this->mDestName, $this->mTempPath, &$error ) ) ) { + array( $this->mDestName, $this->mTempPath, &$error ) ) ) + { + wfProfileOut( __METHOD__ ); return array( 'status' => self::HOOK_ABORTED, 'error' => $error ); } + wfProfileOut( __METHOD__ ); return array( 'status' => self::OK ); } @@ -304,15 +348,18 @@ abstract class UploadBase { */ protected function verifyMimeType( $mime ) { global $wgVerifyMimeType; + wfProfileIn( __METHOD__ ); if ( $wgVerifyMimeType ) { wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n"); global $wgMimeTypeBlacklist; if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { + wfProfileOut( __METHOD__ ); return array( 'filetype-badmime', $mime ); } # XXX: Missing extension will be caught by validateName() via getTitle() if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) { + wfProfileOut( __METHOD__ ); return array( 'filetype-mime-mismatch', $this->mFinalExtension, $mime ); } @@ -326,11 +373,13 @@ abstract class UploadBase { $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime ); foreach ( $ieTypes as $ieType ) { if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) { + wfProfileOut( __METHOD__ ); return array( 'filetype-bad-ie-mime', $ieType ); } } } + wfProfileOut( __METHOD__ ); return true; } @@ -341,6 +390,8 @@ abstract class UploadBase { */ protected function verifyFile() { global $wgAllowJavaUploads, $wgDisableUploadScriptChecks; + wfProfileIn( __METHOD__ ); + # get the title, even though we are doing nothing with it, because # we need to populate mFinalExtension $this->getTitle(); @@ -351,16 +402,19 @@ abstract class UploadBase { $mime = $this->mFileProps[ 'file-mime' ]; $status = $this->verifyMimeType( $mime ); if ( $status !== true ) { + wfProfileOut( __METHOD__ ); return $status; } # check for htmlish code and javascript if ( !$wgDisableUploadScriptChecks ) { if( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) { + wfProfileOut( __METHOD__ ); return array( 'uploadscripted' ); } if( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) { if( $this->detectScriptInSvg( $this->mTempPath ) ) { + wfProfileOut( __METHOD__ ); return array( 'uploadscripted' ); } } @@ -376,10 +430,12 @@ abstract class UploadBase { $errors = $zipStatus->getErrorsArray(); $error = reset( $errors ); if ( $error[0] !== 'zip-wrong-format' ) { + wfProfileOut( __METHOD__ ); return $error; } } if ( $this->mJavaDetected ) { + wfProfileOut( __METHOD__ ); return array( 'uploadjava' ); } } @@ -387,6 +443,7 @@ abstract class UploadBase { # Scan the uploaded file for viruses $virus = $this->detectVirus( $this->mTempPath ); if ( $virus ) { + wfProfileOut( __METHOD__ ); return array( 'uploadvirus', $virus ); } @@ -395,16 +452,19 @@ abstract class UploadBase { $handlerStatus = $handler->verifyUpload( $this->mTempPath ); if ( !$handlerStatus->isOK() ) { $errors = $handlerStatus->getErrorsArray(); + wfProfileOut( __METHOD__ ); return reset( $errors ); } } wfRunHooks( 'UploadVerifyFile', array( $this, $mime, &$status ) ); if ( $status !== true ) { + wfProfileOut( __METHOD__ ); return $status; } wfDebug( __METHOD__ . ": all clear; passing.\n" ); + wfProfileOut( __METHOD__ ); return true; } @@ -490,6 +550,7 @@ abstract class UploadBase { */ public function checkWarnings() { global $wgLang; + wfProfileIn( __METHOD__ ); $warnings = array(); @@ -550,6 +611,7 @@ abstract class UploadBase { $warnings['duplicate-archive'] = $archivedImage->getName(); } + wfProfileOut( __METHOD__ ); return $warnings; } @@ -557,11 +619,16 @@ abstract class UploadBase { * Really perform the upload. Stores the file in the local repo, watches * if necessary and runs the UploadComplete hook. * + * @param $comment + * @param $pageText + * @param $watch * @param $user User * * @return Status indicating the whether the upload succeeded. */ public function performUpload( $comment, $pageText, $watch, $user ) { + wfProfileIn( __METHOD__ ); + $status = $this->getLocalFile()->upload( $this->mTempPath, $comment, @@ -576,10 +643,10 @@ abstract class UploadBase { if ( $watch ) { $user->addWatch( $this->getLocalFile()->getTitle() ); } - wfRunHooks( 'UploadComplete', array( &$this ) ); } + wfProfileOut( __METHOD__ ); return $status; } @@ -699,7 +766,7 @@ abstract class UploadBase { /** * Return the local file and initializes if necessary. * - * @return LocalFile + * @return LocalFile|null */ public function getLocalFile() { if( is_null( $this->mLocalFile ) ) { @@ -710,26 +777,6 @@ abstract class UploadBase { } /** - * NOTE: Probably should be deprecated in favor of UploadStash, but this is sometimes - * called outside that context. - * - * Stash a file in a temporary directory for later processing - * after the user has confirmed it. - * - * If the user doesn't explicitly cancel or accept, these files - * can accumulate in the temp directory. - * - * @param $saveName String: the destination filename - * @param $tempSrc String: the source temporary file to save - * @return String: full path the stashed file, or false on failure - */ - protected function saveTempUploadedFile( $saveName, $tempSrc ) { - $repo = RepoGroup::singleton()->getLocalRepo(); - $status = $repo->storeTemp( $saveName, $tempSrc ); - return $status; - } - - /** * If the user does not supply all necessary information in the first upload form submission (either by accident or * by design) then we may want to stash the file temporarily, get more information, and publish the file later. * @@ -742,9 +789,13 @@ abstract class UploadBase { */ public function stashFile() { // was stashSessionFile + wfProfileIn( __METHOD__ ); + $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() ); $this->mLocalFile = $file; + + wfProfileOut( __METHOD__ ); return $file; } @@ -787,6 +838,7 @@ abstract class UploadBase { * earlier pseudo-'extensions' to determine type and execute * scripts, so the blacklist needs to check them all. * + * @param $filename string * @return array */ public static function splitExtensions( $filename ) { @@ -870,6 +922,7 @@ abstract class UploadBase { */ public static function detectScript( $file, $mime, $extension ) { global $wgAllowTitlesInSVG; + wfProfileIn( __METHOD__ ); # ugly hack: for text files, always look at the entire file. # For binary field, just check the first K. @@ -885,6 +938,7 @@ abstract class UploadBase { $chunk = strtolower( $chunk ); if( !$chunk ) { + wfProfileOut( __METHOD__ ); return false; } @@ -908,6 +962,7 @@ abstract class UploadBase { # check for HTML doctype if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) { + wfProfileOut( __METHOD__ ); return true; } @@ -944,6 +999,7 @@ abstract class UploadBase { foreach( $tags as $tag ) { if( false !== strpos( $chunk, $tag ) ) { wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" ); + wfProfileOut( __METHOD__ ); return true; } } @@ -958,25 +1014,33 @@ abstract class UploadBase { # look for script-types if( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) { wfDebug( __METHOD__ . ": found script types\n" ); + wfProfileOut( __METHOD__ ); return true; } # look for html-style script-urls if( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { wfDebug( __METHOD__ . ": found html-style script urls\n" ); + wfProfileOut( __METHOD__ ); return true; } # look for css-style script-urls if( preg_match( '!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) { wfDebug( __METHOD__ . ": found css-style script urls\n" ); + wfProfileOut( __METHOD__ ); return true; } wfDebug( __METHOD__ . ": no scripts found\n" ); + wfProfileOut( __METHOD__ ); return false; } + /** + * @param $filename string + * @return bool + */ protected function detectScriptInSvg( $filename ) { $check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) ); return $check->filterMatch; @@ -984,6 +1048,9 @@ abstract class UploadBase { /** * @todo Replace this with a whitelist filter! + * @param $element string + * @param $attribs array + * @return bool */ public function checkSvgScriptCallback( $element, $attribs ) { $strippedElement = $this->stripXmlNamespace( $element ); @@ -1054,7 +1121,7 @@ abstract class UploadBase { } - # use handler attribute with remote / data / script + # use handler attribute with remote / data / script if( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) { wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script '$attrib'='$value' in uploaded file.\n" ); return true; @@ -1082,6 +1149,10 @@ abstract class UploadBase { return false; //No scripts detected } + /** + * @param $name string + * @return string + */ private function stripXmlNamespace( $name ) { // 'http://www.w3.org/2000/svg:script' -> 'script' $parts = explode( ':', strtolower( $name ) ); @@ -1100,9 +1171,11 @@ abstract class UploadBase { */ public static function detectVirus( $file ) { global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; + wfProfileIn( __METHOD__ ); if ( !$wgAntivirus ) { wfDebug( __METHOD__ . ": virus scanner disabled\n" ); + wfProfileOut( __METHOD__ ); return null; } @@ -1110,7 +1183,8 @@ abstract class UploadBase { wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" ); $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>", array( 'virus-badscanner', $wgAntivirus ) ); - return wfMsg( 'virus-unknownscanner' ) . " $wgAntivirus"; + wfProfileOut( __METHOD__ ); + return wfMessage( 'virus-unknownscanner' )->text() . " $wgAntivirus"; } # look up scanner configuration @@ -1152,17 +1226,21 @@ abstract class UploadBase { wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" ); if ( $wgAntivirusRequired ) { - return wfMsg( 'virus-scanfailed', array( $exitCode ) ); + wfProfileOut( __METHOD__ ); + return wfMessage( 'virus-scanfailed', array( $exitCode ) )->text(); } else { + wfProfileOut( __METHOD__ ); return null; } } elseif ( $mappedCode === AV_SCAN_ABORTED ) { # scan failed because filetype is unknown (probably imune) wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" ); + wfProfileOut( __METHOD__ ); return null; } elseif ( $mappedCode === AV_NO_VIRUS ) { # no virus found wfDebug( __METHOD__ . ": file passed virus scan.\n" ); + wfProfileOut( __METHOD__ ); return false; } else { $output = trim( $output ); @@ -1179,6 +1257,7 @@ abstract class UploadBase { } wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" ); + wfProfileOut( __METHOD__ ); return $output; } } @@ -1325,6 +1404,8 @@ abstract class UploadBase { /** * Helper function that checks whether the filename looks like a thumbnail + * @param $filename string + * @return bool */ public static function isThumbName( $filename ) { $n = strrpos( $filename, '.' ); @@ -1387,13 +1468,20 @@ abstract class UploadBase { return $info; } - + /** + * @param $error array + * @return Status + */ public function convertVerifyErrorToStatus( $error ) { $code = $error['status']; unset( $code['status'] ); return Status::newFatal( $this->getVerificationErrorCode( $code ), $error ); } + /** + * @param $forType null|string + * @return int + */ public static function getMaxUploadSize( $forType = null ) { global $wgMaxUploadSize; diff --git a/includes/upload/UploadFromChunks.php b/includes/upload/UploadFromChunks.php index ec83f7d3..0542bba5 100644 --- a/includes/upload/UploadFromChunks.php +++ b/includes/upload/UploadFromChunks.php @@ -1,5 +1,27 @@ <?php /** + * Backend for uploading files from chunks. + * + * 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 Upload + */ + +/** * Implements uploading from chunks * * @ingroup Upload @@ -7,10 +29,10 @@ */ class UploadFromChunks extends UploadFromFile { protected $mOffset, $mChunkIndex, $mFileKey, $mVirtualTempPath; - + /** * Setup local pointers to stash, repo and user ( similar to UploadFromStash ) - * + * * @param $user User * @param $stash UploadStash * @param $repo FileRepo @@ -39,37 +61,37 @@ class UploadFromChunks extends UploadFromFile { return true; } /** - * Calls the parent stashFile and updates the uploadsession table to handle "chunks" + * Calls the parent stashFile and updates the uploadsession table to handle "chunks" * * @return UploadStashFile stashed file */ public function stashFile() { - // Stash file is the called on creating a new chunk session: + // Stash file is the called on creating a new chunk session: $this->mChunkIndex = 0; $this->mOffset = 0; // Create a local stash target $this->mLocalFile = parent::stashFile(); - // Update the initial file offset ( based on file size ) + // Update the initial file offset ( based on file size ) $this->mOffset = $this->mLocalFile->getSize(); $this->mFileKey = $this->mLocalFile->getFileKey(); // Output a copy of this first to chunk 0 location: $status = $this->outputChunk( $this->mLocalFile->getPath() ); - - // Update db table to reflect initial "chunk" state + + // Update db table to reflect initial "chunk" state $this->updateChunkStatus(); return $this->mLocalFile; } - + /** * Continue chunk uploading - */ + */ public function continueChunks( $name, $key, $webRequestUpload ) { $this->mFileKey = $key; $this->mUpload = $webRequestUpload; - // Get the chunk status form the db: + // Get the chunk status form the db: $this->getChunkStatus(); - + $metadata = $this->stash->getMetadata( $key ); $this->initializePathInfo( $name, $this->getRealPath( $metadata['us_path'] ), @@ -77,13 +99,13 @@ class UploadFromChunks extends UploadFromFile { false ); } - + /** * Append the final chunk and ready file for parent::performUpload() * @return FileRepoStatus */ public function concatenateChunks() { - wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . + wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); // Concatenate all the chunks to mVirtualTempPath @@ -103,10 +125,10 @@ class UploadFromChunks extends UploadFromFile { // Concatenate the chunks at the temp file $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE ); if( !$status->isOk() ){ - return $status; + return $status; } // Update the mTempPath and mLocalFile - // ( for FileUpload or normal Stash to take over ) + // ( for FileUpload or normal Stash to take over ) $this->mTempPath = $tmpPath; // file system path $this->mLocalFile = parent::stashFile(); @@ -127,42 +149,44 @@ class UploadFromChunks extends UploadFromFile { } /** - * Returns the virtual chunk location: - * @param unknown_type $index + * Returns the virtual chunk location: + * @param $index + * @return string */ function getVirtualChunkLocation( $index ){ - return $this->repo->getVirtualUrl( 'temp' ) . + return $this->repo->getVirtualUrl( 'temp' ) . '/' . - $this->repo->getHashPath( + $this->repo->getHashPath( $this->getChunkFileKey( $index ) - ) . + ) . $this->getChunkFileKey( $index ); } + /** * Add a chunk to the temporary directory * - * @param $chunkPath path to temporary chunk file - * @param $chunkSize size of the current chunk - * @param $offset offset of current chunk ( mutch match database chunk offset ) + * @param $chunkPath string path to temporary chunk file + * @param $chunkSize int size of the current chunk + * @param $offset int offset of current chunk ( mutch match database chunk offset ) * @return Status */ public function addChunk( $chunkPath, $chunkSize, $offset ) { // Get the offset before we add the chunk to the file system $preAppendOffset = $this->getOffset(); - + if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize()) { $status = Status::newFatal( 'file-too-large' ); } else { // Make sure the client is uploading the correct chunk with a matching offset. if ( $preAppendOffset == $offset ) { - // Update local chunk index for the current chunk + // Update local chunk index for the current chunk $this->mChunkIndex++; $status = $this->outputChunk( $chunkPath ); if( $status->isGood() ){ - // Update local offset: + // Update local offset: $this->mOffset = $preAppendOffset + $chunkSize; - // Update chunk table status db - $this->updateChunkStatus(); + // Update chunk table status db + $this->updateChunkStatus(); } } else { $status = Status::newFatal( 'invalid-chunk-offset' ); @@ -170,18 +194,18 @@ class UploadFromChunks extends UploadFromFile { } return $status; } - + /** - * Update the chunk db table with the current status: + * Update the chunk db table with the current status: */ private function updateChunkStatus(){ - wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . + wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); - + $dbw = $this->repo->getMasterDb(); $dbw->update( 'uploadstash', - array( + array( 'us_status' => 'chunks', 'us_chunk_inx' => $this->getChunkIndex(), 'us_size' => $this->getOffset() @@ -190,16 +214,17 @@ class UploadFromChunks extends UploadFromFile { __METHOD__ ); } + /** * Get the chunk db state and populate update relevant local values */ private function getChunkStatus(){ - // get Master db to avoid race conditions. + // get Master db to avoid race conditions. // Otherwise, if chunk upload time < replag there will be spurious errors $dbw = $this->repo->getMasterDb(); $row = $dbw->selectRow( - 'uploadstash', - array( + 'uploadstash', + array( 'us_chunk_inx', 'us_size', 'us_path', @@ -214,8 +239,9 @@ class UploadFromChunks extends UploadFromFile { $this->mVirtualTempPath = $row->us_path; } } + /** - * Get the current Chunk index + * Get the current Chunk index * @return Integer index of the current chunk */ private function getChunkIndex(){ @@ -224,10 +250,10 @@ class UploadFromChunks extends UploadFromFile { } return 0; } - + /** - * Gets the current offset in fromt the stashedupload table - * @return Integer current byte offset of the chunk file set + * Gets the current offset in fromt the stashedupload table + * @return Integer current byte offset of the chunk file set */ private function getOffset(){ if ( $this->mOffset !== null ){ @@ -235,20 +261,23 @@ class UploadFromChunks extends UploadFromFile { } return 0; } - + /** * Output the chunk to disk - * + * * @param $chunkPath string + * @throws UploadChunkFileException + * @return FileRepoStatus */ private function outputChunk( $chunkPath ){ // Key is fileKey + chunk index $fileKey = $this->getChunkFileKey(); - - // Store the chunk per its indexed fileKey: + + // Store the chunk per its indexed fileKey: $hashPath = $this->repo->getHashPath( $fileKey ); - $storeStatus = $this->repo->store( $chunkPath, 'temp', "$hashPath$fileKey" ); - + $storeStatus = $this->repo->quickImport( $chunkPath, + $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" ); + // Check for error in stashing the chunk: if ( ! $storeStatus->isOK() ) { $error = $storeStatus->getErrorsArray(); @@ -264,6 +293,7 @@ class UploadFromChunks extends UploadFromFile { } return $storeStatus; } + private function getChunkFileKey( $index = null ){ if( $index === null ){ $index = $this->getChunkIndex(); diff --git a/includes/upload/UploadFromFile.php b/includes/upload/UploadFromFile.php index 23ec2ef4..aa0cc77b 100644 --- a/includes/upload/UploadFromFile.php +++ b/includes/upload/UploadFromFile.php @@ -1,5 +1,27 @@ <?php /** + * Backend for regular file upload. + * + * 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 Upload + */ + +/** * Implements regular file uploads * * @ingroup Upload @@ -16,12 +38,13 @@ class UploadFromFile extends UploadBase { * @param $request WebRequest */ function initializeFromRequest( &$request ) { - $upload = $request->getUpload( 'wpUploadFile' ); + $upload = $request->getUpload( 'wpUploadFile' ); $desiredDestName = $request->getText( 'wpDestFile' ); - if( !$desiredDestName ) + if( !$desiredDestName ) { $desiredDestName = $upload->getName(); - - return $this->initialize( $desiredDestName, $upload ); + } + + $this->initialize( $desiredDestName, $upload ); } /** @@ -31,7 +54,7 @@ class UploadFromFile extends UploadBase { */ function initialize( $name, $webRequestUpload ) { $this->mUpload = $webRequestUpload; - return $this->initializePathInfo( $name, + $this->initializePathInfo( $name, $this->mUpload->getTempName(), $this->mUpload->getSize() ); } diff --git a/includes/upload/UploadFromStash.php b/includes/upload/UploadFromStash.php index f7589bd2..607965f3 100644 --- a/includes/upload/UploadFromStash.php +++ b/includes/upload/UploadFromStash.php @@ -1,5 +1,27 @@ <?php /** + * Backend for uploading files from previously stored file. + * + * 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 Upload + */ + +/** * Implements uploading from previously stored file. * * @ingroup Upload @@ -40,8 +62,6 @@ class UploadFromStash extends UploadBase { $this->stash = new UploadStash( $this->repo, $this->user ); } - - return true; } /** @@ -99,7 +119,7 @@ class UploadFromStash extends UploadBase { // chooses one of wpDestFile, wpUploadFile, filename in that order. $desiredDestName = $request->getText( 'wpDestFile', $request->getText( 'wpUploadFile', $request->getText( 'filename' ) ) ); - return $this->initialize( $fileKey, $desiredDestName ); + $this->initialize( $fileKey, $desiredDestName ); } /** @@ -140,7 +160,7 @@ class UploadFromStash extends UploadBase { /** * Remove a temporarily kept file stashed by saveTempUploadedFile(). - * @return success + * @return bool success */ public function unsaveUploadedFile() { return $this->stash->removeFile( $this->mFileKey ); diff --git a/includes/upload/UploadFromUrl.php b/includes/upload/UploadFromUrl.php index da772fe2..927c3cd9 100644 --- a/includes/upload/UploadFromUrl.php +++ b/includes/upload/UploadFromUrl.php @@ -1,5 +1,27 @@ <?php /** + * Backend for uploading files from a HTTP resource. + * + * 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 Upload + */ + +/** * Implements uploading from a HTTP resource. * * @ingroup Upload @@ -14,11 +36,12 @@ class UploadFromUrl extends UploadBase { /** * Checks if the user is allowed to use the upload-by-URL feature. If the - * user is allowed, pass on permissions checking to the parent. + * user is not allowed, return the name of the user right as a string. If + * the user is allowed, have the parent do further permissions checking. * * @param $user User * - * @return bool + * @return bool|string */ public static function isAllowed( $user ) { if ( !$user->isAllowed( 'upload_by_url' ) ) { @@ -37,6 +60,31 @@ class UploadFromUrl extends UploadBase { } /** + * Checks whether the URL is for an allowed host + * + * @param $url string + * @return bool + */ + public static function isAllowedHost( $url ) { + global $wgCopyUploadsDomains; + if ( !count( $wgCopyUploadsDomains ) ) { + return true; + } + $parsedUrl = wfParseUrl( $url ); + if ( !$parsedUrl ) { + return false; + } + $valid = false; + foreach( $wgCopyUploadsDomains as $domain ) { + if ( $parsedUrl['host'] === $domain ) { + $valid = true; + break; + } + } + return $valid; + } + + /** * Entry point for API upload * * @param $name string @@ -44,6 +92,7 @@ class UploadFromUrl extends UploadBase { * @param $async mixed Whether the download should be performed * asynchronous. False for synchronous, async or async-leavemessage for * asynchronous download. + * @throws MWException */ public function initialize( $name, $url, $async = false ) { global $wgAllowAsyncCopyUploads; @@ -68,7 +117,7 @@ class UploadFromUrl extends UploadBase { if ( !$desiredDestName ) { $desiredDestName = $request->getText( 'wpUploadFileURL' ); } - return $this->initialize( + $this->initialize( $desiredDestName, trim( $request->getVal( 'wpUploadFileURL' ) ), false @@ -101,6 +150,9 @@ class UploadFromUrl extends UploadBase { return Status::newFatal( 'http-invalid-url' ); } + if( !self::isAllowedHost( $this->mUrl ) ) { + return Status::newFatal( 'upload-copy-upload-invalid-domain' ); + } if ( !$this->mAsync ) { return $this->reallyFetchFile(); } @@ -155,9 +207,14 @@ class UploadFromUrl extends UploadBase { $this->mRemoveTempFile = true; $this->mFileSize = 0; - $req = MWHttpRequest::factory( $this->mUrl, array( + $options = array( 'followRedirects' => true - ) ); + ); + global $wgCopyUploadProxy; + if ( $wgCopyUploadProxy !== false ) { + $options['proxy'] = $wgCopyUploadProxy; + } + $req = MWHttpRequest::factory( $this->mUrl, $options ); $req->setCallback( array( $this, 'saveTempFileChunk' ) ); $status = $req->execute(); @@ -180,6 +237,7 @@ class UploadFromUrl extends UploadBase { /** * Wrapper around the parent function in order to defer verifying the * upload until the file really has been fetched. + * @return array|mixed */ public function verifyUpload() { if ( $this->mAsync ) { @@ -191,6 +249,7 @@ class UploadFromUrl extends UploadBase { /** * Wrapper around the parent function in order to defer checking warnings * until the file really has been fetched. + * @return Array */ public function checkWarnings() { if ( $this->mAsync ) { @@ -203,6 +262,8 @@ class UploadFromUrl extends UploadBase { /** * Wrapper around the parent function in order to defer checking protection * until we are sure that the file can actually be uploaded + * @param $user User + * @return bool|mixed */ public function verifyTitlePermissions( $user ) { if ( $this->mAsync ) { @@ -214,6 +275,11 @@ class UploadFromUrl extends UploadBase { /** * Wrapper around the parent function in order to defer uploading to the * job queue for asynchronous uploads + * @param $comment string + * @param $pageText string + * @param $watch bool + * @param $user User + * @return Status */ public function performUpload( $comment, $pageText, $watch, $user ) { if ( $this->mAsync ) { @@ -226,11 +292,11 @@ class UploadFromUrl extends UploadBase { } /** - * @param $comment - * @param $pageText - * @param $watch - * @param $user User - * @return + * @param $comment + * @param $pageText + * @param $watch + * @param $user User + * @return String */ protected function insertJob( $comment, $pageText, $watch, $user ) { $sessionKey = $this->stashSession(); diff --git a/includes/upload/UploadStash.php b/includes/upload/UploadStash.php index ad153d2f..c7fd23a9 100644 --- a/includes/upload/UploadStash.php +++ b/includes/upload/UploadStash.php @@ -1,5 +1,27 @@ <?php /** + * Temporary storage for uploaded files. + * + * 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 Upload + */ + +/** * UploadStash is intended to accomplish a few things: * - enable applications to temporarily stash files without publishing them to the wiki. * - Several parts of MediaWiki do this in similar ways: UploadBase, UploadWizard, and FirefoggChunkedExtension @@ -46,9 +68,11 @@ class UploadStash { /** * Represents a temporary filestore, with metadata in the database. - * Designed to be compatible with the session stashing code in UploadBase (should replace it eventually) + * Designed to be compatible with the session stashing code in UploadBase + * (should replace it eventually). * * @param $repo FileRepo + * @param $user User (default null) */ public function __construct( FileRepo $repo, $user = null ) { // this might change based on wiki's configuration. @@ -215,10 +239,14 @@ class UploadStash { } } // at this point, $error should contain the single "most important" error, plus any parameters. - throw new UploadStashFileException( "Error storing file in '$path': " . wfMessage( $error )->text() ); + $errorMsg = array_shift( $error ); + throw new UploadStashFileException( "Error storing file in '$path': " . wfMessage( $errorMsg, $error )->text() ); } $stashPath = $storeStatus->value; + // we have renamed the file so we have to cleanup once done + unlink($path); + // fetch the current user ID if ( !$this->isLoggedIn ) { throw new UploadStashNotLoggedInException( __METHOD__ . ' No user is logged in, files must belong to users' ); @@ -391,6 +419,7 @@ class UploadStash { * with an extension. * XXX this is somewhat redundant with the checks that ApiUpload.php does with incoming * uploads versus the desired filename. Maybe we can get that passed to us... + * @return string */ public static function getExtensionForPath( $path ) { // Does this have an extension? @@ -419,6 +448,7 @@ class UploadStash { * Helper function: do the actual database query to fetch file metadata. * * @param $key String: key + * @param $readFromDB: constant (default: DB_SLAVE) * @return boolean */ protected function fetchFileMetadata( $key, $readFromDB = DB_SLAVE ) { @@ -451,7 +481,6 @@ class UploadStash { /** * Helper function: Initialize the UploadStashFile for a given file. * - * @param $path String: path to file * @param $key String: key under which to store the object * @throws UploadStashZeroLengthFileException * @return bool @@ -498,7 +527,7 @@ class UploadStashFile extends UnregisteredLocalFile { } // check if path exists! and is a plain file. - if ( ! $repo->fileExists( $path, FileRepo::FILES_ONLY ) ) { + if ( ! $repo->fileExists( $path ) ) { wfDebug( "UploadStash: tried to construct an UploadStashFile from a file that should already exist at '$path', but path is not found\n" ); throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' ); } @@ -543,16 +572,17 @@ class UploadStashFile extends UnregisteredLocalFile { * ugly file name. * * @param $params Array: handler-specific parameters + * @param $flags integer Bitfield that supports THUMB_* constants * @return String: base name for URL, like '120px-12345.jpg', or null if there is no handler */ - function thumbName( $params ) { + function thumbName( $params, $flags = 0 ) { return $this->generateThumbName( $this->getUrlName(), $params ); } /** * Helper function -- given a 'subpage', return the local URL e.g. /wiki/Special:UploadStash/subpage - * @param {String} $subPage - * @return {String} local URL for this subpage in the Special:UploadStash space. + * @param $subPage String + * @return String: local URL for this subpage in the Special:UploadStash space. */ private function getSpecialUrl( $subPage ) { return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL(); @@ -622,7 +652,7 @@ class UploadStashFile extends UnregisteredLocalFile { * @return Status: success */ public function remove() { - if ( !$this->repo->fileExists( $this->path, FileRepo::FILES_ONLY ) ) { + if ( !$this->repo->fileExists( $this->path ) ) { // Maybe the file's already been removed? This could totally happen in UploadBase. return true; } @@ -631,7 +661,7 @@ class UploadStashFile extends UnregisteredLocalFile { } public function exists() { - return $this->repo->fileExists( $this->path, FileRepo::FILES_ONLY ); + return $this->repo->fileExists( $this->path ); } } diff --git a/includes/zhtable/Makefile.py b/includes/zhtable/Makefile.py index 305422bd..fd603ce4 100644 --- a/includes/zhtable/Makefile.py +++ b/includes/zhtable/Makefile.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # @author Philip import tarfile as tf @@ -31,7 +31,7 @@ def unichr3( *args ): # DEFINE UNIHAN_VER = '5.2.0' -SF_MIRROR = 'cdnetworks-kr-2' +SF_MIRROR = 'dfn' SCIM_TABLES_VER = '0.5.10' SCIM_PINYIN_VER = '0.5.91' LIBTABE_VER = '0.2.3' @@ -39,7 +39,7 @@ LIBTABE_VER = '0.2.3' def download( url, dest ): if os.path.isfile( dest ): - print( 'File %s up to date.' % dest ) + print( 'File %s is up to date.' % dest ) return global islinux if islinux: @@ -370,15 +370,15 @@ $zh2Hant = array(\n''' + PHPArray( toCN ) \ + '\n);\n\n$zh2SG = array(\n' \ + PHPArray( toSG ) \ - + '\n);' + + '\n);\n' - f = open( 'ZhConversion.php', 'wb', encoding = 'utf8' ) + f = open( os.path.join( '..', 'ZhConversion.php' ), 'wb', encoding = 'utf8' ) print ('Writing ZhConversion.php ... ') f.write( php ) f.close() - #Remove temp files - print ('Deleting temp files ... ') + # Remove temporary files + print ('Deleting temporary files ... ') os.remove('EZ-Big.txt.in') os.remove('phrase_lib.txt') os.remove('tsi.src') diff --git a/includes/zhtable/simp2trad.manual b/includes/zhtable/simp2trad.manual index eb5fa396..1b84f8e7 100644 --- a/includes/zhtable/simp2trad.manual +++ b/includes/zhtable/simp2trad.manual @@ -239,7 +239,6 @@ U+09E21鸡|U+096DE雞|U+09DC4鷄| U+09E5A鹚|U+09DBF鶿|U+09DC0鷀| U+09E6E鹮|U+04D09䴉| U+09F44齄|U+09F47齇| -U+0E82D|U+068E1棡| U+20BB6𠮶|U+055F0嗰| U+26216𦈖|U+04308䌈| U+28C3E𨰾|U+093B7鎷| diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual index 41680d1f..243f61b0 100644 --- a/includes/zhtable/toCN.manual +++ b/includes/zhtable/toCN.manual @@ -9,7 +9,6 @@ 乙太網 以太网 點陣圖 位图 常式 例程 -游標 光标 光碟 光盘 光碟機 光驱 全形 全角 diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual index 2ebb7504..1f7fe7d0 100644 --- a/includes/zhtable/toHK.manual +++ b/includes/zhtable/toHK.manual @@ -2240,3 +2240,61 @@ 分布于 分佈於 分布於 分佈於 想象 想像 +無線電視 無綫電視 +无线电视 無綫電視 +無線收費 無綫收費 +无线收费 無綫收費 +無線節目 無綫節目 +无线节目 無綫節目 +無線劇集 無綫劇集 +无线剧集 無綫劇集 +東鐵線 東鐵綫 +东铁线 東鐵綫 +觀塘線 觀塘綫 +观塘线 觀塘綫 +荃灣線 荃灣綫 +荃湾线 荃灣綫 +港島線 港島綫 +港岛线 港島綫 +東涌線 東涌綫 +东涌线 東涌綫 +將軍澳線 將軍澳綫 +将军澳线 將軍澳綫 +西鐵線 西鐵綫 +西铁线 西鐵綫 +馬鞍山線 馬鞍山綫 +马鞍山线 馬鞍山綫 +迪士尼線 迪士尼綫 +迪士尼线 迪士尼綫 +沙田至中環線 沙田至中環綫 +沙田至中环线 沙田至中環綫 +沙中線 沙中綫 +沙中线 沙中綫 +北環線 北環綫 +北环线 北環綫 +機場快線 機場快綫 +机场快线 機場快綫 +505線 505綫 +505线 505綫 +507線 507綫 +507线 507綫 +610線 610綫 +610线 610綫 +614線 614綫 +614线 614綫 +614P線 614P綫 +614P线 614P綫 +615線 615綫 +615线 615綫 +615P線 615P綫 +615P线 615P綫 +705線 705綫 +705线 705綫 +706線 706綫 +706线 706綫 +751線 751綫 +751线 751綫 +751P線 751P綫 +751P线 751P綫 +761P線 761P綫 +761P线 761P綫 diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual index 35b62689..1a14e99a 100644 --- a/includes/zhtable/toTW.manual +++ b/includes/zhtable/toTW.manual @@ -408,3 +408,4 @@ 想象 想像 锎 鉲 信道 信道 +綫 線 diff --git a/includes/zhtable/trad2simp.manual b/includes/zhtable/trad2simp.manual index 692c74b5..7c3ce10d 100644 --- a/includes/zhtable/trad2simp.manual +++ b/includes/zhtable/trad2simp.manual @@ -43,7 +43,6 @@ U+065E3旣|U+065E2既| U+06607昇|U+05347升| U+0672E朮|U+0672F术| U+068CA棊|U+068CB棋| -U+068E1棡|U+0E82D| U+069A6榦|U+05E72干| U+069D3槓|U+06760杠| U+06A11樑|U+06881梁| diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual index ee3bc69f..9a9534f8 100644 --- a/includes/zhtable/tradphrases.manual +++ b/includes/zhtable/tradphrases.manual @@ -3032,6 +3032,7 @@ 細如髮 繫於一髮 膚髮 +皮膚 生華髮 蒼髮 被髮佯狂 @@ -3501,6 +3502,7 @@ 藍澱 皆可作澱 澱山 +海淀山後 澱澱 掛鈎 薴悴 @@ -3982,6 +3984,7 @@ 棺材裡 注釋 月面 +路面 修杰楷 修杰麟 學裡 |