Merge "escape HTML elements in docblock with double quotes"
authorAaron Schulz <aschulz@wikimedia.org>
Wed, 18 Jul 2012 19:17:46 +0000 (19:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 18 Jul 2012 19:17:46 +0000 (19:17 +0000)
69 files changed:
RELEASE-NOTES-1.20
docs/hooks.txt
includes/CacheHelper.php
includes/Export.php
includes/GlobalFunctions.php
includes/HTMLForm.php
includes/RecentChange.php
includes/SkinTemplate.php
includes/actions/InfoAction.php
includes/api/ApiBase.php
includes/api/ApiBlock.php
includes/api/ApiDelete.php
includes/api/ApiMain.php
includes/api/ApiMove.php
includes/api/ApiPageSet.php
includes/api/ApiProtect.php
includes/api/ApiQuery.php
includes/api/ApiQueryAllCategories.php
includes/api/ApiQueryAllImages.php
includes/api/ApiQueryAllLinks.php
includes/api/ApiQueryAllPages.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryCategories.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryDuplicateFiles.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryIWBacklinks.php
includes/api/ApiQueryIWLinks.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryImages.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryLangBacklinks.php
includes/api/ApiQueryLinks.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryStashImageInfo.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiQueryWatchlistRaw.php
includes/api/ApiRollback.php
includes/api/ApiUnblock.php
includes/api/ApiUndelete.php
includes/cache/ProcessCacheLRU.php
includes/db/Database.php
includes/db/DatabaseOracle.php
includes/db/DatabasePostgres.php
includes/filerepo/backend/FSFileBackend.php
includes/filerepo/backend/FileBackend.php
includes/filerepo/backend/FileBackendMultiWrite.php
includes/filerepo/backend/FileBackendStore.php
includes/filerepo/backend/FileOp.php
includes/filerepo/backend/FileOpBatch.php
includes/filerepo/backend/SwiftFileBackend.php
includes/filerepo/backend/lockmanager/MemcLockManager.php
includes/installer/Installer.php
includes/logging/LogFormatter.php
includes/media/FormatMetadata.php
includes/parser/LinkHolderArray.php
includes/search/SearchOracle.php
includes/specials/SpecialProtectedpages.php
includes/specials/SpecialVersion.php
languages/messages/MessagesBa.php
languages/messages/MessagesShi.php
maintenance/copyFileBackend.php
maintenance/mergeMessageFileList.php
maintenance/syncFileBackend.php
maintenance/updateCollation.php
tests/phpunit/MediaWikiLangTestCase.php
tests/phpunit/docs/ExportDemoTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/languages/LanguageTest.php

index fdbc3eb..8bebaaa 100644 (file)
@@ -190,8 +190,9 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
 * (bug 32381) Allow descending order for list=backlinks, list=embeddedin and list=imageusage.
 * (bug 32383) Allow descending order for list=langbacklinks.
 * API meta=siteinfo can now return the list of known variable IDs.
-* (bug 30836) siteinfo prop=specialpagealiases will no longer return nonexistent special pages.
 * (bug 35980) list=deletedrevs now honors drdir correctly in "all" mode (mode #3).
+* (bug 29290) API avoids mangling fields in continuation parameters
+* (bug 36987) API avoids mangling fields in continuation parameters
 
 === Languages updated in 1.20 ===
 
index 47654ce..5a836ae 100644 (file)
@@ -245,6 +245,10 @@ $block: The block from which the autoblock is coming.
 'AbortDiffCache': Can be used to cancel the caching of a diff
 &$diffEngine: DifferenceEngine object
 
+'AbortEmailNotification': Can be used to cancel email notifications for an edit.
+$editor: The User who made the change.
+$title: The Title of the page that was edited.
+
 'AbortLogin': Return false to cancel account login.
 $user: the User object being authenticated against
 $password: the password being submitted, not yet checked for validity
index 5209857..8199cb4 100644 (file)
@@ -73,7 +73,8 @@ interface ICacheHelper {
        function saveCache();
 
        /**
-        * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry..
+        * Sets the time to live for the cache, in seconds or a unix timestamp
+        * indicating the point of expiry...
         *
         * @since 1.20
         *
@@ -319,7 +320,8 @@ class CacheHelper implements ICacheHelper {
        }
 
        /**
-        * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry..
+        * Sets the time to live for the cache, in seconds or a unix timestamp
+        * indicating the point of expiry...
         *
         * @since 1.20
         *
index 2bef114..f01fb23 100644 (file)
@@ -58,6 +58,14 @@ class WikiExporter {
         */
        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
@@ -465,10 +473,12 @@ class WikiExporter {
 class XmlDumpWriter {
        /**
         * Returns the export schema version.
+        * @deprecated in 1.20; use WikiExporter::schemaVersion() instead
         * @return string
         */
        function schemaVersion() {
-               return "0.7";
+               wfDeprecated( __METHOD__, '1.20' );
+               return WikiExporter::schemaVersion();
        }
 
        /**
@@ -483,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",
index f9ab140..35887a4 100644 (file)
@@ -2885,9 +2885,11 @@ function wfEscapeShellArg( ) {
  *                 (non-zero is usually failure)
  * @param $environ Array optional environment variables which should be
  *                 added to the executed command environment.
+ * @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;
@@ -2935,9 +2937,9 @@ function wfShellExec( $cmd, &$retval = null, $environ = array() ) {
        $cmd = $envcmd . $cmd;
 
        if ( php_uname( 's' ) == 'Linux' ) {
-               $time = intval( $wgMaxShellTime );
-               $mem = intval( $wgMaxShellMemory );
-               $filesize = intval( $wgMaxShellFileSize );
+               $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";
@@ -3207,7 +3209,7 @@ function wfUseMW( $req_ver ) {
  * 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
index 9ced19e..38b15b9 100644 (file)
@@ -688,7 +688,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 = '';
@@ -1991,7 +1991,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 ) ) {
index fb8e022..20e7909 100644 (file)
@@ -231,13 +231,15 @@ class RecentChange {
                        }
                        $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'] );
+                       if ( wfRunHooks( 'AbortEmailNotification', array($editor, $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'] );
+                       }
                }
        }
 
index 6ad374d..3f07dd6 100644 (file)
@@ -1699,17 +1699,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
@@ -1717,6 +1722,7 @@ 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() ) {
index 543beb7..89e2cfe 100644 (file)
@@ -107,6 +107,7 @@ class InfoAction extends FormlessAction {
         * @return mixed array or boolean false
         */
        public static function pageCountInfo( $title ) {
+               wfProfileIn( __METHOD__ );
                $id = $title->getArticleID();
                $dbr = wfGetDB( DB_SLAVE );
 
@@ -114,8 +115,8 @@ class InfoAction extends FormlessAction {
                        'watchlist',
                        'COUNT(*)',
                        array(
+                               'wl_namespace' => $title->getNamespace(),
                                'wl_title'     => $title->getDBkey(),
-                               'wl_namespace' => $title->getNamespace()
                        ),
                        __METHOD__
                );
@@ -133,15 +134,21 @@ class InfoAction extends FormlessAction {
                        array( 'rev_page' => $id ),
                        __METHOD__
                );
+               $result = array( 'watchers' => $watchers, 'edits' => $edits,
+                       'authors' => $authors );
 
-               $views = (int)$dbr->selectField(
-                       'page',
-                       'page_counter',
-                       array( 'page_id' => $id ),
-                       __METHOD__
-               );
-
-               return array( 'watchers' => $watchers, 'edits' => $edits,
-                       'authors' => $authors, 'views' => $views );
+               global $wgDisableCounters;
+               if ( !$wgDisableCounters ) {
+                       $views = (int)$dbr->selectField(
+                               'page',
+                               'page_counter',
+                               array( 'page_id' => $id ),
+                               __METHOD__
+                       );
+                       $result['views'] = $views;
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $result;
        }
 }
index dfc2a61..bc94ee4 100644 (file)
@@ -134,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
         */
@@ -1185,6 +1185,7 @@ abstract class ApiBase extends ContextSource {
         *   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
+        * @throws UsageException
         */
        public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
                Profiler::instance()->close();
index 3050c2b..c879b35 100644 (file)
@@ -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'],
@@ -156,7 +156,7 @@ class ApiBlock extends ApiBase {
                                ApiBase::PARAM_DEPRECATED => true,
                        ),
                        'expiry' => 'never',
-                       'reason' => null,
+                       'reason' => '',
                        'anononly' => false,
                        'nocreate' => false,
                        'autoblock' => false,
@@ -174,7 +174,7 @@ class ApiBlock extends ApiBase {
                        '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',
index 39283d8..b5079a2 100644 (file)
@@ -52,7 +52,7 @@ class ApiDelete extends ApiBase {
                }
 
                $titleObj = $pageObj->getTitle();
-               $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
+               $reason = $params['reason'];
                $user = $this->getUser();
 
                if ( $titleObj->getNamespace() == NS_FILE ) {
index 0cb7d59..05f6652 100644 (file)
@@ -1160,8 +1160,18 @@ class ApiMain extends ApiBase {
 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;
index 68d8e6e..1fd81ab 100644 (file)
@@ -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' );
 
@@ -181,7 +178,7 @@ class ApiMove extends ApiBase {
                                ApiBase::PARAM_REQUIRED => true
                        ),
                        'token' => null,
-                       'reason' => null,
+                       'reason' => '',
                        'movetalk' => false,
                        'movesubpages' => false,
                        'noredirect' => false,
@@ -213,7 +210,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',
index 1619681..0f5be6b 100644 (file)
@@ -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
         */
index 63b670e..e763fd5 100644 (file)
@@ -176,7 +176,7 @@ class ApiProtect extends ApiBase {
                        'protections' => 'Pipe-separated 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',
index 4f216cf..99e22d5 100644 (file)
@@ -103,6 +103,10 @@ class ApiQuery extends ApiBase {
 
        protected $mAllowedGenerators = array();
 
+       /**
+        * @param $main ApiMain
+        * @param $action string
+        */
        public function __construct( $main, $action ) {
                parent::__construct( $main, $action );
 
@@ -202,6 +206,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 +265,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 +313,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 +397,9 @@ class ApiQuery extends ApiBase {
 
                // Show redirect information
                $redirValues = array();
+               /**
+                * @var $titleTo Title
+                */
                foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleTo ) {
                        $r = array(
                                'from' => strval( $titleStrFrom ),
index 14bc15f..e2d02a0 100644 (file)
@@ -59,6 +59,17 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
                $this->addFields( 'cat_title' );
                $this->addWhere( 'cat_pages > 0' );
 
+               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'] ) );
@@ -105,8 +116,7 @@ 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;
                        }
 
@@ -128,7 +138,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;
                                }
                        }
@@ -144,6 +154,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
        public function getAllowedParams() {
                return array(
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'dir' => array(
@@ -179,6 +190,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',
@@ -214,6 +226,12 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
                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',
index 4ab4d72..45cc404 100644 (file)
@@ -85,6 +85,17 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
 
                $params = $this->extractRequestParams();
 
+               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( "img_name $op= $cont_from" );
+               }
+
                // Image filters
                $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
                $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
@@ -148,8 +159,7 @@ class ApiQueryAllImages 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
-                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
+                               $this->setContinueEnumParameter( 'continue', $row->img_name );
                                break;
                        }
 
@@ -161,7 +171,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
 
                                $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->img_name );
                                        break;
                                }
                        } else {
@@ -179,6 +189,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
        public function getAllowedParams() {
                return array (
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'minsize' => array(
@@ -215,6 +226,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
        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',
@@ -228,7 +240,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                );
        }
 
-       private $propertyFilter = array( 'archivename' );
+       private $propertyFilter = array( 'archivename', 'thumbmime' );
 
        public function getResultProperties() {
                return array_merge(
@@ -254,6 +266,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                        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' ),
                ) );
        }
 
index 59fdd49..da4840f 100644 (file)
@@ -77,17 +77,25 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
                }
                if ( !is_null( $params['continue'] ) ) {
                        $continueArr = explode( '|', $params['continue'] );
-                       if ( count( $continueArr ) != 2 ) {
-                               $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
-                       }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
-                       $continueTitle = $db->addQuotes( $this->titleToKey( $continueArr[0] ) );
-                       $continueFrom = intval( $continueArr[1] );
-                       $this->addWhere(
-                               "pl_title $op $continueTitle OR " .
-                               "(pl_title = $continueTitle AND " .
-                               "pl_from $op= $continueFrom)"
-                       );
+                       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 = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
@@ -121,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;
                        }
@@ -142,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;
                                }
index b846e8b..16cc31d 100644 (file)
@@ -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' ) {
@@ -180,8 +191,7 @@ class ApiQueryAllPages 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
-                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) );
+                               $this->setContinueEnumParameter( 'continue', $row->page_title );
                                break;
                        }
 
@@ -194,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 {
@@ -212,6 +222,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
 
                return array(
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'namespace' => array(
@@ -285,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',
@@ -324,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' ),
                ) );
        }
 
index bf69094..2c48aca 100644 (file)
@@ -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;
index 1a89313..309c2ce 100644 (file)
@@ -91,7 +91,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
                        }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $clfrom = intval( $cont[0] );
-                       $clto = $this->getDB()->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $clto = $this->getDB()->addQuotes( $cont[1] );
                        $this->addWhere(
                                "cl_from $op $clfrom OR " .
                                "(cl_from = $clfrom AND " .
@@ -143,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;
                                }
 
@@ -164,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;
                                }
                        }
@@ -175,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;
                                }
 
index 963c809..e69ccbd 100644 (file)
@@ -164,7 +164,7 @@ 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 = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $db->addQuotes( $cont[1] );
                        $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
                        $op = ( $dir == 'newer' ? '>' : '<' );
                        $this->addWhere( "ar_namespace $op $ns OR " .
@@ -203,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 ) );
                                }
@@ -269,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 ) );
                                }
index 45b9fda..e05ffb6 100644 (file)
@@ -82,8 +82,8 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                        }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $db = $this->getDB();
-                       $orig = $db->addQuotes( $this->titleTokey( $cont[0] ) );
-                       $dup = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $orig = $db->addQuotes( $cont[0] );
+                       $dup = $db->addQuotes( $cont[1] );
                        $this->addWhere(
                                "i1.img_name $op $orig OR " .
                                "(i1.img_name = $orig AND " .
@@ -110,9 +110,7 @@ class ApiQueryDuplicateFiles 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',
-                                       $this->keyToTitle( $row->orig_name ) . '|' .
-                                       $this->keyToTitle( $row->dup_name ) );
+                               $this->setContinueEnumParameter( 'continue', $row->orig_name . '|' . $row->dup_name );
                                break;
                        }
                        if ( !is_null( $resultPageSet ) ) {
@@ -125,9 +123,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                                );
                                $fit = $this->addPageSubItem( $images[$row->orig_name], $r );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'continue',
-                                                       $this->keyToTitle( $row->orig_name ) . '|' .
-                                                       $this->keyToTitle( $row->dup_name ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->orig_name . '|' . $row->dup_name );
                                        break;
                                }
                        }
index 07d5b41..eed9d7c 100644 (file)
@@ -73,9 +73,23 @@ class ApiQueryFilearchive extends ApiQueryBase {
                $this->addFieldsIf( 'fa_metadata', $fld_metadata );
                $this->addFieldsIf( 'fa_bits', $fld_bitdepth );
 
+               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'] ) ) {
@@ -129,8 +143,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;
                        }
 
@@ -199,7 +212,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;
                        }
                }
@@ -210,6 +223,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
        public function getAllowedParams() {
                return array (
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'limit' => array(
@@ -251,6 +265,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',
@@ -345,6 +360,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' ),
                ) );
        }
 
index ebeb56a..c5012f0 100644 (file)
@@ -64,7 +64,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
                        $db = $this->getDB();
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $prefix = $db->addQuotes( $cont[0] );
-                       $title = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $db->addQuotes( $cont[1] );
                        $from = intval( $cont[2] );
                        $this->addWhere(
                                "iwl_prefix $op $prefix OR " .
index e32e287..30c7f5a 100644 (file)
@@ -66,7 +66,7 @@ class ApiQueryIWLinks extends ApiQueryBase {
                        $db = $this->getDB();
                        $iwlfrom = intval( $cont[0] );
                        $iwlprefix = $db->addQuotes( $cont[1] );
-                       $iwltitle = $db->addQuotes( $this->titleToKey( $cont[2] ) );
+                       $iwltitle = $db->addQuotes( $cont[2] );
                        $this->addWhere(
                                "iwl_from $op $iwlfrom OR " .
                                "(iwl_from = $iwlfrom AND " .
index f53d9a8..7184c88 100644 (file)
@@ -490,7 +490,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',
@@ -502,7 +502,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',
@@ -517,10 +518,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 ) ) )
                );
        }
 
@@ -531,7 +532,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",
index edada14..6052a75 100644 (file)
@@ -68,7 +68,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
                        }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $ilfrom = intval( $cont[0] );
-                       $ilto = $this->getDB()->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $ilto = $this->getDB()->addQuotes( $cont[1] );
                        $this->addWhere(
                                "il_from $op $ilfrom OR " .
                                "(il_from = $ilfrom AND " .
@@ -109,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;
                                }
                        }
@@ -129,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 );
index bc1c9c4..95c6bda 100644 (file)
@@ -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' );
@@ -280,7 +283,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;
@@ -346,7 +352,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] ) {
index c0a3cb4..3920407 100644 (file)
@@ -64,7 +64,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
                        $db = $this->getDB();
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $prefix = $db->addQuotes( $cont[0] );
-                       $title = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $db->addQuotes( $cont[1] );
                        $from = intval( $cont[2] );
                        $this->addWhere(
                                "ll_lang $op $prefix OR " .
index 7b675b0..e54e2e8 100644 (file)
@@ -119,7 +119,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $plfrom = intval( $cont[0] );
                        $plns = intval( $cont[1] );
-                       $pltitle = $this->getDB()->addQuotes( $this->titleToKey( $cont[2] ) );
+                       $pltitle = $this->getDB()->addQuotes( $cont[2] );
                        $this->addWhere(
                                "{$this->prefix}_from $op $plfrom OR " .
                                "({$this->prefix}_from = $plfrom AND " .
@@ -157,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();
@@ -166,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;
                                }
                        }
@@ -179,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 );
index fec76ec..8e01d3a 100644 (file)
@@ -257,9 +257,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
        protected function appendSpecialPageAliases( $property ) {
                global $wgContLang;
                $data = array();
-               $aliases = $wgContLang->getSpecialPageAliases();
-               foreach ( SpecialPageFactory::getList() as $specialpage => $stuff ) {
-                       $arr = array( 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] );
+               foreach ( $wgContLang->getSpecialPageAliases() as $specialpage => $aliases ) {
+                       $arr = array( 'realname' => $specialpage, 'aliases' => $aliases );
                        $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
                        $data[] = $arr;
                }
index 02484ae..a310d10 100644 (file)
@@ -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.",
index c7526df..3806375 100644 (file)
@@ -256,7 +256,7 @@ 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( '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 );
index 74543ce..6b24aef 100644 (file)
@@ -76,7 +76,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
                                        "original value returned by the previous query", "_badcontinue" );
                        }
                        $ns = intval( $cont[0] );
-                       $title = $this->getDB()->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $this->getDB()->addQuotes( $cont[1] );
                        $op = $params['dir'] == 'ascending' ? '>' : '<';
                        $this->addWhere(
                                "wl_namespace $op $ns OR " .
@@ -103,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 );
@@ -118,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 {
index 8e52467..dddbb85 100644 (file)
@@ -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() );
 
@@ -91,7 +91,7 @@ class ApiRollback extends ApiBase {
                                ApiBase::PARAM_REQUIRED => true
                        ),
                        'token' => null,
-                       'summary' => null,
+                       'summary' => '',
                        'markbot' => false,
                        'watchlist' => array(
                                ApiBase::PARAM_DFLT => 'preferences',
@@ -110,7 +110,7 @@ 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',
                );
index 61ead5a..e34771f 100644 (file)
@@ -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() );
@@ -104,7 +104,7 @@ class ApiUnblock extends ApiBase {
                                ApiBase::PARAM_DFLT => false,
                                ApiBase::PARAM_DEPRECATED => true,
                        ),
-                       'reason' => null,
+                       'reason' => '',
                );
        }
 
@@ -115,7 +115,7 @@ class ApiUnblock extends ApiBase {
                        '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 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',
                );
        }
 
index 23d540d..ffecc0e 100644 (file)
@@ -116,7 +116,7 @@ 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',
                );
index a9bcd9b..f215ebd 100644 (file)
@@ -32,7 +32,7 @@ class ProcessCacheLRU {
        protected $maxCacheKeys; // integer; max entries
 
        /**
-        * @param $maxEntries integer Maximum number of entries allowed (min 1).
+        * @param $maxKeys integer Maximum number of entries allowed (min 1).
         * @throws MWException When $maxCacheKeys is not an int or =< 0.
         */
        public function __construct( $maxKeys ) {
index a3faebf..cc4ffaf 100644 (file)
@@ -271,7 +271,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool|null previous value of the flag
         */
-       function debug( $debug = null ) {
+       public function debug( $debug = null ) {
                return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
        }
 
@@ -297,7 +297,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @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 {
@@ -316,7 +316,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 );
        }
 
@@ -329,7 +329,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @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 );
        }
 
@@ -338,7 +338,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @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 );
        }
 
@@ -347,7 +347,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @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 );
        }
 
@@ -360,7 +360,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 {
@@ -380,7 +380,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 {
@@ -393,7 +393,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $lag int
         */
-       function setFakeSlaveLag( $lag ) {
+       public function setFakeSlaveLag( $lag ) {
                $this->mFakeSlaveLag = $lag;
        }
 
@@ -402,7 +402,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $enabled bool
         */
-       function setFakeMaster( $enabled = true ) {
+       public function setFakeMaster( $enabled = true ) {
                $this->mFakeMaster = $enabled;
        }
 
@@ -411,7 +411,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function cascadingDeletes() {
+       public function cascadingDeletes() {
                return false;
        }
 
@@ -420,7 +420,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function cleanupTriggers() {
+       public function cleanupTriggers() {
                return false;
        }
 
@@ -430,7 +430,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function strictIPs() {
+       public function strictIPs() {
                return false;
        }
 
@@ -439,7 +439,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
        */
-       function realTimestamps() {
+       public function realTimestamps() {
                return false;
        }
 
@@ -448,7 +448,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function implicitGroupby() {
+       public function implicitGroupby() {
                return true;
        }
 
@@ -458,7 +458,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function implicitOrderby() {
+       public function implicitOrderby() {
                return true;
        }
 
@@ -478,7 +478,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function searchableIPs() {
+       public function searchableIPs() {
                return false;
        }
 
@@ -487,7 +487,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function functionalIndexes() {
+       public function functionalIndexes() {
                return false;
        }
 
@@ -495,7 +495,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;
        }
 
@@ -505,7 +505,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function doneWrites() {
+       public function doneWrites() {
                return $this->mDoneWrites;
        }
 
@@ -513,7 +513,7 @@ abstract class DatabaseBase implements DatabaseType {
         * Is a connection to the database open?
         * @return Boolean
         */
-       function isOpen() {
+       public function isOpen() {
                return $this->mOpened;
        }
 
@@ -529,7 +529,7 @@ 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 ) {
@@ -542,7 +542,7 @@ 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 ) {
@@ -556,7 +556,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 );
        }
 
@@ -567,14 +567,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 {
@@ -811,7 +811,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function isWriteQuery( $sql ) {
+       public function isWriteQuery( $sql ) {
                return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
        }
 
@@ -957,7 +957,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;
@@ -1013,7 +1013,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();
@@ -1060,7 +1060,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @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;
 
@@ -1147,7 +1147,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @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 ) ) {
@@ -1180,7 +1180,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @return Array
         * @see DatabaseBase::select()
         */
-       function makeSelectOptions( $options ) {
+       public function makeSelectOptions( $options ) {
                $preLimitTail = $postLimitTail = '';
                $startOpts = '';
 
@@ -1405,7 +1405,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 );
 
@@ -1426,7 +1426,9 @@ abstract class DatabaseBase implements DatabaseType {
         * @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 );
                }
@@ -1491,7 +1493,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @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;
@@ -1581,7 +1583,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;
@@ -1598,7 +1600,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;
@@ -1615,7 +1617,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 );
@@ -1630,7 +1632,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;
                }
@@ -1646,7 +1648,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function indexUnique( $table, $index ) {
+       public function indexUnique( $table, $index ) {
                $indexInfo = $this->indexInfo( $table, $index );
 
                if ( !$indexInfo ) {
@@ -1699,7 +1701,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;
@@ -1813,7 +1815,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' );
                }
@@ -1878,7 +1880,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @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 ) {
@@ -1905,7 +1907,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $field
         * @return string
         */
-       function bitNot( $field ) {
+       public function bitNot( $field ) {
                return "(~$field)";
        }
 
@@ -1914,7 +1916,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $fieldRight
         * @return string
         */
-       function bitAnd( $fieldLeft, $fieldRight ) {
+       public function bitAnd( $fieldLeft, $fieldRight ) {
                return "($fieldLeft & $fieldRight)";
        }
 
@@ -1923,7 +1925,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param  $fieldRight
         * @return string
         */
-       function bitOr( $fieldLeft, $fieldRight ) {
+       public function bitOr( $fieldLeft, $fieldRight ) {
                return "($fieldLeft | $fieldRight)";
        }
 
@@ -1936,7 +1938,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.
@@ -1947,14 +1949,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;
        }
 
@@ -1975,7 +1977,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
@@ -2209,7 +2211,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function addQuotes( $s ) {
+       public function addQuotes( $s ) {
                if ( $s === null ) {
                        return 'NULL';
                } else {
@@ -2301,7 +2303,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] ) ) {
@@ -2326,7 +2328,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return LikeMatch
         */
-       function anyChar() {
+       public function anyChar() {
                return new LikeMatch( '_' );
        }
 
@@ -2335,7 +2337,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return LikeMatch
         */
-       function anyString() {
+       public function anyString() {
                return new LikeMatch( '%' );
        }
 
@@ -2350,7 +2352,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $seqName string
         * @return null
         */
-       function nextSequenceValue( $seqName ) {
+       public function nextSequenceValue( $seqName ) {
                return null;
        }
 
@@ -2364,7 +2366,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $index
         * @return string
         */
-       function useIndexClause( $index ) {
+       public function useIndexClause( $index ) {
                return '';
        }
 
@@ -2390,7 +2392,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 ) {
@@ -2491,7 +2493,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 ) {
@@ -2518,7 +2520,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' );
@@ -2543,7 +2545,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 '';
        }
 
@@ -2557,7 +2559,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' );
                }
@@ -2598,7 +2600,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() )
        {
@@ -2656,7 +2658,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @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" );
                }
@@ -2680,7 +2682,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
        }
 
@@ -2692,7 +2694,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 ) . ')';
        }
@@ -2706,7 +2708,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $falseVal String: SQL expression to return if false
         * @return String: SQL fragment
         */
-       function conditional( $cond, $trueVal, $falseVal ) {
+       public function conditional( $cond, $trueVal, $falseVal ) {
                return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
        }
 
@@ -2720,7 +2722,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function strreplace( $orig, $old, $new ) {
+       public function strreplace( $orig, $old, $new ) {
                return "REPLACE({$orig}, {$old}, {$new})";
        }
 
@@ -2730,7 +2732,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return int
         */
-       function getServerUptime() {
+       public function getServerUptime() {
                return 0;
        }
 
@@ -2740,7 +2742,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasDeadlock() {
+       public function wasDeadlock() {
                return false;
        }
 
@@ -2750,7 +2752,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasLockTimeout() {
+       public function wasLockTimeout() {
                return false;
        }
 
@@ -2761,7 +2763,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasErrorReissuable() {
+       public function wasErrorReissuable() {
                return false;
        }
 
@@ -2771,7 +2773,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasReadOnlyError() {
+       public function wasReadOnlyError() {
                return false;
        }
 
@@ -2793,8 +2795,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function deadlockLoop() {
-
+       public function deadlockLoop() {
                $this->begin( __METHOD__ );
                $args = func_get_args();
                $function = array_shift( $args );
@@ -2846,7 +2847,7 @@ abstract class DatabaseBase implements DatabaseType {
         *   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 ) ) {
@@ -2879,7 +2880,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" );
@@ -2895,7 +2896,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 {
@@ -2908,7 +2909,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $fname string
         */
-       function begin( $fname = 'DatabaseBase::begin' ) {
+       public function begin( $fname = 'DatabaseBase::begin' ) {
                $this->query( 'BEGIN', $fname );
                $this->mTrxLevel = 1;
        }
@@ -2918,7 +2919,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $fname string
         */
-       function commit( $fname = 'DatabaseBase::commit' ) {
+       public function commit( $fname = 'DatabaseBase::commit' ) {
                if ( $this->mTrxLevel ) {
                        $this->query( 'COMMIT', $fname );
                        $this->mTrxLevel = 0;
@@ -2931,7 +2932,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $fname string
         */
-       function rollback( $fname = 'DatabaseBase::rollback' ) {
+       public function rollback( $fname = 'DatabaseBase::rollback' ) {
                if ( $this->mTrxLevel ) {
                        $this->query( 'ROLLBACK', $fname, true );
                        $this->mTrxLevel = 0;
@@ -2952,7 +2953,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(
@@ -2980,7 +2981,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function timestamp( $ts = 0 ) {
+       public function timestamp( $ts = 0 ) {
                return wfTimestamp( TS_MW, $ts );
        }
 
@@ -2997,7 +2998,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function timestampOrNull( $ts = null ) {
+       public function timestampOrNull( $ts = null ) {
                if ( is_null( $ts ) ) {
                        return null;
                } else {
@@ -3020,7 +3021,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 ) {
@@ -3050,7 +3051,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool Success or failure
         */
-       function ping() {
+       public function ping() {
                # Stub.  Not essential to override.
                return true;
        }
@@ -3064,7 +3065,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return int Database replication lag in seconds
         */
-       function getLag() {
+       public function getLag() {
                return intval( $this->mFakeSlaveLag );
        }
 
@@ -3085,7 +3086,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $b string
         * @return string
         */
-       function encodeBlob( $b ) {
+       public function encodeBlob( $b ) {
                return $b;
        }
 
@@ -3096,7 +3097,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $b string
         * @return string
         */
-       function decodeBlob( $b ) {
+       public function decodeBlob( $b ) {
                return $b;
        }
 
@@ -3137,7 +3138,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();
@@ -3189,7 +3192,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $vars bool|array mapping variable name to value.
         */
-       function setSchemaVars( $vars ) {
+       public function setSchemaVars( $vars ) {
                $this->mSchemaVars = $vars;
        }
 
index 8ce6e70..e1b3a9a 100644 (file)
@@ -810,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;
        }
 
index 763ca52..2b7521f 100644 (file)
@@ -1037,7 +1037,7 @@ __INDEXATTR__;
        /**
         * Return aggregated value function call
         */
-       function aggregateValue( $valuedata, $valuename = 'value' ) {
+       public function aggregateValue( $valuedata, $valuename = 'value' ) {
                return $valuedata;
        }
 
index 4c861e4..c953d77 100644 (file)
@@ -49,10 +49,10 @@ class FSFileBackend extends FileBackendStore {
        /**
         * @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.
+        *   - 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 );
index 9cf8f94..21261fa 100644 (file)
@@ -74,19 +74,19 @@ abstract class FileBackend {
         * 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.
+        *   - 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
@@ -103,7 +103,9 @@ abstract class FileBackend {
                        ? $config['lockManager']
                        : LockManagerGroup::singleton()->get( $config['lockManager'] );
                $this->fileJournal = isset( $config['fileJournal'] )
-                       ? FileJournal::factory( $config['fileJournal'], $this->name )
+                       ? ( ( $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']
@@ -223,26 +225,27 @@ abstract class FileBackend {
         * @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.
+        *   - 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'               : 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.
+        *   - 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).
         *
         * @remarks Remarks on locking:
         * File system paths given to operations should refer to files that are
@@ -256,15 +259,15 @@ abstract class FileBackend {
         * 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
+        *   - 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() ) {
+               if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                if ( empty( $opts['force'] ) ) { // sanity
@@ -388,6 +391,7 @@ abstract class FileBackend {
         *  - move
         *  - delete
         *  - null
+        *
         * a) Create a new file in storage with the contents of a string
         * @code
         *     array(
@@ -436,8 +440,11 @@ abstract class FileBackend {
         * @endcode
         *
         * @par Boolean flags for operations (operation-specific):
-        * 'ignoreMissingSource' : The operation will simply succeed and do
-        *                         nothing if the source file does not exist.
+        *   - ignoreMissingSource : The operation will simply succeed and do
+        *                           nothing if the source file does not exist.
+        *
+        * $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 occured
@@ -446,11 +453,12 @@ abstract class FileBackend {
         * considered "OK" as long as no fatal errors occured.
         *
         * @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 ) {
-               if ( $this->isReadOnly() ) {
+       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 ) {
@@ -573,15 +581,16 @@ abstract class FileBackend {
         * These flags should always be set for directories that have private files.
         *
         * $params include:
-        *     dir       : storage directory
-        *     noAccess  : try to deny file access (@since 1.20)
-        *     noListing : try to deny file listing (@since 1.20)
+        *   - 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)
         *
         * @param $params Array
         * @return Status
         */
        final public function prepare( array $params ) {
-               if ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doPrepare( $params );
@@ -601,13 +610,14 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *   - dir       : storage directory
-        *   - noAccess  : try to deny file access
-        *   - noListing : try to deny file listing
+        *   - 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 ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doSecure( $params );
@@ -626,16 +636,17 @@ abstract class FileBackend {
         * This essentially can undo the result of secure() calls.
         *
         * $params include:
-        *     dir     : storage directory
-        *     access  : try to allow file access
-        *     listing : try to allow file listing
+        *   - 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)
         *
         * @param $params Array
         * @return Status
         * @since 1.20
         */
        final public function publish( array $params ) {
-               if ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doPublish( $params );
@@ -653,12 +664,13 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     dir       : storage directory
-        *     recursive : recursively delete empty subdirectories first (@since 1.20)
+        *   - 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 ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doClean( $params );
@@ -675,8 +687,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return bool|null Returns null on failure
         */
        abstract public function fileExists( array $params );
@@ -686,8 +698,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - 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 );
@@ -698,8 +710,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return string|bool Returns false on failure
         */
        abstract public function getFileContents( array $params );
@@ -709,8 +721,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return integer|bool Returns false on failure
         */
        abstract public function getFileSize( array $params );
@@ -719,14 +731,14 @@ abstract class FileBackend {
         * 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)
+        *   - 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
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return Array|bool|null Returns null on failure
         */
        abstract public function getFileStat( array $params );
@@ -736,8 +748,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - 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 );
@@ -748,8 +760,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return Array
         */
        abstract public function getFileProps( array $params );
@@ -763,9 +775,9 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src     : source storage path
-        *     headers : additional HTTP headers to send on success
-        *     latest  : use the latest available data
+        *   - src     : source storage path
+        *   - headers : additional HTTP headers to send on success
+        *   - latest  : use the latest available data
         * @return Status
         */
        abstract public function streamFile( array $params );
@@ -785,8 +797,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return FSFile|null Returns null on failure
         */
        abstract public function getLocalReference( array $params );
@@ -798,8 +810,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return TempFSFile|null Returns null on failure
         */
        abstract public function getLocalCopy( array $params );
@@ -813,7 +825,7 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir : storage directory
+        *   - dir : storage directory
         * @return bool|null Returns null on failure
         * @since 1.20
         */
@@ -831,8 +843,8 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir     : storage directory
-        *     topOnly : only return direct child dirs of the directory
+        *   - dir     : storage directory
+        *   - topOnly : only return direct child dirs of the directory
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
@@ -846,7 +858,7 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir : storage directory
+        *   - dir : storage directory
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
@@ -866,8 +878,8 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir     : storage directory
-        *     topOnly : only return direct child files of the directory (@since 1.20)
+        *   - 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 );
@@ -880,7 +892,7 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir : storage directory
+        *   - dir : storage directory
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
index 2fc9d8e..0f67cf1 100644 (file)
@@ -48,22 +48,27 @@ class FileBackendMultiWrite extends FileBackend {
        /* 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 self::CHECK_SIZE and self::CHECK_TIME.
-        *                     The checks are done before allowing any file operations.
+        *   - 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.
         * @param $config Array
+        * @throws MWException
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
@@ -73,7 +78,7 @@ class FileBackendMultiWrite extends FileBackend {
                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 be this backend.
+                               // This should only be used when that config is used only by this backend.
                                $config = $config + FileBackendGroup::singleton()->config( $config['template'] );
                        }
                        $name = $config['name'];
@@ -81,17 +86,23 @@ class FileBackendMultiWrite extends FileBackend {
                                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 );
+                       // Alter certain sub-backend settings for sanity
+                       unset( $config['readOnly'] ); // use proxy backend setting
+                       unset( $config['fileJournal'] ); // use proxy backend journal
+                       $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->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.' );
@@ -108,27 +119,14 @@ class FileBackendMultiWrite extends FileBackend {
        final protected function doOperationsInternal( array $ops, array $opts ) {
                $status = Status::newGood();
 
-               $performOps = array(); // list of FileOp objects
-               $paths = array(); // storage paths read from or written to
-               // 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->getOperationsInternal( $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.
-                               $paths = $backend->getPathsToLockForOpsInternal( $performOps );
-                               // Get the paths under the proxy backend's name
-                               $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
-                               $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
-                       }
-               }
+               $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...
@@ -138,46 +136,29 @@ class FileBackendMultiWrite extends FileBackend {
                                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( $paths['sh'], $paths['ex'] ) ) );
-                       if ( !$status->isOK() ) {
-                               return $status; // abort
+               $status->merge( $this->consistencyCheck( array_merge( $paths['sh'], $paths['ex'] ) ) );
+               if ( !$status->isOK() ) {
+                       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...
+               foreach ( $this->backends as $index => $backend ) {
+                       if ( $index !== $this->masterIndex ) { // not done already
+                               $realOps = $this->substOpBatchPaths( $ops, $backend );
+                               $status->merge( $backend->doOperations( $realOps, $opts ) );
                        }
                }
-
-               // Actually attempt the operation batch...
-               $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
-
-               $success = array();
-               $failCount = 0;
-               $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()
+               $status->success = $masterStatus->success;
+               $status->successCount = $masterStatus->successCount;
+               $status->failCount = $masterStatus->failCount;
 
                return $status;
        }
@@ -190,21 +171,29 @@ class FileBackendMultiWrite extends FileBackend {
         */
        public function consistencyCheck( array $paths ) {
                $status = Status::newGood();
-               if ( $this->syncChecks == 0 ) {
+               if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
                        return $status; // skip checks
                }
 
                $mBackend = $this->backends[$this->masterIndex];
                foreach ( array_unique( $paths ) as $path ) {
                        $params = array( 'src' => $path, 'latest' => true );
+                       $mParams = $this->substOpPaths( $params, $mBackend );
                        // Stat the file on the 'master' backend
-                       $mStat = $mBackend->getFileStat( $this->substOpPaths( $params, $mBackend ) );
+                       $mStat = $mBackend->getFileStat( $mParams );
+                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
+                               $mSha1 = $mBackend->getFileSha1( $mParams );
+                       } else {
+                               $mSha1 = false;
+                       }
+                       $mUsable = $mBackend->isPathUsableInternal( $mParams['src'] );
                        // 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 ) );
+                               $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 );
@@ -224,11 +213,20 @@ class FileBackendMultiWrite extends FileBackend {
                                                        continue;
                                                }
                                        }
+                                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
+                                               if ( $cBackend->getFileSha1( $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 );
                                        }
                                }
+                               if ( $mUsable !== $cBackend->isPathUsableInternal( $cParams['src'] ) ) {
+                                       $status->fatal( 'backend-fail-synced', $path );
+                               }
                        }
                }
 
@@ -302,10 +300,12 @@ class FileBackendMultiWrite extends FileBackend {
         * @see FileBackend::doQuickOperationsInternal()
         * @return Status
         */
-       public function doQuickOperationsInternal( array $ops ) {
+       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] );
-               $status = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
+               $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
+               $status->merge( $masterStatus );
                // Propagate the operations to the clone backends...
                foreach ( $this->backends as $index => $backend ) {
                        if ( $index !== $this->masterIndex ) { // not done already
@@ -313,6 +313,12 @@ class FileBackendMultiWrite extends FileBackend {
                                $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;
        }
 
index 49bb039..852a653 100644 (file)
@@ -86,10 +86,10 @@ abstract class FileBackendStore extends FileBackend {
         * 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
-        *     async         : Status will be returned immediately if supported.
+        *   - content       : the raw file contents
+        *   - dst           : destination storage path
+        *   - overwrite     : overwrite any file that exists at 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.
         *
@@ -122,10 +122,10 @@ abstract class FileBackendStore extends FileBackend {
         * 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
-        *     async         : Status will be returned immediately if supported.
+        *   - src           : source path on disk
+        *   - dst           : destination storage path
+        *   - overwrite     : overwrite any file that exists at 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.
         *
@@ -158,10 +158,10 @@ abstract class FileBackendStore extends FileBackend {
         * 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
-        *     async         : Status will be returned immediately if supported.
+        *   - src           : source storage path
+        *   - dst           : destination storage path
+        *   - overwrite     : overwrite any file that exists at 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.
         *
@@ -189,9 +189,9 @@ abstract class FileBackendStore extends FileBackend {
         * 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.
+        *   - 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.
         *
@@ -219,10 +219,10 @@ abstract class FileBackendStore extends FileBackend {
         * 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
-        *     async         : Status will be returned immediately if supported.
+        *   - src           : source storage path
+        *   - dst           : destination storage path
+        *   - overwrite     : overwrite any file that exists at 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.
         *
@@ -966,7 +966,7 @@ abstract class FileBackendStore extends FileBackend {
         * @see FileBackend::doOperationsInternal()
         * @return Status
         */
-       protected function doOperationsInternal( array $ops, array $opts ) {
+       final protected function doOperationsInternal( array $ops, array $opts ) {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
index ac2496d..fa87c3a 100644 (file)
@@ -432,10 +432,10 @@ abstract class FileOp {
 /**
  * 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
+ *   - 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 {
        /**
@@ -509,10 +509,10 @@ class StoreFileOp extends FileOp {
 /**
  * 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
+ *   - 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
  */
 class CreateFileOp extends FileOp {
        protected function allowedParams() {
@@ -572,10 +572,10 @@ class CreateFileOp extends FileOp {
 /**
  * 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
+ *   - 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
  */
 class CopyFileOp extends FileOp {
        /**
@@ -643,10 +643,10 @@ class CopyFileOp extends FileOp {
 /**
  * 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
+ *   - 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
  */
 class MoveFileOp extends FileOp {
        /**
@@ -720,8 +720,8 @@ class MoveFileOp extends FileOp {
 /**
  * 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
+ *   - src                 : source storage path
+ *   - ignoreMissingSource : don't return an error if the file does not exist
  */
 class DeleteFileOp extends FileOp {
        /**
index d22a2ec..fd83e2c 100644 (file)
@@ -40,17 +40,17 @@ class FileOpBatch {
         * 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.
+        *   - 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 occured and 'force' was not set
+        *   - 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
index d2f26e9..94ddb44 100644 (file)
@@ -56,25 +56,25 @@ class SwiftFileBackend extends FileBackendStore {
        /**
         * @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")
+        *   - 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")
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
@@ -1052,15 +1052,16 @@ class SwiftFileBackend extends FileBackendStore {
         *
         * $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.
+        *   - 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
+        *   - account:user       : Grants access if the request is by the given user
         *
         * @see http://swift.openstack.org/misc.html#acls
         *
index 79696b2..9e81dbf 100644 (file)
@@ -92,7 +92,7 @@ class MemcLockManager extends QuorumLockManager {
                $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
                $this->lockExpiry = $met ? 2*(int)$met : 2*3600;
 
-               $this->session = wfRandomString( 31 );
+               $this->session = wfRandomString( 32 );
        }
 
        /**
@@ -264,11 +264,24 @@ class MemcLockManager extends QuorumLockManager {
        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 );
index 12a84a1..d87f294 100644 (file)
@@ -558,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.
         *
index 93f3f83..a7c803d 100644 (file)
@@ -509,11 +509,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()
                                );
                        }
index 9342374..0ae9a05 100644 (file)
@@ -309,7 +309,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.
@@ -939,8 +939,9 @@ 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 ) {
                if ( $lang === false && $default === false) {
@@ -1019,10 +1020,8 @@ class FormatMetadata {
         * 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 float|int 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 ) {
@@ -1113,7 +1112,7 @@ class FormatMetadata {
         * a string, not an int.
         *
         * @param $val String: The 8 digit news code.
-        * @return srting The human readable form
+        * @return string The human readable form
         */
        static private function convertNewsCode( $val ) {
                if ( !preg_match( '/^\d{8}$/D', $val ) ) {
@@ -1185,7 +1184,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
         */
@@ -1195,17 +1194,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';
                        }
                }
@@ -1354,12 +1350,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 );
        }
 }
index 02950f0..d9356b4 100644 (file)
@@ -457,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(
@@ -473,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 );
@@ -541,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;
index 1c45dc4..a2db52f 100644 (file)
@@ -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
         */
index 5aa2b49..74ed537 100644 (file)
@@ -71,7 +71,7 @@ class SpecialProtectedpages extends SpecialPage {
        /**
         * Callback function to output a restriction
         * @param Title $row Protected title
-        * @return string Formatted <li> element
+        * @return string Formatted "<li>" element
         */
        public function formatRow( $row ) {
                wfProfileIn( __METHOD__ );
index 5a71ccb..51c2d0f 100644 (file)
@@ -108,6 +108,7 @@ 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',
+                       'Timo Tijhof',
                        wfMsg( 'version-poweredby-others' )
                );
 
index 6346c7a..00687eb 100644 (file)
@@ -322,7 +322,6 @@ $1',
 'versionrequiredtext' => 'Был бит менән эшләү өсөн MediaWiki-ның $1 версияһы кәрәк. [[Special:Version|Ҡулланылған версия тураһында мәғлүмәт битен]] ҡара.',
 
 'ok' => 'Тамам',
-'pagetitle' => '{{SITENAME}} проектынан',
 'retrievedfrom' => 'Сығанағы — «$1»',
 'youhavenewmessages' => 'Яңы $1 бар ($2).',
 'newmessageslink' => 'яңы хәбәр',
index 6cd98ab..cc05670 100644 (file)
@@ -269,8 +269,6 @@ $1',
 Ẓr [[Special:Version|ayyaw tasna]].',
 
 'ok' => 'Waxxa',
-'pagetitle' => '(MediaWiki)$1 - {{SITENAME}}',
-'pagetitle-view-mainpage' => '{{SITENAME}}',
 'retrievedfrom' => 'Yurrid z "$1"',
 'youhavenewmessages' => 'Illa dark $1 ($2).',
 'newmessageslink' => 'Tibratin timaynutin',
index 548a33b..498bc6b 100644 (file)
@@ -90,6 +90,8 @@ class CopyFileBackend extends Maintenance {
                                // Only copy the missing files over in the next loop
                                $srcPathsRel = array_diff( $relFilesSrc, $relFilesDst );
                                $this->output( count( $srcPathsRel ) . " file(s) need to be copied.\n" );
+                               unset( $relFilesSrc );
+                               unset( $relFilesDst );
                        }
 
                        $batchPaths = array();
@@ -141,7 +143,7 @@ class CopyFileBackend extends Maintenance {
                        }
                        $fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
                        // Note: prepare() is usually fast for key/value backends
-                       $status = $dst->prepare( array( 'dir' => dirname( $dstPath ) ) );
+                       $status = $dst->prepare( array( 'dir' => dirname( $dstPath ), 'bypassReadOnly' => 1 ) );
                        if ( !$status->isOK() ) {
                                $this->error( print_r( $status->getErrorsArray(), true ) );
                                $this->error( "Could not copy $srcPath to $dstPath.", 1 ); // die
@@ -152,10 +154,10 @@ class CopyFileBackend extends Maintenance {
                }
 
                $t_start = microtime( true );
-               $status = $dst->doQuickOperations( $ops );
+               $status = $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) );
                if ( !$status->isOK() ) {
                        sleep( 10 ); // wait and retry copy again
-                       $status = $dst->doQuickOperations( $ops );
+                       $status = $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) );
                }
                $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( !$status->isOK() ) {
index 8669fe3..6a9baa8 100644 (file)
@@ -33,6 +33,7 @@ class MergeMessageFileList extends Maintenance {
        function __construct() {
                parent::__construct();
                $this->addOption( 'list-file', 'A file containing a list of extension setup files, one per line.', true, true );
+               $this->addOption( 'extensions-dir', 'Path where extensions can be found.', false, true );
                $this->addOption( 'output', 'Send output to this file (omit for stdout)', false, true );
                $this->mDescription = 'Merge $wgExtensionMessagesFiles from various extensions to produce a ' .
                        'single array containing all message files.';
@@ -41,11 +42,36 @@ class MergeMessageFileList extends Maintenance {
        public function execute() {
                global $mmfl;
 
+               # Add setup files contained in file passed to --list-file
                $lines = file( $this->getOption( 'list-file' ) );
                if ( $lines === false ) {
                        $this->error( 'Unable to open list file.' );
                }
                $mmfl = array( 'setupFiles' => array_map( 'trim', $lines ) );
+
+               # Now find out files in a directory
+               $hasError = false;
+               if ( $this->hasOption( 'extensions-dir' ) ) {
+                       $extdir = $this->getOption( 'extensions-dir' );
+                       $entries = scandir( $extdir );
+                       foreach( $entries as $extname ) {
+                               if ( $extname == '.' || $extname == '..' || !is_dir( "$extdir/$extname" ) ) {
+                                       continue;
+                               }
+                               $extfile = "{$extdir}/{$extname}/{$extname}.php";
+                               if ( file_exists( $extfile ) ) {
+                                       $mmfl['setupFiles'][] = $extfile;
+                               } else {
+                                       $hasError = true;
+                                       $this->error( "Extension {$extname} in {$extdir} lacks expected {$extname}.php" );
+                               }
+                       }
+               }
+
+               if ( $hasError ) {
+                       $this->error( "Some files are missing (see above). Giving up.", 1 );
+               }
+
                if ( $this->hasOption( 'output' ) ) {
                        $mmfl['output'] = $this->getOption( 'output' );
                }
index 1af33e6..c4ba66e 100644 (file)
@@ -185,7 +185,8 @@ class SyncFileBackend extends Maintenance {
                                }
                                $fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
                                // Note: prepare() is usually fast for key/value backends
-                               $status->merge( $dst->prepare( array( 'dir' => dirname( $dPath ) ) ) );
+                               $status->merge( $dst->prepare( array(
+                                       'dir' => dirname( $dPath ), 'bypassReadOnly' => 1 ) ) );
                                if ( !$status->isOK() ) {
                                        return $status;
                                }
@@ -201,8 +202,7 @@ class SyncFileBackend extends Maintenance {
                }
 
                $t_start = microtime( true );
-               $status->merge( $dst->doOperations( $ops,
-                       array( 'nonLocking' => 1, 'nonJournaled' => 1 ) ) );
+               $status->merge( $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) ) );
                $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( $status->isOK() && $this->getOption( 'verbose' ) ) {
                        $this->output( "Synchronized these file(s) [{$ellapsed_ms}ms]:\n" .
index fd425fe..2363665 100644 (file)
@@ -32,6 +32,8 @@ class UpdateCollation extends Maintenance {
        const BATCH_SIZE = 50; // Number of rows to process in one batch
        const SYNC_INTERVAL = 20; // Wait for slaves after this many batches
 
+       var $sizeHistogram = array();
+
        public function __construct() {
                parent::__construct();
 
@@ -50,6 +52,13 @@ TEXT;
                        'categorylinks table is large. This will only update rows with that ' .
                        'collation, though, so it may miss out-of-date rows with a different, ' .
                        'even older collation.', false, true );
+               $this->addOption( 'target-collation', 'Set this to the new collation type to ' .
+                       'use instead of $wgCategoryCollation. Usually you should not use this, ' . 
+                       'you should just update $wgCategoryCollation in LocalSettings.php.', 
+                       false, true );
+               $this->addOption( 'dry-run', 'Don\'t actually change the collations, just ' .
+                       'compile statistics.' );
+               $this->addOption( 'verbose-stats', 'Show more statistics.' );
        }
 
        public function execute() {
@@ -57,10 +66,19 @@ TEXT;
 
                $dbw = $this->getDB( DB_MASTER );
                $force = $this->getOption( 'force' );
+               $dryRun = $this->getOption( 'dry-run' );
+               $verboseStats = $this->getOption( 'verbose-stats' );
+               if ( $this->hasOption( 'target-collation' ) ) {
+                       $collationName = $this->getOption( 'target-collation' );
+                       $collation = Collation::factory( $collationName );
+               } else {
+                       $collationName = $wgCategoryCollation;
+                       $collation = Collation::singleton();
+               }
 
                $options = array( 'LIMIT' => self::BATCH_SIZE, 'STRAIGHT_JOIN' );
 
-               if ( $force ) {
+               if ( $force || $dryRun ) {
                        $options['ORDER BY'] = 'cl_from, cl_to';
                        $collationConds = array();
                } else {
@@ -68,7 +86,7 @@ TEXT;
                                $collationConds['cl_collation'] = $this->getOption( 'previous-collation' );
                        } else {
                                $collationConds = array( 0 =>
-                                       'cl_collation != ' . $dbw->addQuotes( $wgCategoryCollation )
+                                       'cl_collation != ' . $dbw->addQuotes( $collationName )
                                );
                        }
 
@@ -82,7 +100,7 @@ TEXT;
                        } else {
                                $count = $dbw->estimateRowCount(
                                        'categorylinks',
-                                       '',
+                                       '*',
                                        $collationConds,
                                        __METHOD__
                                );
@@ -110,7 +128,9 @@ TEXT;
                        );
                        $this->output( " processing..." );
 
-                       $dbw->begin( __METHOD__ );
+                       if ( !$dryRun ) {
+                               $dbw->begin( __METHOD__ );
+                       }
                        foreach ( $res as $row ) {
                                $title = Title::newFromRow( $row );
                                if ( !$row->cl_collation ) {
@@ -135,23 +155,32 @@ TEXT;
                                } else {
                                        $type = 'page';
                                }
-                               $dbw->update(
-                                       'categorylinks',
-                                       array(
-                                               'cl_sortkey' => Collation::singleton()->getSortKey(
-                                                       $title->getCategorySortkey( $prefix ) ),
-                                               'cl_sortkey_prefix' => $prefix,
-                                               'cl_collation' => $wgCategoryCollation,
-                                               'cl_type' => $type,
-                                               'cl_timestamp = cl_timestamp',
-                                       ),
-                                       array( 'cl_from' => $row->cl_from, 'cl_to' => $row->cl_to ),
-                                       __METHOD__
-                               );
+                               $newSortKey = $collation->getSortKey(
+                                       $title->getCategorySortkey( $prefix ) );
+                               if ( $verboseStats ) {
+                                       $this->updateSortKeySizeHistogram( $newSortKey );
+                               }
+
+                               if ( !$dryRun ) {
+                                       $dbw->update(
+                                               'categorylinks',
+                                               array(
+                                                       'cl_sortkey' => $newSortKey,
+                                                       'cl_sortkey_prefix' => $prefix,
+                                                       'cl_collation' => $collationName,
+                                                       'cl_type' => $type,
+                                                       'cl_timestamp = cl_timestamp',
+                                               ),
+                                               array( 'cl_from' => $row->cl_from, 'cl_to' => $row->cl_to ),
+                                               __METHOD__
+                                       );
+                               }
+                       }
+                       if ( !$dryRun ) {
+                               $dbw->commit( __METHOD__ );
                        }
-                       $dbw->commit( __METHOD__ );
 
-                       if ( $force && $row ) {
+                       if ( ( $force || $dryRun ) && $row ) {
                                $encFrom = $dbw->addQuotes( $row->cl_from );
                                $encTo = $dbw->addQuotes( $row->cl_to );
                                $batchConds = array(
@@ -162,12 +191,83 @@ TEXT;
                        $count += $res->numRows();
                        $this->output( "$count done.\n" );
 
-                       if ( ++$batchCount % self::SYNC_INTERVAL == 0 ) {
+                       if ( !$dryRun && ++$batchCount % self::SYNC_INTERVAL == 0 ) {
                                $this->output( "Waiting for slaves ... " );
                                wfWaitForSlaves();
                                $this->output( "done\n" );
                        }
                } while ( $res->numRows() == self::BATCH_SIZE );
+
+               $this->output( "$count rows processed\n" );
+
+               if ( $verboseStats ) {
+                       $this->output( "\n" );
+                       $this->showSortKeySizeHistogram();
+               }
+       }
+
+       function updateSortKeySizeHistogram( $key ) {
+               $length = strlen( $key );
+               if ( !isset( $this->sizeHistogram[$length] ) ) {
+                       $this->sizeHistogram[$length] = 0;
+               }
+               $this->sizeHistogram[$length]++;
+       }
+
+       function showSortKeySizeHistogram() {
+               $maxLength = max( array_keys( $this->sizeHistogram ) );
+               if ( $maxLength == 0 ) {
+                       return;
+               }
+               $numBins = 20;
+               $coarseHistogram = array_fill( 0, $numBins, 0 );
+               $coarseBoundaries = array();
+               $boundary = 0;
+               for ( $i = 0; $i < $numBins - 1; $i++ ) {
+                       $boundary += $maxLength / $numBins;
+                       $coarseBoundaries[$i] = round( $boundary );
+               }
+               $coarseBoundaries[$numBins - 1] = $maxLength + 1;
+               $raw = '';
+               for ( $i = 0; $i <= $maxLength; $i++ ) {
+                       if ( $raw !== '' ) {
+                               $raw .= ', ';
+                       }
+                       if ( !isset( $this->sizeHistogram[$i] ) ) {
+                               $val = 0;
+                       } else {
+                               $val = $this->sizeHistogram[$i];
+                       }
+                       for ( $coarseIndex = 0; $coarseIndex < $numBins - 1; $coarseIndex++ ) {
+                               if ( $coarseBoundaries[$coarseIndex] > $i ) {
+                                       $coarseHistogram[$coarseIndex] += $val;
+                                       break;
+                               }
+                       }
+                       if ( $coarseIndex == $numBins - 1 ) {
+                               $coarseHistogram[$coarseIndex] += $val;
+                       }
+                       $raw .= $val;
+               }
+
+               $this->output( "Sort key size histogram\nRaw data: $raw\n\n" );
+
+               $maxBinVal = max( $coarseHistogram );
+               $scale = 60 / $maxBinVal;
+               $prevBoundary = 0;
+               for ( $coarseIndex = 0; $coarseIndex < $numBins; $coarseIndex++ ) {
+                       if ( !isset( $coarseHistogram[$coarseIndex] ) ) {
+                               $val = 0;
+                       } else {
+                               $val = $coarseHistogram[$coarseIndex];
+                       }
+                       $boundary = $coarseBoundaries[$coarseIndex];
+                       $this->output( sprintf( "%-10s %-10d |%s\n",
+                               $prevBoundary . '-' . ( $boundary - 1 ) . ': ',
+                               $val,
+                               str_repeat( '*', $scale * $val ) ) );
+                       $prevBoundary = $boundary;
+               }
        }
 }
 
index 783f031..6dd8ea3 100644 (file)
@@ -10,6 +10,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase {
        public function setUp() {
                global $wgLanguageCode, $wgLang, $wgContLang;
 
+               parent::setUp();
+
                self::$oldLang = $wgLang;
                self::$oldContLang = $wgContLang;
 
@@ -23,6 +25,7 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase {
 
                $wgContLang = $wgLang = Language::factory( $wgLanguageCode );
                MessageCache::singleton()->disable();
+
        }
 
        public function tearDown() {
@@ -32,6 +35,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase {
                $wgContLang = self::$oldContLang;
                $wgLanguageCode = $wgContLang->getCode();
                self::$oldContLang = self::$oldLang = null;
+
+               parent::tearDown();
        }
 
 }
diff --git a/tests/phpunit/docs/ExportDemoTest.php b/tests/phpunit/docs/ExportDemoTest.php
new file mode 100644 (file)
index 0000000..ce65d49
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Test for the demo xml
+ *
+ * @group Dump
+ */
+class ExportDemoTest extends DumpTestCase {
+
+       /**
+        * @group large
+        */
+       function testExportDemo() {
+               $this->validateXmlFileAgainstXsd( "../../docs/export-demo.xml" );
+       }
+
+       /**
+        * Validates a xml file against the xsd.
+        *
+        * The validation is slow, because php has to read the xsd on each call.
+        *
+        * @param $fname string: name of file to validate
+        */
+       protected function validateXmlFileAgainstXsd( $fname ) {
+               $version = WikiExporter::schemaVersion();
+
+               $dom = new DomDocument();
+               $dom->load( $fname );
+
+               try {
+                       $this->assertTrue( $dom->schemaValidate( "../../docs/export-" . $version . ".xsd" ),
+                               "schemaValidate has found an error" );
+               } catch( Exception $e ) {
+                       $this->fail( "xml not valid against xsd: " . $e->getMessage() );
+               }
+       }
+}
index 61507f5..6fb7ace 100644 (file)
@@ -243,6 +243,8 @@ class FileBackendTest extends MediaWikiTestCase {
                $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $dest ) );
        }
 
        public function provider_testStore() {
@@ -330,6 +332,8 @@ class FileBackendTest extends MediaWikiTestCase {
                $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $source, $dest ) );
        }
 
        public function provider_testCopy() {
@@ -419,6 +423,8 @@ class FileBackendTest extends MediaWikiTestCase {
                        "Source file does not exist accourding to props ($backendName)." );
                $this->assertEquals( true, $props2['fileExists'],
                        "Destination file exists accourding to props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $source, $dest ) );
        }
 
        public function provider_testMove() {
@@ -504,6 +510,8 @@ class FileBackendTest extends MediaWikiTestCase {
                $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
                $this->assertFalse( $props1['fileExists'],
                        "Source file $source does not exist according to props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $source ) );
        }
 
        public function provider_testDelete() {
@@ -595,6 +603,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                $this->backend->getFileSize( array( 'src' => $dest ) ),
                                "Destination file $dest has original size according to props ($backendName)." );
                }
+
+               $this->assertBackendPathsConsistent( array( $dest ) );
        }
 
        /**
@@ -1822,6 +1832,13 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) );
        }
 
+       function assertBackendPathsConsistent( array $paths ) {
+               if ( $this->backend instanceof FileBackendMultiWrite ) {
+                       $status = $this->backend->consistencyCheck( $paths );
+                       $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
+               }
+       }
+
        function assertGoodStatus( $status, $msg ) {
                $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg );
        }
index a1a6d59..bef7677 100644 (file)
@@ -24,43 +24,195 @@ class LanguageTest extends MediaWikiTestCase {
                );
        }
 
-       /** @dataProvider provideFormattableTimes */
+       /**
+        * @dataProvider provideFormattableTimes
+        */
        function testFormatTimePeriod( $seconds, $format, $expected, $desc ) {
                $this->assertEquals( $expected, $this->lang->formatTimePeriod( $seconds, $format ), $desc );
        }
 
        function provideFormattableTimes() {
                return array(
-                       array( 9.45, array(), '9.5s', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 9.45, array( 'noabbrevs' => true ), '9.5 seconds', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 9.95, array(), '10s', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 9.95, array( 'noabbrevs' => true ), '10 seconds', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 59.55, array(), '1m 0s', 'formatTimePeriod() rounding (<60s)' ),
-                       array( 59.55, array( 'noabbrevs' => true ), '1 minute 0 seconds', 'formatTimePeriod() rounding (<60s)' ),
-                       array( 119.55, array(), '2m 0s', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 119.55, array( 'noabbrevs' => true ), '2 minutes 0 seconds', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 3599.55, array(), '1h 0m 0s', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 3599.55, array( 'noabbrevs' => true ), '1 hour 0 minutes 0 seconds', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 7199.55, array(), '2h 0m 0s', 'formatTimePeriod() rounding (>=1h)' ),
-                       array( 7199.55, array( 'noabbrevs' => true ), '2 hours 0 minutes 0 seconds', 'formatTimePeriod() rounding (>=1h)' ),
-                       array( 7199.55, 'avoidseconds', '2h 0m', 'formatTimePeriod() rounding (>=1h), avoidseconds' ),
-                       array( 7199.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 hours 0 minutes', 'formatTimePeriod() rounding (>=1h), avoidseconds' ),
-                       array( 7199.55, 'avoidminutes', '2h 0m', 'formatTimePeriod() rounding (>=1h), avoidminutes' ),
-                       array( 7199.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '2 hours 0 minutes', 'formatTimePeriod() rounding (>=1h), avoidminutes' ),
-                       array( 172799.55, 'avoidseconds', '48h 0m', 'formatTimePeriod() rounding (=48h), avoidseconds' ),
-                       array( 172799.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '48 hours 0 minutes', 'formatTimePeriod() rounding (=48h), avoidseconds' ),
-                       array( 259199.55, 'avoidminutes', '3d 0h', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 259199.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '3 days 0 hours', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 176399.55, 'avoidseconds', '2d 1h 0m', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 176399.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 days 1 hour 0 minutes', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 176399.55, 'avoidminutes', '2d 1h', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 176399.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '2 days 1 hour', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 259199.55, 'avoidseconds', '3d 0h 0m', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 259199.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '3 days 0 hours 0 minutes', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 172801.55, 'avoidseconds', '2d 0h 0m', 'formatTimePeriod() rounding, (>48h), avoidseconds' ),
-                       array( 172801.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 days 0 hours 0 minutes', 'formatTimePeriod() rounding, (>48h), avoidseconds' ),
-                       array( 176460.55, array(), '2d 1h 1m 1s', 'formatTimePeriod() rounding, recursion, (>48h)' ),
-                       array( 176460.55, array( 'noabbrevs' => true ), '2 days 1 hour 1 minute 1 second', 'formatTimePeriod() rounding, recursion, (>48h)' ),
+                       array(
+                               9.45,
+                               array(),
+                               '9.5s',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               9.45,
+                               array( 'noabbrevs' => true ),
+                               '9.5 seconds',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               9.95,
+                               array(),
+                               '10s',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               9.95,
+                               array( 'noabbrevs' => true ),
+                               '10 seconds',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               59.55,
+                               array(),
+                               '1m 0s',
+                               'formatTimePeriod() rounding (<60s)'
+                       ),
+                       array(
+                               59.55,
+                               array( 'noabbrevs' => true ),
+                               '1 minute 0 seconds',
+                               'formatTimePeriod() rounding (<60s)'
+                       ),
+                       array(
+                               119.55,
+                               array(),
+                               '2m 0s',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               119.55,
+                               array( 'noabbrevs' => true ),
+                               '2 minutes 0 seconds',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               3599.55,
+                               array(),
+                               '1h 0m 0s',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               3599.55,
+                               array( 'noabbrevs' => true ),
+                               '1 hour 0 minutes 0 seconds',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               7199.55,
+                               array(),
+                               '2h 0m 0s',
+                               'formatTimePeriod() rounding (>=1h)'
+                       ),
+                       array(
+                               7199.55,
+                               array( 'noabbrevs' => true ),
+                               '2 hours 0 minutes 0 seconds',
+                               'formatTimePeriod() rounding (>=1h)'
+                       ),
+                       array(
+                               7199.55,
+                               'avoidseconds',
+                               '2h 0m',
+                               'formatTimePeriod() rounding (>=1h), avoidseconds'
+                       ),
+                       array(
+                               7199.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '2 hours 0 minutes',
+                               'formatTimePeriod() rounding (>=1h), avoidseconds'
+                       ),
+                       array(
+                               7199.55,
+                               'avoidminutes',
+                               '2h 0m',
+                               'formatTimePeriod() rounding (>=1h), avoidminutes'
+                       ),
+                       array(
+                               7199.55,
+                               array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+                               '2 hours 0 minutes',
+                               'formatTimePeriod() rounding (>=1h), avoidminutes'
+                       ),
+                       array(
+                               172799.55,
+                               'avoidseconds',
+                               '48h 0m',
+                               'formatTimePeriod() rounding (=48h), avoidseconds'
+                       ),
+                       array(
+                               172799.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '48 hours 0 minutes',
+                               'formatTimePeriod() rounding (=48h), avoidseconds'
+                       ),
+                       array(
+                               259199.55,
+                               'avoidminutes',
+                               '3d 0h',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               259199.55,
+                               array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+                               '3 days 0 hours',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               176399.55,
+                               'avoidseconds',
+                               '2d 1h 0m',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               176399.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '2 days 1 hour 0 minutes',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               176399.55,
+                               'avoidminutes',
+                               '2d 1h',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               176399.55,
+                               array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+                               '2 days 1 hour',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               259199.55,
+                               'avoidseconds',
+                               '3d 0h 0m',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               259199.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '3 days 0 hours 0 minutes',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               172801.55,
+                               'avoidseconds',
+                               '2d 0h 0m',
+                               'formatTimePeriod() rounding, (>48h), avoidseconds'
+                       ),
+                       array(
+                               172801.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '2 days 0 hours 0 minutes',
+                               'formatTimePeriod() rounding, (>48h), avoidseconds'
+                       ),
+                       array(
+                               176460.55,
+                               array(),
+                               '2d 1h 1m 1s',
+                               'formatTimePeriod() rounding, recursion, (>48h)'
+                       ),
+                       array(
+                               176460.55,
+                               array( 'noabbrevs' => true ),
+                               '2 days 1 hour 1 minute 1 second',
+                               'formatTimePeriod() rounding, recursion, (>48h)'
+                       ),
                );
 
        }
@@ -98,8 +250,8 @@ class LanguageTest extends MediaWikiTestCase {
        }
 
        /**
-       * @dataProvider provideHTMLTruncateData()
-       */
+        * @dataProvider provideHTMLTruncateData()
+        */
        function testTruncateHtml( $len, $ellipsis, $input, $expected ) {
                // Actual HTML...
                $this->assertEquals(
@@ -792,16 +944,16 @@ class LanguageTest extends MediaWikiTestCase {
                );
        }
 
-       /**\r
-        * @dataProvider provideCheckTitleEncodingData\r
-        */\r
-       function testCheckTitleEncoding( $s ) {\r
-               $this->assertEquals(\r
-                       $s,\r
-                       $this->lang->checkTitleEncoding($s),\r
-                       "checkTitleEncoding('$s')"\r
-               );\r
-       }\r
+       /**
+        * @dataProvider provideCheckTitleEncodingData
+        */
+       function testCheckTitleEncoding( $s ) {
+               $this->assertEquals(
+                       $s,
+                       $this->lang->checkTitleEncoding($s),
+                       "checkTitleEncoding('$s')"
+               );
+       }
 
        function provideCheckTitleEncodingData() {
                return array (
@@ -815,42 +967,42 @@ class LanguageTest extends MediaWikiTestCase {
                        ),
                        // The following two data sets come from bug 36839. They fail if checkTitleEncoding uses a regexp to test for
                        // valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding
-                   // uses mb_check_encoding for its test.
+                       // uses mb_check_encoding for its test.
                        array(
                                rawurldecode(
                                        "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C"
-                                       . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C"
-                                       . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C"
-                                       . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C"
-                                       . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C"
-                                       . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C"
-                                       . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C"
-                                       . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C"
-                                       . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C"
-                                       . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C"
-                                       . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C"
-                                       . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C"
-                                       . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C"
-                                       . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis"
+                                               . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C"
+                                               . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C"
+                                               . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C"
+                                               . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C"
+                                               . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C"
+                                               . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C"
+                                               . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C"
+                                               . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C"
+                                               . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C"
+                                               . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C"
+                                               . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C"
+                                               . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C"
+                                               . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis"
                                ),
                        ),
                        array(
                                rawurldecode(
                                        "Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C"
-                                       . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C"
-                                       . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C"
-                                       . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou"
-                                       . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C"
-                                       . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C"
-                                       . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C"
-                                       . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C"
-                                       . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C"
-                                       . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes"
+                                               . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C"
+                                               . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C"
+                                               . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou"
+                                               . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C"
+                                               . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C"
+                                               . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C"
+                                               . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C"
+                                               . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C"
+                                               . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes"
                                )
                        )
                );