Merge "API: Overhaul ApiResult, make format=xml not throw, and add json formatversion"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 16 Apr 2015 01:05:51 +0000 (01:05 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 16 Apr 2015 01:05:51 +0000 (01:05 +0000)
106 files changed:
RELEASE-NOTES-1.25
autoload.php
includes/api/ApiBase.php
includes/api/ApiComparePages.php
includes/api/ApiContinuationManager.php [new file with mode: 0644]
includes/api/ApiCreateAccount.php
includes/api/ApiEditPage.php
includes/api/ApiErrorFormatter.php [new file with mode: 0644]
includes/api/ApiExpandTemplates.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiFileRevert.php
includes/api/ApiFormatBase.php
includes/api/ApiFormatDbg.php
includes/api/ApiFormatDump.php
includes/api/ApiFormatFeedWrapper.php
includes/api/ApiFormatJson.php
includes/api/ApiFormatPhp.php
includes/api/ApiFormatRaw.php
includes/api/ApiFormatTxt.php
includes/api/ApiFormatWddx.php
includes/api/ApiFormatXml.php
includes/api/ApiHelp.php
includes/api/ApiImageRotate.php
includes/api/ApiImport.php
includes/api/ApiMain.php
includes/api/ApiManageTags.php
includes/api/ApiMessage.php [new file with mode: 0644]
includes/api/ApiMove.php
includes/api/ApiOpenSearch.php
includes/api/ApiPageSet.php
includes/api/ApiParamInfo.php
includes/api/ApiParse.php
includes/api/ApiProtect.php
includes/api/ApiPurge.php
includes/api/ApiQuery.php
includes/api/ApiQueryAllCategories.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryAllImages.php
includes/api/ApiQueryAllLinks.php
includes/api/ApiQueryAllMessages.php
includes/api/ApiQueryAllPages.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryBacklinks.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryBlocks.php
includes/api/ApiQueryCategoryMembers.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryExtLinksUsage.php
includes/api/ApiQueryExternalLinks.php
includes/api/ApiQueryFileRepoInfo.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryIWBacklinks.php
includes/api/ApiQueryIWLinks.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryLangBacklinks.php
includes/api/ApiQueryLangLinks.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQueryORM.php
includes/api/ApiQueryPagePropNames.php
includes/api/ApiQueryPagesWithProp.php
includes/api/ApiQueryPrefixSearch.php
includes/api/ApiQueryProtectedTitles.php
includes/api/ApiQueryQueryPage.php
includes/api/ApiQueryRandom.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisionsBase.php
includes/api/ApiQuerySearch.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryStashImageInfo.php
includes/api/ApiQueryTags.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiQueryUserInfo.php
includes/api/ApiQueryUsers.php
includes/api/ApiQueryWatchlist.php
includes/api/ApiQueryWatchlistRaw.php
includes/api/ApiResult.php
includes/api/ApiRevisionDelete.php
includes/api/ApiRsd.php
includes/api/ApiSerializable.php [new file with mode: 0644]
includes/api/ApiSetNotificationTimestamp.php
includes/api/ApiUpload.php
includes/api/ApiUserrights.php
includes/api/ApiWatch.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/debug/MWDebug.php
tests/phpunit/includes/api/ApiContinuationManagerTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiErrorFormatterTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiMainTest.php
tests/phpunit/includes/api/ApiMessageTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiOptionsTest.php
tests/phpunit/includes/api/ApiResultTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiTestCase.php
tests/phpunit/includes/api/MockApi.php
tests/phpunit/includes/api/MockApiQueryBase.php
tests/phpunit/includes/api/format/ApiFormatDbgTest.php
tests/phpunit/includes/api/format/ApiFormatDumpTest.php
tests/phpunit/includes/api/format/ApiFormatJsonTest.php
tests/phpunit/includes/api/format/ApiFormatNoneTest.php
tests/phpunit/includes/api/format/ApiFormatPhpTest.php
tests/phpunit/includes/api/format/ApiFormatTxtTest.php
tests/phpunit/includes/api/format/ApiFormatWddxTest.php
tests/phpunit/includes/api/format/ApiFormatXmlTest.php
tests/phpunit/includes/debug/MWDebugTest.php
tests/phpunit/includes/upload/UploadFromUrlTest.php

index 2d149b9..4f648f5 100644 (file)
@@ -252,6 +252,7 @@ production.
   Title::userCan() via the API.
 * Default type param for query list=watchlist and list=recentchanges has
   been changed from all types (e.g. including 'external') to 'edit|new|log'.
+* Added formatversion to format=json, still experimental.
 
 === Action API internal changes in 1.25 ===
 * ApiHelp has been rewritten to support i18n and paginated HTML output.
@@ -289,6 +290,15 @@ production.
   the current request was sent with the 'callback' parameter (or any future
   method that breaks the same-origin policy).
 * Profiling methods in ApiBase are deprecated and no longer need to be called.
+* ApiResult was greatly overhauled. See inline documentation for details.
+* ApiResult will automatically convert objects to strings or arrays (depending
+  on whether a __toString() method exists on the object), and will refuse to
+  add unsupported value types.
+  * An informal interface, ApiSerializable, exists to override the default
+    object conversion.
+* ApiResult/ApiFormatBase "raw mode" is deprecated.
+* ApiFormatXml now assumes defaults and so on instead of throwing errors when
+  metadata isn't set.
 * The following methods have been deprecated and may be removed in a future
   release:
   * ApiBase::getDescription
@@ -305,15 +315,31 @@ production.
   * ApiBase::profileDBIn
   * ApiBase::profileDBOut
   * ApiBase::getProfileDBTime
+  * ApiBase::getResultData
   * ApiFormatBase::setUnescapeAmps
   * ApiFormatBase::getWantsHelp
   * ApiFormatBase::setHelp
   * ApiFormatBase::formatHTML
   * ApiFormatBase::setBufferResult
   * ApiFormatBase::getDescription
+  * ApiFormatBase::getNeedsRawData
   * ApiMain::setHelp
   * ApiMain::reallyMakeHelpMsg
   * ApiMain::makeHelpMsgHeader
+  * ApiResult::setRawMode
+  * ApiResult::getIsRawMode
+  * ApiResult::getData
+  * ApiResult::setElement
+  * ApiResult::setContent
+  * ApiResult::setIndexedTagName_recursive
+  * ApiResult::setIndexedTagName_internal
+  * ApiResult::setParsedLimit
+  * ApiResult::beginContinuation
+  * ApiResult::setContinueParam
+  * ApiResult::setGeneratorContinueParam
+  * ApiResult::endContinuation
+  * ApiResult::size
+  * ApiResult::convertStatusToArray
   * ApiQueryImageInfo::getPropertyDescriptions
 * The following classes have been deprecated and may be removed in a future
   release:
index 92d6014..93f8e43 100644 (file)
@@ -21,11 +21,14 @@ $wgAutoloadLocalClasses = array(
        'ApiCheckToken' => __DIR__ . '/includes/api/ApiCheckToken.php',
        'ApiClearHasMsg' => __DIR__ . '/includes/api/ApiClearHasMsg.php',
        'ApiComparePages' => __DIR__ . '/includes/api/ApiComparePages.php',
+       'ApiContinuationManager' => __DIR__ . '/includes/api/ApiContinuationManager.php',
        'ApiCreateAccount' => __DIR__ . '/includes/api/ApiCreateAccount.php',
        'ApiDelete' => __DIR__ . '/includes/api/ApiDelete.php',
        'ApiDisabled' => __DIR__ . '/includes/api/ApiDisabled.php',
        'ApiEditPage' => __DIR__ . '/includes/api/ApiEditPage.php',
        'ApiEmailUser' => __DIR__ . '/includes/api/ApiEmailUser.php',
+       'ApiErrorFormatter' => __DIR__ . '/includes/api/ApiErrorFormatter.php',
+       'ApiErrorFormatter_BackCompat' => __DIR__ . '/includes/api/ApiErrorFormatter.php',
        'ApiExpandTemplates' => __DIR__ . '/includes/api/ApiExpandTemplates.php',
        'ApiFeedContributions' => __DIR__ . '/includes/api/ApiFeedContributions.php',
        'ApiFeedRecentChanges' => __DIR__ . '/includes/api/ApiFeedRecentChanges.php',
@@ -53,6 +56,7 @@ $wgAutoloadLocalClasses = array(
        'ApiLogout' => __DIR__ . '/includes/api/ApiLogout.php',
        'ApiMain' => __DIR__ . '/includes/api/ApiMain.php',
        'ApiManageTags' => __DIR__ . '/includes/api/ApiManageTags.php',
+       'ApiMessage' => __DIR__ . '/includes/api/ApiMessage.php',
        'ApiModuleManager' => __DIR__ . '/includes/api/ApiModuleManager.php',
        'ApiMove' => __DIR__ . '/includes/api/ApiMove.php',
        'ApiOpenSearch' => __DIR__ . '/includes/api/ApiOpenSearch.php',
@@ -118,10 +122,12 @@ $wgAutoloadLocalClasses = array(
        'ApiQueryUsers' => __DIR__ . '/includes/api/ApiQueryUsers.php',
        'ApiQueryWatchlist' => __DIR__ . '/includes/api/ApiQueryWatchlist.php',
        'ApiQueryWatchlistRaw' => __DIR__ . '/includes/api/ApiQueryWatchlistRaw.php',
+       'ApiRawMessage' => __DIR__ . '/includes/api/ApiMessage.php',
        'ApiResult' => __DIR__ . '/includes/api/ApiResult.php',
        'ApiRevisionDelete' => __DIR__ . '/includes/api/ApiRevisionDelete.php',
        'ApiRollback' => __DIR__ . '/includes/api/ApiRollback.php',
        'ApiRsd' => __DIR__ . '/includes/api/ApiRsd.php',
+       'ApiSerializable' => __DIR__ . '/includes/api/ApiSerializable.php',
        'ApiSetNotificationTimestamp' => __DIR__ . '/includes/api/ApiSetNotificationTimestamp.php',
        'ApiStashEdit' => __DIR__ . '/includes/api/ApiStashEdit.php',
        'ApiTag' => __DIR__ . '/includes/api/ApiTag.php',
@@ -516,6 +522,7 @@ $wgAutoloadLocalClasses = array(
        'Http' => __DIR__ . '/includes/HttpFunctions.php',
        'HttpError' => __DIR__ . '/includes/exception/HttpError.php',
        'HttpStatus' => __DIR__ . '/includes/libs/HttpStatus.php',
+       'IApiMessage' => __DIR__ . '/includes/api/ApiMessage.php',
        'ICacheHelper' => __DIR__ . '/includes/cache/CacheHelper.php',
        'IContextSource' => __DIR__ . '/includes/context/IContextSource.php',
        'IDBAccessObject' => __DIR__ . '/includes/dao/IDBAccessObject.php',
index f4f2c8c..143fc0f 100644 (file)
@@ -472,11 +472,17 @@ abstract class ApiBase extends ContextSource {
        }
 
        /**
-        * Get the result data array (read-only)
-        * @return array
+        * Get the error formatter
+        * @return ApiErrorFormatter
         */
-       public function getResultData() {
-               return $this->getResult()->getData();
+       public function getErrorFormatter() {
+               // Main module has getErrorFormatter() method overridden
+               // Safety - avoid infinite loop:
+               if ( $this->isMain() ) {
+                       ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
+               }
+
+               return $this->getMain()->getErrorFormatter();
        }
 
        /**
@@ -491,6 +497,34 @@ abstract class ApiBase extends ContextSource {
                return $this->mSlaveDB;
        }
 
+       /**
+        * Get the continuation manager
+        * @return ApiContinuationManager|null
+        */
+       public function getContinuationManager() {
+               // Main module has getContinuationManager() method overridden
+               // Safety - avoid infinite loop:
+               if ( $this->isMain() ) {
+                       ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
+               }
+
+               return $this->getMain()->getContinuationManager();
+       }
+
+       /**
+        * Set the continuation manager
+        * @param ApiContinuationManager|null
+        */
+       public function setContinuationManager( $manager ) {
+               // Main module has setContinuationManager() method overridden
+               // Safety - avoid infinite loop:
+               if ( $this->isMain() ) {
+                       ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
+               }
+
+               $this->getMain()->setContinuationManager( $manager );
+       }
+
        /**@}*/
 
        /************************************************************************//**
@@ -870,7 +904,7 @@ abstract class ApiBase extends ContextSource {
                                                        $value = $this->getMain()->canApiHighLimits()
                                                                ? $paramSettings[self::PARAM_MAX2]
                                                                : $paramSettings[self::PARAM_MAX];
-                                                       $this->getResult()->setParsedLimit( $this->getModuleName(), $value );
+                                                       $this->getResult()->addParsedLimit( $this->getModuleName(), $value );
                                                } else {
                                                        $value = intval( $value );
                                                        $this->validateLimit(
@@ -1246,28 +1280,8 @@ abstract class ApiBase extends ContextSource {
         * @param string $warning Warning message
         */
        public function setWarning( $warning ) {
-               $result = $this->getResult();
-               $data = $result->getData();
-               $moduleName = $this->getModuleName();
-               if ( isset( $data['warnings'][$moduleName] ) ) {
-                       // Don't add duplicate warnings
-                       $oldWarning = $data['warnings'][$moduleName]['*'];
-                       $warnPos = strpos( $oldWarning, $warning );
-                       // If $warning was found in $oldWarning, check if it starts at 0 or after "\n"
-                       if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) {
-                               // Check if $warning is followed by "\n" or the end of the $oldWarning
-                               $warnPos += strlen( $warning );
-                               if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) {
-                                       return;
-                               }
-                       }
-                       // If there is a warning already, append it to the existing one
-                       $warning = "$oldWarning\n$warning";
-               }
-               $msg = array();
-               ApiResult::setContent( $msg, $warning );
-               $result->addValue( 'warnings', $moduleName,
-                       $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+               $msg = new ApiRawMessage( $warning, 'warning' );
+               $this->getErrorFormatter()->addWarning( $this->getModuleName(), $msg );
        }
 
        /**
@@ -2809,6 +2823,15 @@ abstract class ApiBase extends ContextSource {
                return 0;
        }
 
+       /**
+        * Get the result data array (read-only)
+        * @deprecated since 1.25, use $this->getResult() methods instead
+        * @return array
+        */
+       public function getResultData() {
+               return $this->getResult()->getData();
+       }
+
        /**@}*/
 }
 
index ce256a6..2300912 100644 (file)
@@ -72,7 +72,7 @@ class ApiComparePages extends ApiBase {
                        );
                }
 
-               ApiResult::setContent( $vals, $difftext );
+               ApiResult::setContentValue( $vals, 'body', $difftext );
 
                $this->getResult()->addValue( null, $this->getModuleName(), $vals );
        }
diff --git a/includes/api/ApiContinuationManager.php b/includes/api/ApiContinuationManager.php
new file mode 100644 (file)
index 0000000..dea1cf4
--- /dev/null
@@ -0,0 +1,238 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * This manages continuation state.
+ * @since 1.25 this is no longer a subclass of ApiBase
+ * @ingroup API
+ */
+class ApiContinuationManager {
+       private $source;
+
+       private $allModules = array();
+       private $generatedModules = array();
+
+       private $continuationData = array();
+       private $generatorContinuationData = array();
+
+       private $generatorParams = array();
+       private $generatorDone = false;
+
+       /**
+        * @param ApiBase $module Module starting the continuation
+        * @param ApiBase[] $allModules Contains ApiBase instances that will be executed
+        * @param array $generatedModules Names of modules that depend on the generator
+        */
+       public function __construct(
+               ApiBase $module, array $allModules = array(), array $generatedModules = array()
+       ) {
+               $this->source = get_class( $module );
+               $request = $module->getRequest();
+
+               $this->generatedModules = $generatedModules
+                       ? array_combine( $generatedModules, $generatedModules )
+                       : array();
+
+               $skip = array();
+               $continue = $request->getVal( 'continue', '' );
+               if ( $continue !== '' ) {
+                       $continue = explode( '||', $continue );
+                       if ( count( $continue ) !== 2 ) {
+                               throw new UsageException(
+                                       'Invalid continue param. You should pass the original value returned by the previous query',
+                                       'badcontinue'
+                               );
+                       }
+                       $this->generatorDone = ( $continue[0] === '-' );
+                       $skip = explode( '|', $continue[1] );
+                       if ( !$this->generatorDone ) {
+                               $params = explode( '|', $continue[0] );
+                               if ( $params ) {
+                                       $this->generatorParams = array_intersect_key(
+                                               $request->getValues(),
+                                               array_flip( $params )
+                                       );
+                               }
+                       } else {
+                               // When the generator is complete, don't run any modules that
+                               // depend on it.
+                               $skip += $this->generatedModules;
+                       }
+               }
+
+               foreach ( $allModules as $module ) {
+                       $name = $module->getModuleName();
+                       if ( in_array( $name, $skip, true ) ) {
+                               $this->allModules[$name] = false;
+                               // Prevent spurious "unused parameter" warnings
+                               $module->extractRequestParams();
+                       } else {
+                               $this->allModules[$name] = $module;
+                       }
+               }
+       }
+
+       /**
+        * Get the class that created this manager
+        * @return string
+        */
+       public function getSource() {
+               return $this->source;
+       }
+
+       /**
+        * Is the generator done?
+        * @return bool
+        */
+       public function isGeneratorDone() {
+               return $this->generatorDone;
+       }
+
+       /**
+        * Get the list of modules that should actually be run
+        * @return ApiBase[]
+        */
+       public function getRunModules() {
+               return array_values( array_filter( $this->allModules ) );
+       }
+
+       /**
+        * Set the continuation parameter for a module
+        * @param ApiBase $module
+        * @param string $paramName
+        * @param string|array $paramValue
+        * @throws UnexpectedValueException
+        */
+       public function addContinueParam( ApiBase $module, $paramName, $paramValue ) {
+               $name = $module->getModuleName();
+               if ( !isset( $this->allModules[$name] ) ) {
+                       throw new UnexpectedValueException(
+                               "Module '$name' called " . __METHOD__ .
+                                       ' but was not passed to ' . __CLASS__ . '::__construct'
+                       );
+               }
+               if ( !$this->allModules[$name] ) {
+                       throw new UnexpectedValueException(
+                               "Module '$name' was not supposed to have been executed, but " .
+                                       'it was executed anyway'
+                       );
+               }
+               $paramName = $module->encodeParamName( $paramName );
+               if ( is_array( $paramValue ) ) {
+                       $paramValue = join( '|', $paramValue );
+               }
+               $this->continuationData[$name][$paramName] = $paramValue;
+       }
+
+       /**
+        * Set the continuation parameter for the generator module
+        * @param ApiBase $module
+        * @param string $paramName
+        * @param string|array $paramValue
+        */
+       public function addGeneratorContinueParam( ApiBase $module, $paramName, $paramValue ) {
+               $name = $module->getModuleName();
+               $paramName = $module->encodeParamName( $paramName );
+               if ( is_array( $paramValue ) ) {
+                       $paramValue = join( '|', $paramValue );
+               }
+               $this->generatorContinuationData[$name][$paramName] = $paramValue;
+       }
+
+       /**
+        * Fetch raw continuation data
+        * @return array
+        */
+       public function getRawContinuation() {
+               return array_merge_recursive( $this->continuationData, $this->generatorContinuationData );
+       }
+
+       /**
+        * Fetch continuation result data
+        * @return array Array( (array)$data, (bool)$batchcomplete )
+        */
+       public function getContinuation() {
+               $data = array();
+               $batchcomplete = false;
+
+               $finishedModules = array_diff(
+                       array_keys( $this->allModules ),
+                       array_keys( $this->continuationData )
+               );
+
+               // First, grab the non-generator-using continuation data
+               $continuationData = array_diff_key( $this->continuationData, $this->generatedModules );
+               foreach ( $continuationData as $module => $kvp ) {
+                       $data += $kvp;
+               }
+
+               // Next, handle the generator-using continuation data
+               $continuationData = array_intersect_key( $this->continuationData, $this->generatedModules );
+               if ( $continuationData ) {
+                       // Some modules are unfinished: include those params, and copy
+                       // the generator params.
+                       foreach ( $continuationData as $module => $kvp ) {
+                               $data += $kvp;
+                       }
+                       $data += $this->generatorParams;
+                       $generatorKeys = join( '|', array_keys( $this->generatorParams ) );
+               } elseif ( $this->generatorContinuationData ) {
+                       // All the generator-using modules are complete, but the
+                       // generator isn't. Continue the generator and restart the
+                       // generator-using modules
+                       $generatorParams = array();
+                       foreach ( $this->generatorContinuationData as $kvp ) {
+                               $generatorParams += $kvp;
+                       }
+                       $data += $generatorParams;
+                       $finishedModules = array_diff( $finishedModules, $this->generatedModules );
+                       $generatorKeys = join( '|', array_keys( $generatorParams ) );
+                       $batchcomplete = true;
+               } else {
+                       // Generator and prop modules are all done. Mark it so.
+                       $generatorKeys = '-';
+                       $batchcomplete = true;
+               }
+
+               // Set 'continue' if any continuation data is set or if the generator
+               // still needs to run
+               if ( $data || $generatorKeys !== '-' ) {
+                       $data['continue'] = $generatorKeys . '||' . join( '|', $finishedModules );
+               }
+
+               return array( $data, $batchcomplete );
+       }
+
+       /**
+        * Store the continuation data into the result
+        * @param ApiResult $result
+        */
+       public function setContinuationIntoResult( ApiResult $result ) {
+               list( $data, $batchcomplete ) = $this->getContinuation();
+               if ( $data ) {
+                       $result->addValue( null, 'continue', $data,
+                               ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+               }
+               if ( $batchcomplete ) {
+                       $result->addValue( null, 'batchcomplete', '',
+                               ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+               }
+       }
+}
index b56a244..455540b 100644 (file)
@@ -152,9 +152,9 @@ class ApiCreateAccount extends ApiBase {
                        $warnings = $status->getErrorsByType( 'warning' );
                        if ( $warnings ) {
                                foreach ( $warnings as &$warning ) {
-                                       $apiResult->setIndexedTagName( $warning['params'], 'param' );
+                                       ApiResult::setIndexedTagName( $warning['params'], 'param' );
                                }
-                               $apiResult->setIndexedTagName( $warnings, 'warning' );
+                               ApiResult::setIndexedTagName( $warnings, 'warning' );
                                $result['warnings'] = $warnings;
                        }
                } else {
index 8c7d31d..56c67e0 100644 (file)
@@ -82,7 +82,7 @@ class ApiEditPage extends ApiBase {
                                        $titleObj = $newTitle;
                                }
 
-                               $apiResult->setIndexedTagName( $redirValues, 'r' );
+                               ApiResult::setIndexedTagName( $redirValues, 'r' );
                                $apiResult->addValue( null, 'redirects', $redirValues );
 
                                // Since the page changed, update $pageObj
diff --git a/includes/api/ApiErrorFormatter.php b/includes/api/ApiErrorFormatter.php
new file mode 100644 (file)
index 0000000..9414329
--- /dev/null
@@ -0,0 +1,303 @@
+<?php
+/**
+ * This file contains the ApiErrorFormatter definition, plus implementations of
+ * specific formatters.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Formats errors and warnings for the API, and add them to the associated
+ * ApiResult.
+ * @since 1.25
+ * @ingroup API
+ */
+class ApiErrorFormatter {
+       /** @var Title Dummy title to silence warnings from MessageCache::parse() */
+       private static $dummyTitle = null;
+
+       /** @var ApiResult */
+       protected $result;
+
+       /** @var Language */
+       protected $lang;
+       protected $useDB = false;
+       protected $format = 'none';
+
+       /**
+        * @param ApiResult $result Into which data will be added
+        * @param Language $lang Used for i18n
+        * @param string $format
+        *  - text: Error message as wikitext
+        *  - html: Error message as HTML
+        *  - raw: Raw message key and parameters, no human-readable text
+        *  - none: Code and data only, no human-readable text
+        * @param bool $useDB Whether to use local translations for errors and warnings.
+        */
+       public function __construct( ApiResult $result, Language $lang, $format, $useDB = false ) {
+               $this->result = $result;
+               $this->lang = $lang;
+               $this->useDB = $useDB;
+               $this->format = $format;
+       }
+
+       /**
+        * Fetch a dummy title to set on Messages
+        * @return Title
+        */
+       protected function getDummyTitle() {
+               if ( self::$dummyTitle === null ) {
+                       self::$dummyTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __METHOD__ );
+               }
+               return self::$dummyTitle;
+       }
+
+       /**
+        * Add a warning to the result
+        * @param string $moduleName
+        * @param MessageSpecifier|array|string $msg i18n message for the warning
+        * @param string $code Machine-readable code for the warning. Defaults as
+        *   for IApiMessage::getApiCode().
+        * @param array $data Machine-readable data for the warning, if any.
+        *   Uses IApiMessage::getApiData() if $msg implements that interface.
+        */
+       public function addWarning( $moduleName, $msg, $code = null, $data = null ) {
+               $msg = ApiMessage::create( $msg, $code, $data )
+                       ->inLanguage( $this->lang )
+                       ->title( $this->getDummyTitle() )
+                       ->useDatabase( $this->useDB );
+               $this->addWarningOrError( 'warning', $moduleName, $msg );
+       }
+
+       /**
+        * Add an error to the result
+        * @param string $moduleName
+        * @param MessageSpecifier|array|string $msg i18n message for the error
+        * @param string $code Machine-readable code for the warning. Defaults as
+        *   for IApiMessage::getApiCode().
+        * @param array $data Machine-readable data for the warning, if any.
+        *   Uses IApiMessage::getApiData() if $msg implements that interface.
+        */
+       public function addError( $moduleName, $msg, $code = null, $data = null ) {
+               $msg = ApiMessage::create( $msg, $code, $data )
+                       ->inLanguage( $this->lang )
+                       ->title( $this->getDummyTitle() )
+                       ->useDatabase( $this->useDB );
+               $this->addWarningOrError( 'error', $moduleName, $msg );
+       }
+
+       /**
+        * Add warnings and errors from a Status object to the result
+        * @param string $moduleName
+        * @param Status $status
+        * @param string[] $types 'warning' and/or 'error'
+        */
+       public function addMessagesFromStatus(
+               $moduleName, Status $status, $types = array( 'warning', 'error' )
+       ) {
+               if ( $status->isGood() || !$status->errors ) {
+                       return;
+               }
+
+               $types = (array)$types;
+               foreach ( $status->errors as $error ) {
+                       if ( !in_array( $error['type'], $types, true ) ) {
+                               continue;
+                       }
+
+                       if ( $error['type'] === 'error' ) {
+                               $tag = 'error';
+                       } else {
+                               // Assume any unknown type is a warning
+                               $tag = 'warning';
+                       }
+
+                       if ( is_array( $error ) && isset( $error['message'] ) ) {
+                               // Normal case
+                               if ( $error['message'] instanceof Message ) {
+                                       $msg = ApiMessage::create( $error['message'], null, array() );
+                               } else {
+                                       $args = isset( $error['params'] ) ? $error['params'] : array();
+                                       array_unshift( $args, $error['message'] );
+                                       $error += array( 'params' => array() );
+                                       $msg = ApiMessage::create( $args, null, array() );
+                               }
+                       } elseif ( is_array( $error ) ) {
+                               // Weird case handled by Message::getErrorMessage
+                               $msg = ApiMessage::create( $error, null, array() );
+                       } else {
+                               // Another weird case handled by Message::getErrorMessage
+                               $msg = ApiMessage::create( $error, null, array() );
+                       }
+
+                       $msg->inLanguage( $this->lang )
+                               ->title( $this->getDummyTitle() )
+                               ->useDatabase( $this->useDB );
+                       $this->addWarningOrError( $tag, $moduleName, $msg );
+               }
+       }
+
+       /**
+        * Format messages from a Status as an array
+        * @param Status $status
+        * @param string $type 'warning' or 'error'
+        * @param string|null $format
+        * @return array
+        */
+       public function arrayFromStatus( Status $status, $type = 'error', $format = null ) {
+               if ( $status->isGood() || !$status->errors ) {
+                       return array();
+               }
+
+               $result = new ApiResult( 1e6 );
+               $formatter = new ApiErrorFormatter(
+                       $result, $this->lang, $format ?: $this->format, $this->useDB
+               );
+               $formatter->addMessagesFromStatus( 'dummy', $status, array( $type ) );
+               switch ( $type ) {
+                       case 'error':
+                               return (array)$result->getResultData( array( 'errors', 'dummy' ) );
+                       case 'warning':
+                               return (array)$result->getResultData( array( 'warnings', 'dummy' ) );
+               }
+       }
+
+       /**
+        * Actually add the warning or error to the result
+        * @param string $tag 'warning' or 'error'
+        * @param string $moduleName
+        * @param ApiMessage|ApiRawMessage $msg
+        */
+       protected function addWarningOrError( $tag, $moduleName, $msg ) {
+               $value = array( 'code' => $msg->getApiCode() );
+               switch ( $this->format ) {
+                       case 'wikitext':
+                               $value += array(
+                                       'text' => $msg->text(),
+                                       ApiResult::META_CONTENT => 'text',
+                               );
+                               break;
+
+                       case 'html':
+                               $value += array(
+                                       'html' => $msg->parse(),
+                                       ApiResult::META_CONTENT => 'html',
+                               );
+                               break;
+
+                       case 'raw':
+                               $value += array(
+                                       'message' => $msg->getKey(),
+                                       'params' => $msg->getParams(),
+                               );
+                               ApiResult::setIndexedTagName( $value['params'], 'param' );
+                               break;
+
+                       case 'none':
+                               break;
+               }
+               $value += $msg->getApiData();
+
+               $path = array( $tag . 's', $moduleName );
+               $existing = $this->result->getResultData( $path );
+               if ( $existing === null || !in_array( $value, $existing ) ) {
+                       $flags = ApiResult::NO_SIZE_CHECK;
+                       if ( $existing === null ) {
+                               $flags |= ApiResult::ADD_ON_TOP;
+                       }
+                       $this->result->addValue( $path, null, $value, $flags );
+                       $this->result->addIndexedTagName( $path, $tag );
+               }
+       }
+}
+
+/**
+ * Format errors and warnings in the old style, for backwards compatibility.
+ * @since 1.25
+ * @deprecated Only for backwards compatibility, do not use
+ * @ingroup API
+ */
+class ApiErrorFormatter_BackCompat extends ApiErrorFormatter {
+       /**
+        * @param ApiResult $result Into which data will be added
+        */
+       public function __construct( ApiResult $result ) {
+               parent::__construct( $result, Language::factory( 'en' ), 'none', false );
+       }
+
+       public function arrayFromStatus( Status $status, $type = 'error', $format = null ) {
+               if ( $status->isGood() || !$status->errors ) {
+                       return array();
+               }
+
+               $result = array();
+               foreach ( $status->getErrorsByType( $type ) as $error ) {
+                       if ( $error['message'] instanceof Message ) {
+                               $error = array(
+                                       'message' => $error['message']->getKey(),
+                                       'params' => $error['message']->getParams(),
+                               ) + $error;
+                       }
+                       ApiResult::setIndexedTagName( $error['params'], 'param' );
+                       $result[] = $error;
+               }
+               ApiResult::setIndexedTagName( $result, $type );
+
+               return $result;
+       }
+
+       protected function addWarningOrError( $tag, $moduleName, $msg ) {
+               $value = $msg->plain();
+
+               if ( $tag === 'error' ) {
+                       // In BC mode, only one error
+                       $code = $msg->getApiCode();
+                       if ( isset( ApiBase::$messageMap[$code] ) ) {
+                               // Backwards compatibility
+                               $code = ApiBase::$messageMap[$code]['code'];
+                       }
+
+                       $value = array(
+                               'code' => $code,
+                               'info' => $value,
+                       ) + $msg->getApiData();
+                       $this->result->addValue( null, 'error', $value,
+                               ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+               } else {
+                       // Don't add duplicate warnings
+                       $tag .= 's';
+                       $path = array( $tag, $moduleName );
+                       $oldWarning = $this->result->getResultData( array( $tag, $moduleName, $tag ) );
+                       if ( $oldWarning !== null ) {
+                               $warnPos = strpos( $oldWarning, $value );
+                               // If $value was found in $oldWarning, check if it starts at 0 or after "\n"
+                               if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) {
+                                       // Check if $value is followed by "\n" or the end of the $oldWarning
+                                       $warnPos += strlen( $value );
+                                       if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) {
+                                               return;
+                                       }
+                               }
+                               // If there is a warning already, append it to the existing one
+                               $value = "$oldWarning\n$value";
+                       }
+                       $this->result->addContentValue( $path, $tag, $value,
+                               ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+               }
+       }
+}
index 4a5afb9..5c7717f 100644 (file)
@@ -97,7 +97,7 @@ class ApiExpandTemplates extends ApiBase {
                        } else {
                                // the old way
                                $xml_result = array();
-                               ApiResult::setContent( $xml_result, $xml );
+                               ApiResult::setContentValue( $xml_result, 'xml', $xml );
                                $result->addValue( null, 'parsetree', $xml_result );
                        }
                }
@@ -110,7 +110,7 @@ class ApiExpandTemplates extends ApiBase {
                        $wikitext = $wgParser->preprocess( $params['text'], $title_obj, $options, $revid, $frame );
                        if ( $params['prop'] === null ) {
                                // the old way
-                               ApiResult::setContent( $retval, $wikitext );
+                               ApiResult::setContentValue( $retval, 'wikitext', $wikitext );
                        } else {
                                if ( isset( $prop['categories'] ) ) {
                                        $categories = $wgParser->getOutput()->getCategories();
@@ -119,10 +119,10 @@ class ApiExpandTemplates extends ApiBase {
                                                foreach ( $categories as $category => $sortkey ) {
                                                        $entry = array();
                                                        $entry['sortkey'] = $sortkey;
-                                                       ApiResult::setContent( $entry, $category );
+                                                       ApiResult::setContentValue( $entry, 'category', $category );
                                                        $categories_result[] = $entry;
                                                }
-                                               $result->setIndexedTagName( $categories_result, 'category' );
+                                               ApiResult::setIndexedTagName( $categories_result, 'category' );
                                                $retval['categories'] = $categories_result;
                                        }
                                }
@@ -133,10 +133,10 @@ class ApiExpandTemplates extends ApiBase {
                                                foreach ( $properties as $name => $value ) {
                                                        $entry = array();
                                                        $entry['name'] = $name;
-                                                       ApiResult::setContent( $entry, $value );
+                                                       ApiResult::setContentValue( $entry, 'value', $value );
                                                        $properties_result[] = $entry;
                                                }
-                                               $result->setIndexedTagName( $properties_result, 'property' );
+                                               ApiResult::setIndexedTagName( $properties_result, 'property' );
                                                $retval['properties'] = $properties_result;
                                        }
                                }
@@ -151,7 +151,7 @@ class ApiExpandTemplates extends ApiBase {
                                }
                        }
                }
-               $result->setSubelements( $retval, array( 'wikitext', 'parsetree' ) );
+               ApiResult::setSubelementsList( $retval, array( 'wikitext', 'parsetree' ) );
                $result->addValue( null, $this->getModuleName(), $retval );
        }
 
index bfa750b..d1beef8 100644 (file)
@@ -112,11 +112,12 @@ class ApiFeedWatchlist extends ApiBase {
                        $module = new ApiMain( $fauxReq );
                        $module->execute();
 
-                       // Get data array
-                       $data = $module->getResultData();
-
+                       $data = $module->getResult()->getResultData( array( 'query', 'watchlist' ) );
                        $feedItems = array();
-                       foreach ( (array)$data['query']['watchlist'] as $info ) {
+                       foreach ( (array)$data as $key => $info ) {
+                               if ( ApiResult::isMetadataKey( $key ) ) {
+                                       continue;
+                               }
                                $feedItem = $this->createFeedItem( $info );
                                if ( $feedItem ) {
                                        $feedItems[] = $feedItem;
index 61966e5..5517ee0 100644 (file)
@@ -61,7 +61,7 @@ class ApiFileRevert extends ApiBase {
                } else {
                        $result = array(
                                'result' => 'Failure',
-                               'errors' => $this->getResult()->convertStatusToArray( $status ),
+                               'errors' => $this->getErrorFormatter()->arrayFromStatus( $status ),
                        );
                }
 
index 7bbd968..26eac08 100644 (file)
@@ -60,14 +60,6 @@ abstract class ApiFormatBase extends ApiBase {
         */
        abstract public function getMimeType();
 
-       /**
-        * Whether this formatter needs raw data such as _element tags
-        * @return bool
-        */
-       public function getNeedsRawData() {
-               return false;
-       }
-
        /**
         * Get the internal format name
         * @return string
@@ -350,6 +342,22 @@ abstract class ApiFormatBase extends ApiBase {
        public function setBufferResult( $value ) {
        }
 
+       /**
+        * Formerly indicated whether the formatter needed metadata from ApiResult.
+        *
+        * ApiResult previously (indirectly) used this to decide whether to add
+        * metadata or to ignore calls to metadata-setting methods, which
+        * unfortunately made several methods that should have been static have to
+        * be dynamic instead. Now ApiResult always stores metadata and formatters
+        * are required to ignore it or filter it out.
+        *
+        * @deprecated since 1.25
+        * @return bool
+        */
+       public function getNeedsRawData() {
+               return false;
+       }
+
        /**@}*/
 }
 
index 273e205..7d359ad 100644 (file)
@@ -40,7 +40,12 @@ class ApiFormatDbg extends ApiFormatBase {
 
        public function execute() {
                $this->markDeprecated();
-               $this->printText( var_export( $this->getResultData(), true ) );
+               $data = $this->getResult()->getResultData( null, array(
+                       'BC' => array(),
+                       'Types' => array(),
+                       'Strip' => 'all',
+               ) );
+               $this->printText( var_export( $data, true ) );
        }
 
        public function isDeprecated() {
index 7ef8960..f34e1ae 100644 (file)
@@ -40,8 +40,13 @@ class ApiFormatDump extends ApiFormatBase {
 
        public function execute() {
                $this->markDeprecated();
+               $data = $this->getResult()->getResultData( null, array(
+                       'BC' => array(),
+                       'Types' => array(),
+                       'Strip' => 'all',
+               ) );
                ob_start();
-               var_dump( $this->getResultData() );
+               var_dump( $data );
                $result = ob_get_contents();
                ob_end_clean();
                $this->printText( $result );
index 3f53ed4..00747ee 100644 (file)
@@ -46,8 +46,8 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
                // Disable size checking for this because we can't continue
                // cleanly; size checking would cause more problems than it'd
                // solve
-               $result->addValue( null, '_feed', $feed, ApiResult::NO_SIZE_CHECK );
-               $result->addValue( null, '_feeditems', $feedItems, ApiResult::NO_SIZE_CHECK );
+               $result->addValue( null, '_feed', $feed, ApiResult::NO_VALIDATE );
+               $result->addValue( null, '_feeditems', $feedItems, ApiResult::NO_VALIDATE );
        }
 
        /**
@@ -89,7 +89,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
                        return;
                }
 
-               $data = $this->getResultData();
+               $data = $this->getResult()->getResultData();
                if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) {
                        $data['_feed']->httpHeaders();
                } else {
@@ -104,7 +104,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
         * $result['_feeditems'] - an array of FeedItem instances
         */
        public function execute() {
-               $data = $this->getResultData();
+               $data = $this->getResult()->getResultData();
                if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) {
                        $feed = $data['_feed'];
                        $items = $data['_feeditems'];
index 966e82d..41d7051 100644 (file)
  */
 class ApiFormatJson extends ApiFormatBase {
 
-       private $mIsRaw;
+       private $isRaw;
 
        public function __construct( ApiMain $main, $format ) {
                parent::__construct( $main, $format );
-               $this->mIsRaw = ( $format === 'rawfm' );
+               $this->isRaw = ( $format === 'rawfm' );
        }
 
        public function getMimeType() {
@@ -47,8 +47,11 @@ class ApiFormatJson extends ApiFormatBase {
                return 'application/json';
        }
 
+       /**
+        * @deprecated since 1.25
+        */
        public function getNeedsRawData() {
-               return $this->mIsRaw;
+               return $this->isRaw;
        }
 
        /**
@@ -62,11 +65,37 @@ class ApiFormatJson extends ApiFormatBase {
 
        public function execute() {
                $params = $this->extractRequestParams();
-               $json = FormatJson::encode(
-                       $this->getResultData(),
-                       $this->getIsHtml(),
-                       $params['utf8'] ? FormatJson::ALL_OK : FormatJson::XMLMETA_OK
-               );
+
+               $opt = 0;
+               if ( $this->isRaw ) {
+                       $opt |= FormatJson::ALL_OK;
+                       $transform = array();
+               } else {
+                       switch ( $params['formatversion'] ) {
+                               case 1:
+                                       $opt |= $params['utf8'] ? FormatJson::ALL_OK : FormatJson::XMLMETA_OK;
+                                       $transform = array(
+                                               'BC' => array(),
+                                               'Types' => array( 'AssocAsObject' => true ),
+                                               'Strip' => 'all',
+                                       );
+                                       break;
+
+                               case 2:
+                               case 'latest':
+                                       $opt |= $params['ascii'] ? FormatJson::XMLMETA_OK : FormatJson::ALL_OK;
+                                       $transform = array(
+                                               'Types' => array( 'AssocAsObject' => true ),
+                                               'Strip' => 'all',
+                                       );
+                                       break;
+
+                               default:
+                                       self::dieUsage( __METHOD__ . ': Unknown value for \'formatversion\'' );
+                       }
+               }
+               $data = $this->getResult()->getResultData( null, $transform );
+               $json = FormatJson::encode( $data, $this->getIsHtml(), $opt );
 
                // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in
                // Flash, but what it does isn't friendly for the API, so we need to
@@ -89,7 +118,11 @@ class ApiFormatJson extends ApiFormatBase {
        }
 
        public function getAllowedParams() {
-               return array(
+               if ( $this->isRaw ) {
+                       return array();
+               }
+
+               $ret = array(
                        'callback' => array(
                                ApiBase::PARAM_HELP_MSG => 'apihelp-json-param-callback',
                        ),
@@ -97,6 +130,16 @@ class ApiFormatJson extends ApiFormatBase {
                                ApiBase::PARAM_DFLT => false,
                                ApiBase::PARAM_HELP_MSG => 'apihelp-json-param-utf8',
                        ),
+                       'ascii' => array(
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-json-param-ascii',
+                       ),
+                       'formatversion' => array(
+                               ApiBase::PARAM_TYPE => array( 1, 2, 'latest' ),
+                               ApiBase::PARAM_DFLT => 1,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-json-param-formatversion',
+                       ),
                );
+               return $ret;
        }
 }
index a4b4a11..a6a6f32 100644 (file)
@@ -35,7 +35,29 @@ class ApiFormatPhp extends ApiFormatBase {
        }
 
        public function execute() {
-               $text = serialize( $this->getResultData() );
+               $params = $this->extractRequestParams();
+
+               switch ( $params['formatversion'] ) {
+                       case 1:
+                               $transforms = array(
+                                       'BC' => array(),
+                                       'Types' => array(),
+                                       'Strip' => 'all',
+                               );
+                               break;
+
+                       case 2:
+                       case 'latest':
+                               $transforms = array(
+                                       'Types' => array(),
+                                       'Strip' => 'all',
+                               );
+                               break;
+
+                       default:
+                               self::dieUsage( __METHOD__ . ': Unknown value for \'formatversion\'' );
+               }
+               $text = serialize( $this->getResult()->getResultData( null, $transforms ) );
 
                // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in
                // Flash, but what it does isn't friendly for the API. There's nothing
@@ -53,4 +75,15 @@ class ApiFormatPhp extends ApiFormatBase {
 
                $this->printText( $text );
        }
+
+       public function getAllowedParams() {
+               $ret = array(
+                       'formatversion' => array(
+                               ApiBase::PARAM_TYPE => array( 1, 2, 'latest' ),
+                               ApiBase::PARAM_DFLT => 1,
+                               ApiBase::PARAM_HELP_MSG => 'apihelp-php-param-formatversion',
+                       ),
+               );
+               return $ret;
+       }
 }
index 81d2f4f..7bb2453 100644 (file)
@@ -42,7 +42,7 @@ class ApiFormatRaw extends ApiFormatBase {
        }
 
        public function getMimeType() {
-               $data = $this->getResultData();
+               $data = $this->getResult()->getResultData();
 
                if ( isset( $data['error'] ) ) {
                        return $this->errorFallback->getMimeType();
@@ -56,7 +56,7 @@ class ApiFormatRaw extends ApiFormatBase {
        }
 
        public function initPrinter( $unused = false ) {
-               $data = $this->getResultData();
+               $data = $this->getResult()->getResultData();
                if ( isset( $data['error'] ) ) {
                        $this->errorFallback->initPrinter( $unused );
                } else {
@@ -65,7 +65,7 @@ class ApiFormatRaw extends ApiFormatBase {
        }
 
        public function closePrinter() {
-               $data = $this->getResultData();
+               $data = $this->getResult()->getResultData();
                if ( isset( $data['error'] ) ) {
                        $this->errorFallback->closePrinter();
                } else {
@@ -74,7 +74,7 @@ class ApiFormatRaw extends ApiFormatBase {
        }
 
        public function execute() {
-               $data = $this->getResultData();
+               $data = $this->getResult()->getResultData();
                if ( isset( $data['error'] ) ) {
                        $this->errorFallback->execute();
                        return;
index 505b259..e739d5a 100644 (file)
@@ -40,7 +40,12 @@ class ApiFormatTxt extends ApiFormatBase {
 
        public function execute() {
                $this->markDeprecated();
-               $this->printText( print_r( $this->getResultData(), true ) );
+               $data = $this->getResult()->getResultData( null, array(
+                       'BC' => array(),
+                       'Types' => array(),
+                       'Strip' => 'all',
+               ) );
+               $this->printText( print_r( $data, true ) );
        }
 
        public function isDeprecated() {
index 8662a64..c18353f 100644 (file)
@@ -38,8 +38,20 @@ class ApiFormatWddx extends ApiFormatBase {
        public function execute() {
                $this->markDeprecated();
 
+               $data = $this->getResult()->getResultData( null, array(
+                       'BC' => array(),
+                       'Types' => array( 'AssocAsObject' => true ),
+                       'Strip' => 'all',
+               ) );
+
                if ( !$this->getIsHtml() && !static::useSlowPrinter() ) {
-                       $this->printText( wddx_serialize_value( $this->getResultData() ) );
+                       $txt = wddx_serialize_value( $data );
+                       $txt = str_replace(
+                               '<struct><var name=\'php_class_name\'><string>stdClass</string></var>',
+                               '<struct>',
+                               $txt
+                       );
+                       $this->printText( $txt );
                } else {
                        // Don't do newlines and indentation if we weren't asked
                        // for pretty output
@@ -49,7 +61,7 @@ class ApiFormatWddx extends ApiFormatBase {
                        $this->printText( "<wddxPacket version=\"1.0\">$nl" );
                        $this->printText( "$indstr<header />$nl" );
                        $this->printText( "$indstr<data>$nl" );
-                       $this->slowWddxPrinter( $this->getResultData(), 4 );
+                       $this->slowWddxPrinter( $data, 4 );
                        $this->printText( "$indstr</data>$nl" );
                        $this->printText( "</wddxPacket>$nl" );
                }
@@ -102,34 +114,37 @@ class ApiFormatWddx extends ApiFormatBase {
                $indstr = ( $this->getIsHtml() ? str_repeat( ' ', $indent ) : '' );
                $indstr2 = ( $this->getIsHtml() ? str_repeat( ' ', $indent + 2 ) : '' );
                $nl = ( $this->getIsHtml() ? "\n" : '' );
+
                if ( is_array( $elemValue ) ) {
-                       // Check whether we've got an associative array (<struct>)
-                       // or a regular array (<array>)
                        $cnt = count( $elemValue );
-                       if ( $cnt == 0 || array_keys( $elemValue ) === range( 0, $cnt - 1 ) ) {
-                               // Regular array
-                               $this->printText( $indstr . Xml::element( 'array', array(
-                                       'length' => $cnt ), null ) . $nl );
-                               foreach ( $elemValue as $subElemValue ) {
-                                       $this->slowWddxPrinter( $subElemValue, $indent + 2 );
-                               }
-                               $this->printText( "$indstr</array>$nl" );
-                       } else {
-                               // Associative array (<struct>)
-                               $this->printText( "$indstr<struct>$nl" );
-                               foreach ( $elemValue as $subElemName => $subElemValue ) {
-                                       $this->printText( $indstr2 . Xml::element( 'var', array(
-                                               'name' => $subElemName
-                                       ), null ) . $nl );
-                                       $this->slowWddxPrinter( $subElemValue, $indent + 4 );
-                                       $this->printText( "$indstr2</var>$nl" );
-                               }
-                               $this->printText( "$indstr</struct>$nl" );
+                       if ( $cnt != 0 && array_keys( $elemValue ) !== range( 0, $cnt - 1 ) ) {
+                               $elemValue = (object)$elemValue;
+                       }
+               }
+
+               if ( is_array( $elemValue ) ) {
+                       // Regular array
+                       $this->printText( $indstr . Xml::element( 'array', array(
+                               'length' => count( $elemValue ) ), null ) . $nl );
+                       foreach ( $elemValue as $subElemValue ) {
+                               $this->slowWddxPrinter( $subElemValue, $indent + 2 );
+                       }
+                       $this->printText( "$indstr</array>$nl" );
+               } elseif ( is_object( $elemValue ) ) {
+                       // Associative array (<struct>)
+                       $this->printText( "$indstr<struct>$nl" );
+                       foreach ( $elemValue as $subElemName => $subElemValue ) {
+                               $this->printText( $indstr2 . Xml::element( 'var', array(
+                                       'name' => $subElemName
+                               ), null ) . $nl );
+                               $this->slowWddxPrinter( $subElemValue, $indent + 4 );
+                               $this->printText( "$indstr2</var>$nl" );
                        }
+                       $this->printText( "$indstr</struct>$nl" );
                } elseif ( is_int( $elemValue ) || is_float( $elemValue ) ) {
                        $this->printText( $indstr . Xml::element( 'number', null, $elemValue ) . $nl );
                } elseif ( is_string( $elemValue ) ) {
-                       $this->printText( $indstr . Xml::element( 'string', null, $elemValue ) . $nl );
+                       $this->printText( $indstr . Xml::element( 'string', null, $elemValue, false ) . $nl );
                } elseif ( is_bool( $elemValue ) ) {
                        $this->printText( $indstr . Xml::element( 'boolean',
                                array( 'value' => $elemValue ? 'true' : 'false' ) ) . $nl
index 7010dd6..dbd5645 100644 (file)
@@ -39,6 +39,9 @@ class ApiFormatXml extends ApiFormatBase {
                return 'text/xml';
        }
 
+       /**
+        * @deprecated since 1.25
+        */
        public function getNeedsRawData() {
                return true;
        }
@@ -56,18 +59,32 @@ class ApiFormatXml extends ApiFormatBase {
                if ( !is_null( $this->mXslt ) ) {
                        $this->addXslt();
                }
-               if ( $this->mIncludeNamespace ) {
+
+               $result = $this->getResult();
+               if ( $this->mIncludeNamespace && $result->getResultData( 'xmlns' ) === null ) {
                        // If the result data already contains an 'xmlns' namespace added
                        // for custom XML output types, it will override the one for the
                        // generic API results.
                        // This allows API output of other XML types like Atom, RSS, RSD.
-                       $data = $this->getResultData() + array( 'xmlns' => self::$namespace );
-               } else {
-                       $data = $this->getResultData();
+                       $result->addValue( null, 'xmlns', self::$namespace, ApiResult::NO_SIZE_CHECK );
                }
+               $data = $result->getResultData( null, array(
+                       'Custom' => function ( &$data, &$metadata ) {
+                               if ( isset( $metadata[ApiResult::META_TYPE] ) ) {
+                                       // We want to use non-BC for BCassoc to force outputting of _idx.
+                                       switch( $metadata[ApiResult::META_TYPE] ) {
+                                               case 'BCassoc':
+                                                       $metadata[ApiResult::META_TYPE] = 'assoc';
+                                                       break;
+                                       }
+                               }
+                       },
+                       'BC' => array( 'nobool', 'no*', 'nosub' ),
+                       'Types' => array( 'ArmorKVP' => '_name' ),
+               ) );
 
                $this->printText(
-                       self::recXmlPrint( $this->mRootElemName,
+                       static::recXmlPrint( $this->mRootElemName,
                                $data,
                                $this->getIsHtml() ? -2 : null
                        )
@@ -77,143 +94,185 @@ class ApiFormatXml extends ApiFormatBase {
        /**
         * This method takes an array and converts it to XML.
         *
-        * There are several noteworthy cases:
-        *
-        * If array contains a key '_element', then the code assumes that ALL
-        * other keys are not important and replaces them with the
-        * value['_element'].
-        *
-        * @par Example:
-        * @verbatim
-        * name='root', value = array( '_element'=>'page', 'x', 'y', 'z')
-        * @endverbatim
-        * creates:
-        * @verbatim
-        * <root>  <page>x</page>  <page>y</page>  <page>z</page> </root>
-        * @endverbatim
-        *
-        * If any of the array's element key is '*', then the code treats all
-        * other key->value pairs as attributes, and the value['*'] as the
-        * element's content.
-        *
-        * @par Example:
-        * @verbatim
-        * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10)
-        * @endverbatim
-        * creates:
-        * @verbatim
-        * <root lang='en' id='10'>text</root>
-        * @endverbatim
-        *
-        * Finally neither key is found, all keys become element names, and values
-        * become element content.
-        *
-        * @note The method is recursive, so the same rules apply to any
-        * sub-arrays.
-        *
-        * @param string $elemName
-        * @param mixed $elemValue
-        * @param int $indent
-        *
+        * @param string|null $name Tag name
+        * @param mixed $value Tag value (attributes/content/subelements)
+        * @param int|null $indent Indentation
+        * @param array $attributes Additional attributes
         * @return string
         */
-       public static function recXmlPrint( $elemName, $elemValue, $indent ) {
+       public static function recXmlPrint( $name, $value, $indent, $attributes = array() ) {
                $retval = '';
-               if ( !is_null( $indent ) ) {
-                       $indent += 2;
+               if ( $indent !== null ) {
+                       if ( $name !== null ) {
+                               $indent += 2;
+                       }
                        $indstr = "\n" . str_repeat( ' ', $indent );
                } else {
                        $indstr = '';
                }
-               $elemName = str_replace( ' ', '_', $elemName );
-
-               if ( is_array( $elemValue ) ) {
-                       if ( isset( $elemValue['*'] ) ) {
-                               $subElemContent = $elemValue['*'];
-                               unset( $elemValue['*'] );
 
-                               // Add xml:space="preserve" to the
-                               // element so XML parsers will leave
-                               // whitespace in the content alone
-                               $elemValue['xml:space'] = 'preserve';
-                       } else {
-                               $subElemContent = null;
+               if ( is_object( $value ) ) {
+                       $value = (array)$value;
+               }
+               if ( is_array( $value ) ) {
+                       $contentKey = isset( $value[ApiResult::META_CONTENT] )
+                               ? $value[ApiResult::META_CONTENT]
+                               : '*';
+                       $subelementKeys = isset( $value[ApiResult::META_SUBELEMENTS] )
+                               ? $value[ApiResult::META_SUBELEMENTS]
+                               : array();
+                       if ( isset( $value[ApiResult::META_BC_SUBELEMENTS] ) ) {
+                               $subelementKeys = array_merge(
+                                       $subelementKeys, $value[ApiResult::META_BC_SUBELEMENTS]
+                               );
                        }
+                       $preserveKeys = isset( $value[ApiResult::META_PRESERVE_KEYS] )
+                               ? $value[ApiResult::META_PRESERVE_KEYS]
+                               : array();
+                       $indexedTagName = isset( $value[ApiResult::META_INDEXED_TAG_NAME] )
+                               ? $value[ApiResult::META_INDEXED_TAG_NAME]
+                               : '_v';
+                       $bcBools = isset( $value[ApiResult::META_BC_BOOLS] )
+                               ? $value[ApiResult::META_BC_BOOLS]
+                               : array();
+                       $indexSubelements = isset( $value[ApiResult::META_TYPE] )
+                               ? $value[ApiResult::META_TYPE] !== 'array'
+                               : false;
 
-                       if ( isset( $elemValue['_element'] ) ) {
-                               $subElemIndName = $elemValue['_element'];
-                               unset( $elemValue['_element'] );
-                       } else {
-                               $subElemIndName = null;
-                       }
+                       $content = null;
+                       $subelements = array();
+                       $indexedSubelements = array();
+                       foreach ( $value as $k => $v ) {
+                               if ( ApiResult::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
+                                       continue;
+                               }
 
-                       if ( isset( $elemValue['_subelements'] ) ) {
-                               foreach ( $elemValue['_subelements'] as $subElemId ) {
-                                       if ( isset( $elemValue[$subElemId] ) && !is_array( $elemValue[$subElemId] ) ) {
-                                               $elemValue[$subElemId] = array( '*' => $elemValue[$subElemId] );
-                                       }
+                               $oldv = $v;
+                               if ( is_bool( $v ) && !in_array( $k, $bcBools, true ) ) {
+                                       $v = $v ? 'true' : 'false';
                                }
-                               unset( $elemValue['_subelements'] );
-                       }
 
-                       $indElements = array();
-                       $subElements = array();
-                       foreach ( $elemValue as $subElemId => & $subElemValue ) {
-                               if ( is_int( $subElemId ) ) {
-                                       $indElements[] = $subElemValue;
-                                       unset( $elemValue[$subElemId] );
-                               } elseif ( is_array( $subElemValue ) ) {
-                                       $subElements[$subElemId] = $subElemValue;
-                                       unset( $elemValue[$subElemId] );
-                               } elseif ( is_bool( $subElemValue ) ) {
-                                       // treat true as empty string, skip false in xml format
-                                       if ( $subElemValue === true ) {
-                                               $subElemValue = '';
-                                       } else {
-                                               unset( $elemValue[$subElemId] );
+                               if ( $name !== null && $k === $contentKey ) {
+                                       $content = $v;
+                               } elseif ( is_int( $k ) ) {
+                                       $indexedSubelements[$k] = $v;
+                               } elseif ( is_array( $v ) || is_object( $v ) ) {
+                                       $subelements[self::mangleName( $k, $preserveKeys )] = $v;
+                               } elseif ( in_array( $k, $subelementKeys, true ) || $name === null ) {
+                                       $subelements[self::mangleName( $k, $preserveKeys )] = array(
+                                               'content' => $v,
+                                               ApiResult::META_CONTENT => 'content',
+                                               ApiResult::META_TYPE => 'assoc',
+                                       );
+                               } elseif ( is_bool( $oldv ) ) {
+                                       if ( $oldv ) {
+                                               $attributes[self::mangleName( $k, $preserveKeys )] = '';
                                        }
+                               } elseif ( $v !== null ) {
+                                       $attributes[self::mangleName( $k, $preserveKeys )] = $v;
                                }
                        }
 
-                       if ( is_null( $subElemIndName ) && count( $indElements ) ) {
-                               ApiBase::dieDebug( __METHOD__, "($elemName, ...) has integer keys " .
-                                       "without _element value. Use ApiResult::setIndexedTagName()." );
-                       }
-
-                       if ( count( $subElements ) && count( $indElements ) && !is_null( $subElemContent ) ) {
-                               ApiBase::dieDebug( __METHOD__, "($elemName, ...) has content and subelements" );
+                       if ( $content !== null ) {
+                               if ( $subelements || $indexedSubelements ) {
+                                       $subelements[self::mangleName( $contentKey, $preserveKeys )] = array(
+                                               'content' => $content,
+                                               ApiResult::META_CONTENT => 'content',
+                                               ApiResult::META_TYPE => 'assoc',
+                                       );
+                                       $content = null;
+                               } elseif ( is_scalar( $content ) ) {
+                                       // Add xml:space="preserve" to the element so XML parsers
+                                       // will leave whitespace in the content alone
+                                       $attributes += array( 'xml:space' => 'preserve' );
+                               }
                        }
 
-                       if ( !is_null( $subElemContent ) ) {
-                               $retval .= $indstr . Xml::element( $elemName, $elemValue, $subElemContent );
-                       } elseif ( !count( $indElements ) && !count( $subElements ) ) {
-                               $retval .= $indstr . Xml::element( $elemName, $elemValue );
+                       if ( $content !== null ) {
+                               if ( is_scalar( $content ) ) {
+                                       $retval .= $indstr . Xml::element( $name, $attributes, $content );
+                               } else {
+                                       if ( $name !== null ) {
+                                               $retval .= $indstr . Xml::element( $name, $attributes, null );
+                                       }
+                                       $retval .= static::recXmlPrint( null, $content, $indent );
+                                       if ( $name !== null ) {
+                                               $retval .= $indstr . Xml::closeElement( $name );
+                                       }
+                               }
+                       } elseif ( !$indexedSubelements && !$subelements ) {
+                               if ( $name !== null ) {
+                                       $retval .= $indstr . Xml::element( $name, $attributes );
+                               }
                        } else {
-                               $retval .= $indstr . Xml::element( $elemName, $elemValue, null );
-
-                               foreach ( $subElements as $subElemId => & $subElemValue ) {
-                                       $retval .= self::recXmlPrint( $subElemId, $subElemValue, $indent );
+                               if ( $name !== null ) {
+                                       $retval .= $indstr . Xml::element( $name, $attributes, null );
                                }
-
-                               foreach ( $indElements as &$subElemValue ) {
-                                       $retval .= self::recXmlPrint( $subElemIndName, $subElemValue, $indent );
+                               foreach ( $subelements as $k => $v ) {
+                                       $retval .= static::recXmlPrint( $k, $v, $indent );
+                               }
+                               foreach ( $indexedSubelements as $k => $v ) {
+                                       $retval .= static::recXmlPrint( $indexedTagName, $v, $indent,
+                                               $indexSubelements ? array( '_idx' => $k ) : array()
+                                       );
+                               }
+                               if ( $name !== null ) {
+                                       $retval .= $indstr . Xml::closeElement( $name );
                                }
-
-                               $retval .= $indstr . Xml::closeElement( $elemName );
                        }
-               } elseif ( !is_object( $elemValue ) ) {
+               } else {
                        // to make sure null value doesn't produce unclosed element,
-                       // which is what Xml::element( $elemName, null, null ) returns
-                       if ( $elemValue === null ) {
-                               $retval .= $indstr . Xml::element( $elemName );
+                       // which is what Xml::element( $name, null, null ) returns
+                       if ( $value === null ) {
+                               $retval .= $indstr . Xml::element( $name, $attributes );
                        } else {
-                               $retval .= $indstr . Xml::element( $elemName, null, $elemValue );
+                               $retval .= $indstr . Xml::element( $name, $attributes, $value );
                        }
                }
 
                return $retval;
        }
 
+       /**
+        * Mangle XML-invalid names to be valid in XML
+        * @param string $name
+        * @param array $preserveKeys Names to not mangle
+        * @return string Mangled name
+        */
+       private static function mangleName( $name, $preserveKeys = array() ) {
+               static $nsc = null, $nc = null;
+
+               if ( in_array( $name, $preserveKeys, true ) ) {
+                       return $name;
+               }
+
+               if ( $name === '' ) {
+                       return '_';
+               }
+
+               if ( $nsc === null ) {
+                       // Note we omit ':' from $nsc and $nc because it's reserved for XML
+                       // namespacing, and we omit '_' from $nsc (but not $nc) because we
+                       // reserve it.
+                       $nsc = 'A-Za-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}' .
+                               '\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}' .
+                               '\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}';
+                       $nc = $nsc . '_\-.0-9\x{B7}\x{300}-\x{36F}\x{203F}-\x{2040}';
+               }
+
+               if ( preg_match( "/^[$nsc][$nc]*$/uS", $name ) ) {
+                       return $name;
+               }
+
+               return '_' . preg_replace_callback(
+                       "/[^$nc]/uS",
+                       function ( $m ) {
+                               return sprintf( '.%X.', utf8ToCodepoint( $m[0] ) );
+                       },
+                       str_replace( '.', '.2E.', $name )
+               );
+       }
+
        function addXslt() {
                $nt = Title::newFromText( $this->mXslt );
                if ( is_null( $nt ) || !$nt->exists() ) {
index d2d5e7c..00fc12e 100644 (file)
@@ -60,7 +60,7 @@ class ApiHelp extends ApiBase {
                                'mime' => 'text/html',
                                'help' => $html,
                        );
-                       $result->setSubelements( $data, 'help' );
+                       ApiResult::setSubelementsList( $data, 'help' );
                        $result->addValue( null, $this->getModuleName(), $data );
                } else {
                        $result->reset();
index 6fd79f4..7b99921 100644 (file)
@@ -52,7 +52,8 @@ class ApiImageRotate extends ApiBase {
                $params = $this->extractRequestParams();
                $rotation = $params['rotation'];
 
-               $this->getResult()->beginContinuation( $params['continue'], array(), array() );
+               $continuationManager = new ApiContinuationManager( $this, array(), array() );
+               $this->setContinuationManager( $continuationManager );
 
                $pageSet = $this->getPageSet();
                $pageSet->execute();
@@ -122,7 +123,7 @@ class ApiImageRotate extends ApiBase {
                                        $r['result'] = 'Success';
                                } else {
                                        $r['result'] = 'Failure';
-                                       $r['errormessage'] = $this->getResult()->convertStatusToArray( $status );
+                                       $r['errormessage'] = $this->getErrorFormatter()->arrayFromStatus( $status );
                                }
                        } else {
                                $r['result'] = 'Failure';
@@ -131,9 +132,11 @@ class ApiImageRotate extends ApiBase {
                        $result[] = $r;
                }
                $apiResult = $this->getResult();
-               $apiResult->setIndexedTagName( $result, 'page' );
+               ApiResult::setIndexedTagName( $result, 'page' );
                $apiResult->addValue( null, $this->getModuleName(), $result );
-               $apiResult->endContinuation();
+
+               $this->setContinuationManager( null );
+               $continuationManager->setContinuationIntoResult( $apiResult );
        }
 
        /**
index c7dcce8..9d76a46 100644 (file)
@@ -85,7 +85,7 @@ class ApiImport extends ApiBase {
 
                $resultData = $reporter->getData();
                $result = $this->getResult();
-               $result->setIndexedTagName( $resultData, 'page' );
+               ApiResult::setIndexedTagName( $resultData, 'page' );
                $result->addValue( null, $this->getModuleName(), $resultData );
        }
 
index ee1cfa6..465025c 100644 (file)
@@ -140,7 +140,7 @@ class ApiMain extends ApiBase {
         */
        private $mPrinter;
 
-       private $mModuleMgr, $mResult;
+       private $mModuleMgr, $mResult, $mErrorFormatter, $mContinuationManager;
        private $mAction;
        private $mEnableWrite;
        private $mInternalMode, $mSquidMaxage, $mModule;
@@ -218,7 +218,11 @@ class ApiMain extends ApiBase {
 
                Hooks::run( 'ApiMain::moduleManager', array( $this->mModuleMgr ) );
 
-               $this->mResult = new ApiResult( $this );
+               $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
+               $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
+               $this->mResult->setErrorFormatter( $this->mErrorFormatter );
+               $this->mResult->setMainForContinuation( $this );
+               $this->mContinuationManager = null;
                $this->mEnableWrite = $enableWrite;
 
                $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
@@ -242,6 +246,43 @@ class ApiMain extends ApiBase {
                return $this->mResult;
        }
 
+       /**
+        * Get the ApiErrorFormatter object associated with current request
+        * @return ApiErrorFormatter
+        */
+       public function getErrorFormatter() {
+               return $this->mErrorFormatter;
+       }
+
+       /**
+        * Get the continuation manager
+        * @return ApiContinuationManager|null
+        */
+       public function getContinuationManager() {
+               return $this->mContinuationManager;
+       }
+
+       /**
+        * Set the continuation manager
+        * @param ApiContinuationManager|null
+        */
+       public function setContinuationManager( $manager ) {
+               if ( $manager !== null ) {
+                       if ( !$manager instanceof ApiContinuationManager ) {
+                               throw new InvalidArgumentException( __METHOD__ . ': Was passed ' .
+                                       is_object( $manager ) ? get_class( $manager ) : gettype( $manager )
+                               );
+                       }
+                       if ( $this->mContinuationManager !== null ) {
+                               throw new UnexpectedValueException(
+                                       __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
+                                       ' when a manager is already set from ' . $this->mContinuationManager->getSource()
+                               );
+                       }
+               }
+               $this->mContinuationManager = $manager;
+       }
+
        /**
         * Get the API module object. Only works after executeAction()
         *
@@ -785,7 +826,7 @@ class ApiMain extends ApiBase {
                        // User entered incorrect parameters - generate error response
                        $errMessage = $e->getMessageArray();
                        $link = wfExpandUrl( wfScript( 'api' ) );
-                       ApiResult::setContent( $errMessage, "See $link for API usage" );
+                       ApiResult::setContentValue( $errMessage, 'docref', "See $link for API usage" );
                } else {
                        // Something is seriously wrong
                        if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
@@ -799,16 +840,16 @@ class ApiMain extends ApiBase {
                                'info' => '[' . MWExceptionHandler::getLogId( $e ) . '] ' . $info,
                        );
                        if ( $config->get( 'ShowExceptionDetails' ) ) {
-                               ApiResult::setContent(
+                               ApiResult::setContentValue(
                                        $errMessage,
+                                       'trace',
                                        MWExceptionHandler::getRedactedTraceAsString( $e )
                                );
                        }
                }
 
                // Remember all the warnings to re-add them later
-               $oldResult = $result->getData();
-               $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
+               $warnings = $result->getResultData( array( 'warnings' ) );
 
                $result->reset();
                // Re-add the id
@@ -1191,9 +1232,7 @@ class ApiMain extends ApiBase {
                        $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
                }
 
-               $this->getResult()->cleanUpUTF8();
                $printer = $this->mPrinter;
-
                $printer->initPrinter( false );
                $printer->execute();
                $printer->closePrinter();
index b027f33..80317d3 100644 (file)
@@ -47,7 +47,7 @@ class ApiManageTags extends ApiBase {
                        'tag' => $params['tag'],
                );
                if ( !$status->isGood() ) {
-                       $ret['warnings'] = $result->convertStatusToArray( $status, 'warning' );
+                       $ret['warnings'] = $this->getErrorFormatter()->arrayFromStatus( $status, 'warning' );
                }
                if ( $status->value !== null ) {
                        $ret['success'] = '';
diff --git a/includes/api/ApiMessage.php b/includes/api/ApiMessage.php
new file mode 100644 (file)
index 0000000..6717c39
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+/**
+ * Defines an interface for messages with additional machine-readable data for
+ * use by the API, and provides concrete implementations of that interface.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Interface for messages with machine-readable data for use by the API
+ * @since 1.25
+ * @ingroup API
+ */
+interface IApiMessage extends MessageSpecifier {
+       /**
+        * Returns a machine-readable code for use by the API
+        *
+        * The message key is often sufficient, but sometimes there are multiple
+        * messages used for what is really the same underlying condition (e.g.
+        * badaccess-groups and badaccess-group0)
+        * @return string
+        */
+       public function getApiCode();
+
+       /**
+        * Returns additional machine-readable data about the error condition
+        * @return array
+        */
+       public function getApiData();
+
+       /**
+        * Sets the machine-readable code for use by the API
+        * @param string|null $code If null, the message key should be returned by self::getApiCode()
+        * @param array|null $data If non-null, passed to self::setApiData()
+        */
+       public function setApiCode( $code, array $data = null );
+
+       /**
+        * Sets additional machine-readable data about the error condition
+        * @param array $data
+        */
+       public function setApiData( array $data );
+}
+
+/**
+ * Extension of Message implementing IApiMessage
+ * @since 1.25
+ * @ingroup API
+ * @todo: Would be nice to use a Trait here to avoid code duplication
+ */
+class ApiMessage extends Message implements IApiMessage {
+       protected $apiCode = null;
+       protected $apiData = array();
+
+       /**
+        * Create an IApiMessage for the message
+        *
+        * This returns $msg if it's an IApiMessage, calls 'new ApiRawMessage' if
+        * $msg is a RawMessage, or calls 'new ApiMessage' in all other cases.
+        *
+        * @param Message|RawMessage|array|string $msg
+        * @param string|null $code
+        * @param array|null $data
+        * @return ApiMessage
+        */
+       public static function create( $msg, $code = null, array $data = null ) {
+               if ( $msg instanceof IApiMessage ) {
+                       return $msg;
+               } elseif ( $msg instanceof RawMessage ) {
+                       return new ApiRawMessage( $msg, $code, $data );
+               } else {
+                       return new ApiMessage( $msg, $code, $data );
+               }
+       }
+
+       /**
+        * @param Message|string|array $msg
+        *  - Message: is cloned
+        *  - array: first element is $key, rest are $params to Message::__construct
+        *  - string: passed to Message::__construct
+        * @param string|null $code
+        * @param array|null $data
+        * @return ApiMessage
+        */
+       public function __construct( $msg, $code = null, array $data = null ) {
+               if ( $msg instanceof Message ) {
+                       foreach ( get_class_vars( get_class( $this ) ) as $key => $value ) {
+                               if ( isset( $msg->$key ) ) {
+                                       $this->$key = $msg->$key;
+                               }
+                       }
+               } elseif ( is_array( $msg ) ) {
+                       $key = array_shift( $msg );
+                       parent::__construct( $key, $msg );
+               } else {
+                       parent::__construct( $msg );
+               }
+               $this->apiCode = $code;
+               $this->apiData = (array)$data;
+       }
+
+       public function getApiCode() {
+               return $this->apiCode === null ? $this->getKey() : $this->apiCode;
+       }
+
+       public function setApiCode( $code, array $data = null ) {
+               $this->apiCode = $code;
+               if ( $data !== null ) {
+                       $this->setApiData( $data );
+               }
+       }
+
+       public function getApiData() {
+               return $this->apiData;
+       }
+
+       public function setApiData( array $data ) {
+               $this->apiData = $data;
+       }
+}
+
+/**
+ * Extension of RawMessage implementing IApiMessage
+ * @since 1.25
+ * @ingroup API
+ * @todo: Would be nice to use a Trait here to avoid code duplication
+ */
+class ApiRawMessage extends RawMessage implements IApiMessage {
+       protected $apiCode = null;
+       protected $apiData = array();
+
+       /**
+        * @param RawMessage|string|array $msg
+        *  - RawMessage: is cloned
+        *  - array: first element is $key, rest are $params to RawMessage::__construct
+        *  - string: passed to RawMessage::__construct
+        * @param string|null $code
+        * @param array|null $data
+        * @return ApiMessage
+        */
+       public function __construct( $msg, $code = null, array $data = null ) {
+               if ( $msg instanceof RawMessage ) {
+                       foreach ( get_class_vars( get_class( $this ) ) as $key => $value ) {
+                               if ( isset( $msg->$key ) ) {
+                                       $this->$key = $msg->$key;
+                               }
+                       }
+               } elseif ( is_array( $msg ) ) {
+                       $key = array_shift( $msg );
+                       parent::__construct( $key, $msg );
+               } else {
+                       parent::__construct( $msg );
+               }
+               $this->apiCode = $code;
+               $this->apiData = (array)$data;
+       }
+
+       public function getApiCode() {
+               return $this->apiCode === null ? $this->getKey() : $this->apiCode;
+       }
+
+       public function setApiCode( $code, array $data = null ) {
+               $this->apiCode = $code;
+               if ( $data !== null ) {
+                       $this->setApiData( $data );
+               }
+       }
+
+       public function getApiData() {
+               return $this->apiData;
+       }
+
+       public function setApiData( array $data ) {
+               $this->apiData = $data;
+       }
+}
index 7fb6303..0db18e7 100644 (file)
@@ -120,12 +120,12 @@ class ApiMove extends ApiBase {
                if ( $params['movesubpages'] ) {
                        $r['subpages'] = $this->moveSubpages( $fromTitle, $toTitle,
                                $params['reason'], $params['noredirect'] );
-                       $result->setIndexedTagName( $r['subpages'], 'subpage' );
+                       ApiResult::setIndexedTagName( $r['subpages'], 'subpage' );
 
                        if ( $params['movetalk'] ) {
                                $r['subpages-talk'] = $this->moveSubpages( $fromTalk, $toTalk,
                                        $params['reason'], $params['noredirect'] );
-                               $result->setIndexedTagName( $r['subpages-talk'], 'subpage' );
+                               ApiResult::setIndexedTagName( $r['subpages-talk'], 'subpage' );
                        }
                }
 
index f24a03f..33790f9 100644 (file)
@@ -237,23 +237,24 @@ class ApiOpenSearch extends ApiBase {
                                );
                                $items = array();
                                foreach ( $results as $r ) {
-                                       $item = array();
-                                       $result->setContent( $item, $r['title']->getPrefixedText(), 'Text' );
-                                       $result->setContent( $item, $r['url'], 'Url' );
+                                       $item = array(
+                                               'Text' => $r['title']->getPrefixedText(),
+                                               'Url' => $r['url'],
+                                       );
                                        if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) {
-                                               $result->setContent( $item, $r['extract'], 'Description' );
+                                               $item['Description'] = $r['extract'];
                                        }
                                        if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
                                                $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
                                        }
+                                       ApiResult::setSubelementsList( $item, array_keys( $item ) );
                                        $items[] = $item;
                                }
-                               $result->setIndexedTagName( $items, 'Item' );
+                               ApiResult::setIndexedTagName( $items, 'Item' );
                                $result->addValue( null, 'version', '2.0' );
                                $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' );
-                               $query = array();
-                               $result->setContent( $query, strval( $search ) );
-                               $result->addValue( null, 'Query', $query );
+                               $result->addValue( null, 'Query', strval( $search ) );
+                               $result->addSubelementsList( null, 'Query' );
                                $result->addValue( null, 'Section', $items );
                                break;
 
index d462862..02a25fe 100644 (file)
@@ -442,7 +442,7 @@ class ApiPageSet extends ApiBase {
                        $values[] = $r;
                }
                if ( !empty( $values ) && $result ) {
-                       $result->setIndexedTagName( $values, 'r' );
+                       ApiResult::setIndexedTagName( $values, 'r' );
                }
 
                return $values;
@@ -473,7 +473,7 @@ class ApiPageSet extends ApiBase {
                        );
                }
                if ( !empty( $values ) && $result ) {
-                       $result->setIndexedTagName( $values, 'n' );
+                       ApiResult::setIndexedTagName( $values, 'n' );
                }
 
                return $values;
@@ -504,7 +504,7 @@ class ApiPageSet extends ApiBase {
                        );
                }
                if ( !empty( $values ) && $result ) {
-                       $result->setIndexedTagName( $values, 'c' );
+                       ApiResult::setIndexedTagName( $values, 'c' );
                }
 
                return $values;
@@ -541,7 +541,7 @@ class ApiPageSet extends ApiBase {
                        $values[] = $item;
                }
                if ( !empty( $values ) && $result ) {
-                       $result->setIndexedTagName( $values, 'i' );
+                       ApiResult::setIndexedTagName( $values, 'i' );
                }
 
                return $values;
@@ -633,7 +633,7 @@ class ApiPageSet extends ApiBase {
                        );
                }
                if ( !empty( $values ) && $result ) {
-                       $result->setIndexedTagName( $values, 'rev' );
+                       ApiResult::setIndexedTagName( $values, 'rev' );
                }
 
                return $values;
@@ -1188,17 +1188,20 @@ class ApiPageSet extends ApiBase {
         */
        public function populateGeneratorData( &$result, array $path = array() ) {
                if ( $result instanceof ApiResult ) {
-                       $data = $result->getData();
+                       $data = $result->getResultData( $path );
+                       if ( $data === null ) {
+                               return true;
+                       }
                } else {
                        $data = &$result;
-               }
-               foreach ( $path as $key ) {
-                       if ( !isset( $data[$key] ) ) {
-                               // Path isn't in $result, so nothing to add, so everything
-                               // "fits"
-                               return true;
+                       foreach ( $path as $key ) {
+                               if ( !isset( $data[$key] ) ) {
+                                       // Path isn't in $result, so nothing to add, so everything
+                                       // "fits"
+                                       return true;
+                               }
+                               $data = &$data[$key];
                        }
-                       $data = &$data[$key];
                }
                foreach ( $this->mGeneratorData as $ns => $dbkeys ) {
                        if ( $ns === -1 ) {
index bb4967b..bb661b2 100644 (file)
@@ -105,7 +105,7 @@ class ApiParamInfo extends ApiBase {
                $result->addValue( array( $this->getModuleName() ), 'helpformat', $this->helpFormat );
 
                foreach ( $res as $key => $stuff ) {
-                       $result->setIndexedTagName( $res[$key], 'module' );
+                       ApiResult::setIndexedTagName( $res[$key], 'module' );
                }
 
                if ( $params['mainmodule'] ) {
@@ -171,7 +171,7 @@ class ApiParamInfo extends ApiBase {
                                        }
                                        $res[$key][] = $a;
                                }
-                               $this->getResult()->setIndexedTagName( $res[$key], 'msg' );
+                               ApiResult::setIndexedTagName( $res[$key], 'msg' );
                                break;
                }
        }
@@ -223,7 +223,7 @@ class ApiParamInfo extends ApiBase {
                if ( isset( $ret['helpurls'][0] ) && $ret['helpurls'][0] === false ) {
                        $ret['helpurls'] = array();
                }
-               $result->setIndexedTagName( $ret['helpurls'], 'helpurl' );
+               ApiResult::setIndexedTagName( $ret['helpurls'], 'helpurl' );
 
                if ( $this->helpFormat !== 'none' ) {
                        $ret['examples'] = array();
@@ -242,12 +242,12 @@ class ApiParamInfo extends ApiBase {
                                        if ( is_array( $item['description'] ) ) {
                                                $item['description'] = $item['description'][0];
                                        } else {
-                                               $result->setSubelements( $item, 'description' );
+                                               ApiResult::setSubelementsList( $item, 'description' );
                                        }
                                }
                                $ret['examples'][] = $item;
                        }
-                       $result->setIndexedTagName( $ret['examples'], 'example' );
+                       ApiResult::setIndexedTagName( $ret['examples'], 'example' );
                }
 
                $ret['parameters'] = array();
@@ -331,7 +331,7 @@ class ApiParamInfo extends ApiBase {
                                if ( is_array( $item['type'] ) ) {
                                        // To prevent sparse arrays from being serialized to JSON as objects
                                        $item['type'] = array_values( $item['type'] );
-                                       $result->setIndexedTagName( $item['type'], 't' );
+                                       ApiResult::setIndexedTagName( $item['type'], 't' );
                                }
                        }
                        if ( isset( $settings[ApiBase::PARAM_MAX] ) ) {
@@ -353,7 +353,7 @@ class ApiParamInfo extends ApiBase {
                                        );
                                        if ( count( $i ) ) {
                                                $info['values'] = $i;
-                                               $result->setIndexedTagName( $info['values'], 'v' );
+                                               ApiResult::setIndexedTagName( $info['values'], 'v' );
                                        }
                                        $this->formatHelpMessages( $info, 'text', array(
                                                $this->context->msg( "apihelp-{$path}-paraminfo-{$tag}" )
@@ -361,15 +361,15 @@ class ApiParamInfo extends ApiBase {
                                                        ->params( $this->context->getLanguage()->commaList( $i ) )
                                                        ->params( $module->getModulePrefix() )
                                        ) );
-                                       $result->setSubelements( $info, 'text' );
+                                       ApiResult::setSubelementsList( $info, 'text' );
                                        $item['info'][] = $info;
                                }
-                               $result->setIndexedTagName( $item['info'], 'i' );
+                               ApiResult::setIndexedTagName( $item['info'], 'i' );
                        }
 
                        $ret['parameters'][] = $item;
                }
-               $result->setIndexedTagName( $ret['parameters'], 'param' );
+               ApiResult::setIndexedTagName( $ret['parameters'], 'param' );
 
                return $ret;
        }
index b565dea..267bced 100644 (file)
@@ -127,7 +127,6 @@ class ApiParse extends ApiBase {
                        } else { // Not $oldid, but $pageid or $page
                                if ( $params['redirects'] ) {
                                        $reqParams = array(
-                                               'action' => 'query',
                                                'redirects' => '',
                                        );
                                        if ( !is_null( $pageid ) ) {
@@ -137,14 +136,12 @@ class ApiParse extends ApiBase {
                                        }
                                        $req = new FauxRequest( $reqParams );
                                        $main = new ApiMain( $req );
-                                       $main->execute();
-                                       $data = $main->getResultData();
-                                       $redirValues = isset( $data['query']['redirects'] )
-                                               ? $data['query']['redirects']
-                                               : array();
+                                       $pageSet = new ApiPageSet( $main );
+                                       $pageSet->execute();
+
                                        $to = $page;
-                                       foreach ( (array)$redirValues as $r ) {
-                                               $to = $r['to'];
+                                       foreach ( $pageSet->getRedirectTitles() as $title ) {
+                                               $to = $title->getFullText();
                                        }
                                        $pageParams = array( 'title' => $to );
                                } elseif ( !is_null( $pageid ) ) {
@@ -229,16 +226,20 @@ class ApiParse extends ApiBase {
                                // Build a result and bail out
                                $result_array = array();
                                $result_array['text'] = array();
-                               ApiResult::setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
+                               ApiResult::setContentValue( $result_array['text'], 'text', $this->pstContent->serialize( $format ) );
                                if ( isset( $prop['wikitext'] ) ) {
                                        $result_array['wikitext'] = array();
-                                       ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
+                                       ApiResult::setContentValue( $result_array['wikitext'], 'wikitext', $this->content->serialize( $format ) );
                                }
                                if ( !is_null( $params['summary'] ) ||
                                        ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
                                ) {
                                        $result_array['parsedsummary'] = array();
-                                       ApiResult::setContent( $result_array['parsedsummary'], $this->formatSummary( $titleObj, $params ) );
+                                       ApiResult::setContentValue(
+                                               $result_array['parsedsummary'],
+                                               'parsedsummary',
+                                               $this->formatSummary( $titleObj, $params )
+                                       );
                                }
 
                                $result->addValue( null, $this->getModuleName(), $result_array );
@@ -272,14 +273,18 @@ class ApiParse extends ApiBase {
 
                if ( isset( $prop['text'] ) ) {
                        $result_array['text'] = array();
-                       ApiResult::setContent( $result_array['text'], $p_result->getText() );
+                       ApiResult::setContentValue( $result_array['text'], 'text', $p_result->getText() );
                }
 
                if ( !is_null( $params['summary'] ) ||
                        ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
                ) {
                        $result_array['parsedsummary'] = array();
-                       ApiResult::setContent( $result_array['parsedsummary'], $this->formatSummary( $titleObj, $params ) );
+                       ApiResult::setContentValue(
+                               $result_array['parsedsummary'],
+                               'parsedsummary',
+                               $this->formatSummary( $titleObj, $params )
+                       );
                }
 
                if ( isset( $prop['langlinks'] ) ) {
@@ -304,7 +309,7 @@ class ApiParse extends ApiBase {
                if ( isset( $prop['categorieshtml'] ) ) {
                        $categoriesHtml = $this->categoriesHtml( $p_result->getCategories() );
                        $result_array['categorieshtml'] = array();
-                       ApiResult::setContent( $result_array['categorieshtml'], $categoriesHtml );
+                       ApiResult::setContentValue( $result_array['categorieshtml'], 'categorieshtml', $categoriesHtml );
                }
                if ( isset( $prop['links'] ) ) {
                        $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
@@ -345,8 +350,9 @@ class ApiParse extends ApiBase {
 
                        if ( isset( $prop['headhtml'] ) ) {
                                $result_array['headhtml'] = array();
-                               ApiResult::setContent(
+                               ApiResult::setContentValue(
                                        $result_array['headhtml'],
+                                       'headhtml',
                                        $context->getOutput()->headElement( $context->getSkin() )
                                );
                        }
@@ -362,7 +368,7 @@ class ApiParse extends ApiBase {
                if ( isset( $prop['indicators'] ) ) {
                        foreach ( $p_result->getIndicators() as $name => $content ) {
                                $indicator = array( 'name' => $name );
-                               ApiResult::setContent( $indicator, $content );
+                               ApiResult::setContentValue( $indicator, 'content', $content );
                                $result_array['indicators'][] = $indicator;
                        }
                }
@@ -373,10 +379,10 @@ class ApiParse extends ApiBase {
 
                if ( isset( $prop['wikitext'] ) ) {
                        $result_array['wikitext'] = array();
-                       ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
+                       ApiResult::setContentValue( $result_array['wikitext'], 'wikitext', $this->content->serialize( $format ) );
                        if ( !is_null( $this->pstContent ) ) {
                                $result_array['psttext'] = array();
-                               ApiResult::setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
+                               ApiResult::setContentValue( $result_array['psttext'], 'psttext', $this->pstContent->serialize( $format ) );
                        }
                }
                if ( isset( $prop['properties'] ) ) {
@@ -390,7 +396,7 @@ class ApiParse extends ApiBase {
                if ( isset( $prop['limitreporthtml'] ) ) {
                        $limitreportHtml = EditPage::getPreviewLimitReport( $p_result );
                        $result_array['limitreporthtml'] = array();
-                       ApiResult::setContent( $result_array['limitreporthtml'], $limitreportHtml );
+                       ApiResult::setContentValue( $result_array['limitreporthtml'], 'limitreporthtml', $limitreportHtml );
                }
 
                if ( $params['generatexml'] ) {
@@ -406,7 +412,7 @@ class ApiParse extends ApiBase {
                                $xml = $dom->__toString();
                        }
                        $result_array['parsetree'] = array();
-                       ApiResult::setContent( $result_array['parsetree'], $xml );
+                       ApiResult::setContentValue( $result_array['parsetree'], 'parsetree', $xml );
                }
 
                $result_mapping = array(
@@ -546,7 +552,7 @@ class ApiParse extends ApiBase {
                                // native language name
                                $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
                        }
-                       ApiResult::setContent( $entry, $bits[1] );
+                       ApiResult::setContentValue( $entry, 'title', $bits[1] );
                        $result[] = $entry;
                }
 
@@ -581,7 +587,7 @@ class ApiParse extends ApiBase {
                foreach ( $links as $link => $sortkey ) {
                        $entry = array();
                        $entry['sortkey'] = $sortkey;
-                       ApiResult::setContent( $entry, $link );
+                       ApiResult::setContentValue( $entry, 'category', $link );
                        if ( !isset( $hiddencats[$link] ) ) {
                                $entry['missing'] = '';
                        } elseif ( $hiddencats[$link] ) {
@@ -606,7 +612,7 @@ class ApiParse extends ApiBase {
                        foreach ( $nslinks as $title => $id ) {
                                $entry = array();
                                $entry['ns'] = $ns;
-                               ApiResult::setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() );
+                               ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
                                if ( $id != 0 ) {
                                        $entry['exists'] = '';
                                }
@@ -629,7 +635,7 @@ class ApiParse extends ApiBase {
                                        $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
                                }
 
-                               ApiResult::setContent( $entry, $title->getFullText() );
+                               ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
                                $result[] = $entry;
                        }
                }
@@ -642,7 +648,7 @@ class ApiParse extends ApiBase {
                foreach ( $headItems as $tag => $content ) {
                        $entry = array();
                        $entry['tag'] = $tag;
-                       ApiResult::setContent( $entry, $content );
+                       ApiResult::setContentValue( $entry, 'content', $content );
                        $result[] = $entry;
                }
 
@@ -654,7 +660,7 @@ class ApiParse extends ApiBase {
                foreach ( $properties as $name => $value ) {
                        $entry = array();
                        $entry['name'] = $name;
-                       ApiResult::setContent( $entry, $value );
+                       ApiResult::setContentValue( $entry, 'value', $value );
                        $result[] = $entry;
                }
 
@@ -666,7 +672,7 @@ class ApiParse extends ApiBase {
                foreach ( $css as $file => $link ) {
                        $entry = array();
                        $entry['file'] = $file;
-                       ApiResult::setContent( $entry, $link );
+                       ApiResult::setContentValue( $entry, 'link', $link );
                        $result[] = $entry;
                }
 
@@ -683,8 +689,8 @@ class ApiParse extends ApiBase {
                        if ( !is_array( $value ) ) {
                                $value = array( $value );
                        }
-                       $apiResult->setIndexedTagName( $value, 'param' );
-                       $apiResult->setIndexedTagName_recursive( $value, 'param' );
+                       ApiResult::setIndexedTagName( $value, 'param' );
+                       ApiResult::setIndexedTagNameOnSubarrays( $value, 'param' );
                        $entry = array_merge( $entry, $value );
                        $result[] = $entry;
                }
@@ -695,7 +701,7 @@ class ApiParse extends ApiBase {
        private function setIndexedTagNames( &$array, $mapping ) {
                foreach ( $mapping as $key => $name ) {
                        if ( isset( $array[$key] ) ) {
-                               $this->getResult()->setIndexedTagName( $array[$key], $name );
+                               ApiResult::setIndexedTagName( $array[$key], $name );
                        }
                }
        }
index 675aa58..496db8f 100644 (file)
@@ -127,7 +127,7 @@ class ApiProtect extends ApiBase {
                }
                $res['protections'] = $resultProtections;
                $result = $this->getResult();
-               $result->setIndexedTagName( $res['protections'], 'protection' );
+               ApiResult::setIndexedTagName( $res['protections'], 'protection' );
                $result->addValue( null, $this->getModuleName(), $res );
        }
 
index ec55137..67f7834 100644 (file)
@@ -38,7 +38,8 @@ class ApiPurge extends ApiBase {
        public function execute() {
                $params = $this->extractRequestParams();
 
-               $this->getResult()->beginContinuation( $params['continue'], array(), array() );
+               $continuationManager = new ApiContinuationManager( $this, array(), array() );
+               $this->setContinuationManager( $continuationManager );
 
                $forceLinkUpdate = $params['forcelinkupdate'];
                $forceRecursiveLinkUpdate = $params['forcerecursivelinkupdate'];
@@ -89,7 +90,7 @@ class ApiPurge extends ApiBase {
                        $result[] = $r;
                }
                $apiResult = $this->getResult();
-               $apiResult->setIndexedTagName( $result, 'page' );
+               ApiResult::setIndexedTagName( $result, 'page' );
                $apiResult->addValue( null, $this->getModuleName(), $result );
 
                $values = $pageSet->getNormalizedTitlesAsResult( $apiResult );
@@ -105,7 +106,8 @@ class ApiPurge extends ApiBase {
                        $apiResult->addValue( null, 'redirects', $values );
                }
 
-               $apiResult->endContinuation();
+               $this->setContinuationManager( null );
+               $continuationManager->setContinuationIntoResult( $apiResult );
        }
 
        /**
index ac89419..b1069c7 100644 (file)
@@ -257,11 +257,11 @@ class ApiQuery extends ApiBase {
                $this->instantiateModules( $allModules, 'meta' );
 
                // Filter modules based on continue parameter
-               list( $generatorDone, $modules ) = $this->getResult()->beginContinuation(
-                       $this->mParams['continue'], $allModules, $propModules
-               );
+               $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules );
+               $this->setContinuationManager( $continuationManager );
+               $modules = $continuationManager->getRunModules();
 
-               if ( !$generatorDone ) {
+               if ( !$continuationManager->isGeneratorDone() ) {
                        // Query modules may optimize data requests through the $this->getPageSet()
                        // object by adding extra fields from the page table.
                        foreach ( $modules as $module ) {
@@ -291,12 +291,19 @@ class ApiQuery extends ApiBase {
                $this->getMain()->setCacheMode( $cacheMode );
 
                // Write the continuation data into the result
-               $this->getResult()->endContinuation(
-                       $this->mParams['continue'] === null ? 'raw' : 'standard'
-               );
+               $this->setContinuationManager( null );
+               if ( $this->mParams['continue'] === null ) {
+                       $data = $continuationManager->getRawContinuation();
+                       if ( $data ) {
+                               $this->getResult()->addValue( null, 'query-continue', $data,
+                                       ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+                       }
+               } else {
+                       $continuationManager->setContinuationIntoResult( $this->getResult() );
+               }
 
                if ( $this->mParams['continue'] === null && !$this->mParams['rawcontinue'] &&
-                       array_key_exists( 'query-continue', $this->getResult()->getData() )
+                       $this->getResult()->getResultData( 'query-continue' ) !== null
                ) {
                        $this->logFeatureUsage( 'action=query&!rawcontinue&!continue' );
                        $this->setWarning(
@@ -443,11 +450,11 @@ class ApiQuery extends ApiBase {
                                $pageIDs = array_keys( $pages );
                                // json treats all map keys as strings - converting to match
                                $pageIDs = array_map( 'strval', $pageIDs );
-                               $result->setIndexedTagName( $pageIDs, 'id' );
+                               ApiResult::setIndexedTagName( $pageIDs, 'id' );
                                $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs );
                        }
 
-                       $result->setIndexedTagName( $pages, 'page' );
+                       ApiResult::setIndexedTagName( $pages, 'page' );
                        $fit = $fit && $result->addValue( 'query', 'pages', $pages );
                }
 
@@ -476,7 +483,7 @@ class ApiQuery extends ApiBase {
         */
        public function setGeneratorContinue( $module, $paramName, $paramValue ) {
                wfDeprecated( __METHOD__, '1.24' );
-               $this->getResult()->setGeneratorContinueParam( $module, $paramName, $paramValue );
+               $this->getContinuationManager()->addGeneratorContinueParam( $module, $paramName, $paramValue );
                return $this->getParameter( 'continue' ) !== null;
        }
 
@@ -519,7 +526,7 @@ class ApiQuery extends ApiBase {
                        $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
                } else {
                        $r = array();
-                       ApiResult::setContent( $r, $exportxml );
+                       ApiResult::setContentValue( $r, 'xml', $exportxml );
                        $result->addValue( 'query', 'export', $r, ApiResult::NO_SIZE_CHECK );
                }
        }
index 672c234..4ecc4b7 100644 (file)
@@ -128,7 +128,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
                                $pages[] = $titleObj;
                        } else {
                                $item = array();
-                               ApiResult::setContent( $item, $titleObj->getText() );
+                               ApiResult::setContentValue( $item, 'category', $titleObj->getText() );
                                if ( isset( $prop['size'] ) ) {
                                        $item['size'] = intval( $row->cat_pages );
                                        $item['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files;
@@ -147,7 +147,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'c' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'c' );
                } else {
                        $resultPageSet->populateFromTitles( $pages );
                }
index 4e95f5b..4e4d2af 100644 (file)
@@ -319,7 +319,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                                                'pageid' => $title->getArticleID(),
                                                'revisions' => array( $rev ),
                                        );
-                                       $result->setIndexedTagName( $a['revisions'], 'rev' );
+                                       ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
                                        ApiQueryBase::addTitleInfo( $a, $title );
                                        $fit = $result->addValue( array( 'query', $this->getModuleName() ), $index, $a );
                                } else {
@@ -348,7 +348,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                                $resultPageSet->populateFromRevisionIDs( $generated );
                        }
                } else {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'page' );
                }
        }
 
index 6c962cd..381938b 100644 (file)
@@ -308,7 +308,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'img' );
                } else {
                        $resultPageSet->populateFromTitles( $titles );
                }
index a70d019..1a4a4d9 100644 (file)
@@ -230,7 +230,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $this->indexTag );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), $this->indexTag );
                } elseif ( $params['unique'] ) {
                        $resultPageSet->populateFromTitles( $titles );
                } else {
index 98552ba..00816a3 100644 (file)
@@ -165,7 +165,7 @@ class ApiQueryAllMessages extends ApiQueryBase {
                                                $msgString = $msg->plain();
                                        }
                                        if ( !$params['nocontent'] ) {
-                                               ApiResult::setContent( $a, $msgString );
+                                               ApiResult::setContentValue( $a, 'content', $msgString );
                                        }
                                        if ( isset( $prop['default'] ) ) {
                                                $default = wfMessage( $message )->inLanguage( $langObj )->useDatabase( false );
@@ -183,7 +183,7 @@ class ApiQueryAllMessages extends ApiQueryBase {
                                }
                        }
                }
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'message' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'message' );
        }
 
        public function getCacheMode( $params ) {
index e243593..0149ad2 100644 (file)
@@ -234,7 +234,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'p' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'p' );
                }
        }
 
index 1c3f9fb..8c9e1ba 100644 (file)
@@ -100,7 +100,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
 
                        // no group with the given right(s) exists, no need for a query
                        if ( !count( $groups ) ) {
-                               $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), '' );
+                               $this->getResult()->addIndexedTagName( array( 'query', $this->getModuleName() ), '' );
 
                                return;
                        }
@@ -279,17 +279,17 @@ class ApiQueryAllUsers extends ApiQueryBase {
 
                                if ( $fld_groups ) {
                                        $data['groups'] = $groups;
-                                       $result->setIndexedTagName( $data['groups'], 'g' );
+                                       ApiResult::setIndexedTagName( $data['groups'], 'g' );
                                }
 
                                if ( $fld_implicitgroups ) {
                                        $data['implicitgroups'] = $implicitGroups;
-                                       $result->setIndexedTagName( $data['implicitgroups'], 'g' );
+                                       ApiResult::setIndexedTagName( $data['implicitgroups'], 'g' );
                                }
 
                                if ( $fld_rights ) {
                                        $data['rights'] = User::getGroupPermissions( $groups );
-                                       $result->setIndexedTagName( $data['rights'], 'r' );
+                                       ApiResult::setIndexedTagName( $data['rights'], 'r' );
                                }
                        }
 
@@ -300,7 +300,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
                        }
                }
 
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'u' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'u' );
        }
 
        public function getCacheMode( $params ) {
index 5e17a5c..92cf62d 100644 (file)
@@ -338,7 +338,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
 
                if ( $this->params['limit'] == 'max' ) {
                        $this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
-                       $result->setParsedLimit( $this->getModuleName(), $this->params['limit'] );
+                       $result->addParsedLimit( $this->getModuleName(), $this->params['limit'] );
                } else {
                        $this->params['limit'] = intval( $this->params['limit'] );
                        $this->validateLimit( 'limit', $this->params['limit'], 1, $userMax, $botMax );
@@ -426,7 +426,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
                        $data = array_map( function ( $arr ) use ( $result, $code ) {
                                if ( isset( $arr['redirlinks'] ) ) {
                                        $arr['redirlinks'] = array_values( $arr['redirlinks'] );
-                                       $result->setIndexedTagName( $arr['redirlinks'], $code );
+                                       ApiResult::setIndexedTagName( $arr['redirlinks'], $code );
                                }
                                return $arr;
                        }, array_values( $this->resultArr ) );
@@ -482,7 +482,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
                                                $hasRedirs = true;
                                        }
                                        if ( $hasRedirs ) {
-                                               $result->setIndexedTagName_internal(
+                                               $result->addIndexedTagName(
                                                        array( 'query', $this->getModuleName(), $idx, 'redirlinks' ),
                                                        $this->bl_code );
                                        }
@@ -494,7 +494,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
                                }
                        }
 
-                       $result->setIndexedTagName_internal(
+                       $result->addIndexedTagName(
                                array( 'query', $this->getModuleName() ),
                                $this->bl_code
                        );
index 6c9d999..89e92b8 100644 (file)
@@ -457,7 +457,7 @@ abstract class ApiQueryBase extends ApiBase {
         */
        protected function addPageSubItems( $pageId, $data ) {
                $result = $this->getResult();
-               $result->setIndexedTagName( $data, $this->getModulePrefix() );
+               ApiResult::setIndexedTagName( $data, $this->getModulePrefix() );
 
                return $result->addValue( array( 'query', 'pages', intval( $pageId ) ),
                        $this->getModuleName(),
@@ -482,7 +482,7 @@ abstract class ApiQueryBase extends ApiBase {
                if ( !$fit ) {
                        return false;
                }
-               $result->setIndexedTagName_internal( array( 'query', 'pages', $pageId,
+               $result->addIndexedTagName( array( 'query', 'pages', $pageId,
                        $this->getModuleName() ), $elemname );
 
                return true;
@@ -494,7 +494,7 @@ abstract class ApiQueryBase extends ApiBase {
         * @param string|array $paramValue Parameter value
         */
        protected function setContinueEnumParameter( $paramName, $paramValue ) {
-               $this->getResult()->setContinueParam( $this, $paramName, $paramValue );
+               $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
        }
 
        /**
@@ -711,7 +711,7 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
         */
        protected function setContinueEnumParameter( $paramName, $paramValue ) {
                if ( $this->mGeneratorPageSet !== null ) {
-                       $this->getResult()->setGeneratorContinueParam( $this, $paramName, $paramValue );
+                       $this->getContinuationManager()->addGeneratorContinueParam( $this, $paramName, $paramValue );
                } else {
                        parent::setContinueEnumParameter( $paramName, $paramValue );
                }
index f6bde41..72e4fef 100644 (file)
@@ -246,7 +246,7 @@ class ApiQueryBlocks extends ApiQueryBase {
                                break;
                        }
                }
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'block' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'block' );
        }
 
        protected function prepareUsername( $user ) {
index a6fc223..82ab939 100644 (file)
@@ -285,7 +285,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal(
+                       $result->addIndexedTagName(
                                array( 'query', $this->getModuleName() ), 'cm' );
                }
        }
index f46fb34..b2c59d8 100644 (file)
@@ -177,7 +177,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
 
                if ( $limit == 'max' ) {
                        $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
-                       $this->getResult()->setParsedLimit( $this->getModuleName(), $limit );
+                       $this->getResult()->addParsedLimit( $this->getModuleName(), $limit );
                }
 
                $this->validateLimit( 'limit', $limit, 1, $userMax, $botMax );
@@ -376,9 +376,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) {
                                        if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                                                // Pre-1.5 ar_text row (if condition from Revision::newFromArchiveRow)
-                                               ApiResult::setContent( $rev, Revision::getRevisionText( $row, 'ar_' ) );
+                                               ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row, 'ar_' ) );
                                        } else {
-                                               ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
+                                               ApiResult::setContentValue( $rev, 'text', Revision::getRevisionText( $row ) );
                                        }
                                }
                        }
@@ -386,7 +386,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        if ( $fld_tags ) {
                                if ( $row->ts_tags ) {
                                        $tags = explode( ',', $row->ts_tags );
-                                       $this->getResult()->setIndexedTagName( $tags, 'tag' );
+                                       ApiResult::setIndexedTagName( $tags, 'tag' );
                                        $rev['tags'] = $tags;
                                } else {
                                        $rev['tags'] = array();
@@ -401,7 +401,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                $pageID = $newPageID++;
                                $pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
                                $a['revisions'] = array( $rev );
-                               $result->setIndexedTagName( $a['revisions'], 'rev' );
+                               ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
                                $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
                                ApiQueryBase::addTitleInfo( $a, $title );
                                if ( $fld_token ) {
@@ -425,7 +425,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                break;
                        }
                }
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'page' );
        }
 
        public function isDeprecated() {
index e77355b..a26eff2 100644 (file)
@@ -139,7 +139,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ),
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ),
                                $this->getModulePrefix() );
                }
        }
index 6ddb6c8..ec3d9d2 100644 (file)
@@ -91,7 +91,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
                        if ( $params['expandurl'] ) {
                                $to = wfExpandUrl( $to, PROTO_CANONICAL );
                        }
-                       ApiResult::setContent( $entry, $to );
+                       ApiResult::setContentValue( $entry, 'url', $to );
                        $fit = $this->addPageSubItem( $row->el_from, $entry );
                        if ( !$fit ) {
                                $this->setContinueEnumParameter( 'offset', $offset + $count - 1 );
index 39c5902..8896140 100644 (file)
@@ -55,7 +55,7 @@ class ApiQueryFileRepoInfo extends ApiQueryBase {
                $repos[] = array_intersect_key( $repoGroup->getLocalRepo()->getInfo(), $props );
 
                $result = $this->getResult();
-               $result->setIndexedTagName( $repos, 'repo' );
+               ApiResult::setIndexedTagName( $repos, 'repo' );
                $result->addValue( array( 'query' ), 'repos', $repos );
        }
 
index 6b92603..cba3b73 100644 (file)
@@ -240,7 +240,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
                        }
                }
 
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'fa' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'fa' );
        }
 
        public function getAllowedParams() {
index a2af124..61928c3 100644 (file)
@@ -155,7 +155,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'iw' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'iw' );
                } else {
                        $resultPageSet->populateFromTitles( $pages );
                }
index c1208cb..aca3f70 100644 (file)
@@ -129,7 +129,7 @@ class ApiQueryIWLinks extends ApiQueryBase {
                                }
                        }
 
-                       ApiResult::setContent( $entry, $row->iwl_title );
+                       ApiResult::setContentValue( $entry, 'title', $row->iwl_title );
                        $fit = $this->addPageSubItem( $row->iwl_from, $entry );
                        if ( !$fit ) {
                                $this->setContinueEnumParameter(
index c4ca5d6..d5da495 100644 (file)
@@ -599,7 +599,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                $retval[] = $r;
                        }
                }
-               $result->setIndexedTagName( $retval, 'metadata' );
+               ApiResult::setIndexedTagName( $retval, 'metadata' );
 
                return $retval;
        }
index 5af44ee..db28df7 100644 (file)
@@ -421,14 +421,14 @@ class ApiQueryInfo extends ApiQueryBase {
                                $pageInfo['protection'] =
                                        $this->protections[$ns][$dbkey];
                        }
-                       $this->getResult()->setIndexedTagName( $pageInfo['protection'], 'pr' );
+                       ApiResult::setIndexedTagName( $pageInfo['protection'], 'pr' );
 
                        $pageInfo['restrictiontypes'] = array();
                        if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
                                $pageInfo['restrictiontypes'] =
                                        $this->restrictionTypes[$ns][$dbkey];
                        }
-                       $this->getResult()->setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
+                       ApiResult::setIndexedTagName( $pageInfo['restrictiontypes'], 'rt' );
                }
 
                if ( $this->fld_watched && isset( $this->watched[$ns][$dbkey] ) ) {
index b41b4b7..885d10c 100644 (file)
@@ -154,7 +154,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'll' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'll' );
                } else {
                        $resultPageSet->populateFromTitles( $pages );
                }
index 2d03347..5919ee9 100644 (file)
@@ -124,7 +124,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
                        if ( isset( $prop['autonym'] ) ) {
                                $entry['autonym'] = Language::fetchLanguageName( $row->ll_lang );
                        }
-                       ApiResult::setContent( $entry, $row->ll_title );
+                       ApiResult::setContentValue( $entry, 'title', $row->ll_title );
                        $fit = $this->addPageSubItem( $row->ll_from, $entry );
                        if ( !$fit ) {
                                $this->setContinueEnumParameter( 'continue', "{$row->ll_from}|{$row->ll_lang}" );
index bbb8060..a626c8a 100644 (file)
@@ -239,7 +239,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
                                break;
                        }
                }
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'item' );
        }
 
        /**
@@ -387,8 +387,8 @@ class ApiQueryLogEvents extends ApiQueryBase {
                                $logParam = explode( ':', $key, 3 );
                                $logParams[$logParam[2]] = $value;
                        }
-                       $result->setIndexedTagName( $logParams, 'param' );
-                       $result->setIndexedTagName_recursive( $logParams, 'param' );
+                       ApiResult::setIndexedTagName( $logParams, 'param' );
+                       ApiResult::setIndexedTagNameOnSubarrays( $logParams, 'param' );
                        $vals = array_merge( $vals, $logParams );
                }
 
@@ -482,7 +482,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
                if ( $this->fld_tags ) {
                        if ( $row->ts_tags ) {
                                $tags = explode( ',', $row->ts_tags );
-                               $this->getResult()->setIndexedTagName( $tags, 'tag' );
+                               ApiResult::setIndexedTagName( $tags, 'tag' );
                                $vals['tags'] = $tags;
                        } else {
                                $vals['tags'] = array();
index 035f901..dc10c91 100644 (file)
@@ -205,7 +205,7 @@ abstract class ApiQueryORM extends ApiQueryBase {
         * @param array $serializedResults
         */
        protected function setIndexedTagNames( array &$serializedResults ) {
-               $this->getResult()->setIndexedTagName( $serializedResults, $this->getRowName() );
+               ApiResult::setIndexedTagName( $serializedResults, $this->getRowName() );
        }
 
        /**
index 026f061..11a29ff 100644 (file)
@@ -78,7 +78,7 @@ class ApiQueryPagePropNames extends ApiQueryBase {
                        }
                }
 
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'p' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'p' );
        }
 
        public function getAllowedParams() {
index 6ffe0ae..143bc06 100644 (file)
@@ -121,7 +121,7 @@ class ApiQueryPagesWithProp extends ApiQueryGeneratorBase {
                }
 
                if ( $resultPageSet === null ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'page' );
                }
        }
 
index 7a31c48..35942ca 100644 (file)
@@ -79,7 +79,7 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase {
                                        break;
                                }
                        }
-                       $result->setIndexedTagName_internal(
+                       $result->addIndexedTagName(
                                array( 'query', $this->getModuleName() ), $this->getModulePrefix()
                        );
                }
index f1e6d01..fb65e5e 100644 (file)
@@ -156,7 +156,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal(
+                       $result->addIndexedTagName(
                                array( 'query', $this->getModuleName() ),
                                $this->getModulePrefix()
                        );
index 74586bb..062a44f 100644 (file)
@@ -119,7 +119,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
                        }
                }
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal(
+                       $result->addIndexedTagName(
                                array( 'query', $this->getModuleName(), 'results' ),
                                'page'
                        );
index 282f498..a2c2844 100644 (file)
@@ -131,7 +131,7 @@ class ApiQueryRandom extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'page' );
                }
        }
 
index aa22264..3dbfdf9 100644 (file)
@@ -399,7 +399,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
 
                if ( is_null( $resultPageSet ) ) {
                        /* Format the result */
-                       $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'rc' );
+                       $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'rc' );
                } else {
                        $resultPageSet->populateFromTitles( $titles );
                }
@@ -551,7 +551,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                if ( $this->fld_tags ) {
                        if ( $row->ts_tags ) {
                                $tags = explode( ',', $row->ts_tags );
-                               $this->getResult()->setIndexedTagName( $tags, 'tag' );
+                               ApiResult::setIndexedTagName( $tags, 'tag' );
                                $vals['tags'] = $tags;
                        } else {
                                $vals['tags'] = array();
index 281f838..1805f40 100644 (file)
@@ -138,7 +138,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                if ( $this->limit == 'max' ) {
                        $this->limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
                        if ( $this->setParsedLimit ) {
-                               $this->getResult()->setParsedLimit( $this->getModuleName(), $this->limit );
+                               $this->getResult()->addParsedLimit( $this->getModuleName(), $this->limit );
                        }
                }
 
@@ -243,7 +243,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                if ( $this->fld_tags ) {
                        if ( $row->ts_tags ) {
                                $tags = explode( ',', $row->ts_tags );
-                               $this->getResult()->setIndexedTagName( $tags, 'tag' );
+                               ApiResult::setIndexedTagName( $tags, 'tag' );
                                $vals['tags'] = $tags;
                        } else {
                                $vals['tags'] = array();
@@ -347,7 +347,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                        }
 
                        if ( $text !== false ) {
-                               ApiResult::setContent( $vals, $text );
+                               ApiResult::setContentValue( $vals, 'content', $text );
                        }
                }
 
@@ -389,7 +389,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                                }
                                if ( $engine ) {
                                        $difftext = $engine->getDiffBody();
-                                       ApiResult::setContent( $vals['diff'], $difftext );
+                                       ApiResult::setContentValue( $vals['diff'], 'body', $difftext );
                                        if ( !$engine->wasCacheHit() ) {
                                                $n++;
                                        }
index e489b2f..e29ef8d 100644 (file)
@@ -252,11 +252,11 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                }
 
                if ( $resultPageSet === null ) {
-                       $apiResult->setIndexedTagName_internal( array(
+                       $apiResult->addIndexedTagName( array(
                                'query', $this->getModuleName()
                        ), 'p' );
                        if ( $hasInterwikiResults ) {
-                               $apiResult->setIndexedTagName_internal( array(
+                               $apiResult->addIndexedTagName( array(
                                        'query', 'interwiki' . $this->getModuleName()
                                ), 'p' );
                        }
index d4f7e6a..22447f7 100644 (file)
@@ -158,7 +158,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                }
                if ( $allowException ) {
                        $data['externalimages'] = (array)$allowFrom;
-                       $this->getResult()->setIndexedTagName( $data['externalimages'], 'prefix' );
+                       ApiResult::setIndexedTagName( $data['externalimages'], 'prefix' );
                }
 
                if ( !$config->get( 'DisableLangConversion' ) ) {
@@ -210,7 +210,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        $fallbacks[] = array( 'code' => $code );
                }
                $data['fallback'] = $fallbacks;
-               $this->getResult()->setIndexedTagName( $data['fallback'], 'lang' );
+               ApiResult::setIndexedTagName( $data['fallback'], 'lang' );
 
                if ( $wgContLang->hasVariants() ) {
                        $variants = array();
@@ -221,7 +221,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                );
                        }
                        $data['variants'] = $variants;
-                       $this->getResult()->setIndexedTagName( $data['variants'], 'lang' );
+                       ApiResult::setIndexedTagName( $data['variants'], 'lang' );
                }
 
                if ( $wgContLang->isRTL() ) {
@@ -263,9 +263,9 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                $data['maxuploadsize'] = UploadBase::getMaxUploadSize();
 
                $data['thumblimits'] = $config->get( 'ThumbLimits' );
-               $this->getResult()->setIndexedTagName( $data['thumblimits'], 'limit' );
+               ApiResult::setIndexedTagName( $data['thumblimits'], 'limit' );
                $data['imagelimits'] = array();
-               $this->getResult()->setIndexedTagName( $data['imagelimits'], 'limit' );
+               ApiResult::setIndexedTagName( $data['imagelimits'], 'limit' );
                foreach ( $config->get( 'ImageLimits' ) as $k => $limit ) {
                        $data['imagelimits'][$k] = array( 'width' => $limit[0], 'height' => $limit[1] );
                }
@@ -290,7 +290,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                'id' => intval( $ns ),
                                'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
                        );
-                       ApiResult::setContent( $data[$ns], $title );
+                       ApiResult::setContentValue( $data[$ns], 'name', $title );
                        $canonical = MWNamespace::getCanonicalName( $ns );
 
                        if ( MWNamespace::hasSubpages( $ns ) ) {
@@ -315,7 +315,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        }
                }
 
-               $this->getResult()->setIndexedTagName( $data, 'ns' );
+               ApiResult::setIndexedTagName( $data, 'ns' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -334,13 +334,13 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        $item = array(
                                'id' => intval( $ns )
                        );
-                       ApiResult::setContent( $item, strtr( $title, '_', ' ' ) );
+                       ApiResult::setContentValue( $item, 'alias', strtr( $title, '_', ' ' ) );
                        $data[] = $item;
                }
 
                sort( $data );
 
-               $this->getResult()->setIndexedTagName( $data, 'ns' );
+               ApiResult::setIndexedTagName( $data, 'ns' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -352,11 +352,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                foreach ( SpecialPageFactory::getNames() as $specialpage ) {
                        if ( isset( $aliases[$specialpage] ) ) {
                                $arr = array( 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] );
-                               $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
+                               ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
                                $data[] = $arr;
                        }
                }
-               $this->getResult()->setIndexedTagName( $data, 'specialpage' );
+               ApiResult::setIndexedTagName( $data, 'specialpage' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -370,10 +370,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        if ( $caseSensitive ) {
                                $arr['case-sensitive'] = '';
                        }
-                       $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
+                       ApiResult::setIndexedTagName( $arr['aliases'], 'alias' );
                        $data[] = $arr;
                }
-               $this->getResult()->setIndexedTagName( $data, 'magicword' );
+               ApiResult::setIndexedTagName( $data, 'magicword' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -442,7 +442,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        $data[] = $val;
                }
 
-               $this->getResult()->setIndexedTagName( $data, 'iw' );
+               ApiResult::setIndexedTagName( $data, 'iw' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -477,7 +477,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                }
 
                $result = $this->getResult();
-               $result->setIndexedTagName( $data, 'db' );
+               ApiResult::setIndexedTagName( $data, 'db' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -533,16 +533,16 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                        $groups = array_intersect( $rights[$group], $allGroups );
                                        if ( $groups ) {
                                                $arr[$type] = $groups;
-                                               $result->setIndexedTagName( $arr[$type], 'group' );
+                                               ApiResult::setIndexedTagName( $arr[$type], 'group' );
                                        }
                                }
                        }
 
-                       $result->setIndexedTagName( $arr['rights'], 'permission' );
+                       ApiResult::setIndexedTagName( $arr['rights'], 'permission' );
                        $data[] = $arr;
                }
 
-               $result->setIndexedTagName( $data, 'group' );
+               ApiResult::setIndexedTagName( $data, 'group' );
 
                return $result->addValue( 'query', $property, $data );
        }
@@ -552,7 +552,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                foreach ( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) as $ext ) {
                        $data[] = array( 'ext' => $ext );
                }
-               $this->getResult()->setIndexedTagName( $data, 'fe' );
+               ApiResult::setIndexedTagName( $data, 'fe' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -581,7 +581,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                'version' => $info['version'],
                        );
                }
-               $this->getResult()->setIndexedTagName( $data, 'library' );
+               ApiResult::setIndexedTagName( $data, 'library' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
 
@@ -607,7 +607,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                        if ( is_array( $ext['descriptionmsg'] ) ) {
                                                $ret['descriptionmsg'] = $ext['descriptionmsg'][0];
                                                $ret['descriptionmsgparams'] = array_slice( $ext['descriptionmsg'], 1 );
-                                               $this->getResult()->setIndexedTagName( $ret['descriptionmsgparams'], 'param' );
+                                               ApiResult::setIndexedTagName( $ret['descriptionmsgparams'], 'param' );
                                        } else {
                                                $ret['descriptionmsg'] = $ext['descriptionmsg'];
                                        }
@@ -667,7 +667,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        }
                }
 
-               $this->getResult()->setIndexedTagName( $data, 'ext' );
+               ApiResult::setIndexedTagName( $data, 'ext' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -704,10 +704,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        'semiprotectedlevels' => $config->get( 'SemiprotectedRestrictionLevels' ),
                );
 
-               $this->getResult()->setIndexedTagName( $data['types'], 'type' );
-               $this->getResult()->setIndexedTagName( $data['levels'], 'level' );
-               $this->getResult()->setIndexedTagName( $data['cascadinglevels'], 'level' );
-               $this->getResult()->setIndexedTagName( $data['semiprotectedlevels'], 'level' );
+               ApiResult::setIndexedTagName( $data['types'], 'type' );
+               ApiResult::setIndexedTagName( $data['levels'], 'level' );
+               ApiResult::setIndexedTagName( $data['cascadinglevels'], 'level' );
+               ApiResult::setIndexedTagName( $data['semiprotectedlevels'], 'level' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -721,10 +721,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
 
                foreach ( $langNames as $code => $name ) {
                        $lang = array( 'code' => $code );
-                       ApiResult::setContent( $lang, $name );
+                       ApiResult::setContentValue( $lang, 'name', $name );
                        $data[] = $lang;
                }
-               $this->getResult()->setIndexedTagName( $data, 'lang' );
+               ApiResult::setIndexedTagName( $data, 'lang' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -745,7 +745,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                $displayName = $msg->text();
                        }
                        $skin = array( 'code' => $name );
-                       ApiResult::setContent( $skin, $displayName );
+                       ApiResult::setContentValue( $skin, 'name', $displayName );
                        if ( !isset( $allowed[$name] ) ) {
                                $skin['unusable'] = '';
                        }
@@ -754,7 +754,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        }
                        $data[] = $skin;
                }
-               $this->getResult()->setIndexedTagName( $data, 'skin' );
+               ApiResult::setIndexedTagName( $data, 'skin' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
@@ -763,7 +763,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                global $wgParser;
                $wgParser->firstCallInit();
                $tags = array_map( array( $this, 'formatParserTags' ), $wgParser->getTags() );
-               $this->getResult()->setIndexedTagName( $tags, 't' );
+               ApiResult::setIndexedTagName( $tags, 't' );
 
                return $this->getResult()->addValue( 'query', $property, $tags );
        }
@@ -772,14 +772,14 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                global $wgParser;
                $wgParser->firstCallInit();
                $hooks = $wgParser->getFunctionHooks();
-               $this->getResult()->setIndexedTagName( $hooks, 'h' );
+               ApiResult::setIndexedTagName( $hooks, 'h' );
 
                return $this->getResult()->addValue( 'query', $property, $hooks );
        }
 
        public function appendVariables( $property ) {
                $variables = MagicWord::getVariableIDs();
-               $this->getResult()->setIndexedTagName( $variables, 'v' );
+               ApiResult::setIndexedTagName( $variables, 'v' );
 
                return $this->getResult()->addValue( 'query', $property, $variables );
        }
@@ -787,7 +787,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
        public function appendProtocols( $property ) {
                // Make a copy of the global so we don't try to set the _element key of it - bug 45130
                $protocols = array_values( $this->getConfig()->get( 'UrlProtocols' ) );
-               $this->getResult()->setIndexedTagName( $protocols, 'p' );
+               ApiResult::setIndexedTagName( $protocols, 'p' );
 
                return $this->getResult()->addValue( 'query', $property, $protocols );
        }
@@ -812,11 +812,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                'subscribers' => array_map( array( 'SpecialVersion', 'arrayToString' ), $subscribers ),
                        );
 
-                       $this->getResult()->setIndexedTagName( $arr['subscribers'], 's' );
+                       ApiResult::setIndexedTagName( $arr['subscribers'], 's' );
                        $data[] = $arr;
                }
 
-               $this->getResult()->setIndexedTagName( $data, 'hook' );
+               ApiResult::setIndexedTagName( $data, 'hook' );
 
                return $this->getResult()->addValue( 'query', $property, $data );
        }
index 342e367..1126842 100644 (file)
@@ -59,7 +59,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
                                $finalThumbParam = $this->mergeThumbParams( $file, $scale, $params['urlparam'] );
                                $imageInfo = ApiQueryImageInfo::getInfo( $file, $prop, $result, $finalThumbParam );
                                $result->addValue( array( 'query', $this->getModuleName() ), null, $imageInfo );
-                               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $modulePrefix );
+                               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), $modulePrefix );
                        }
                // @todo Update exception handling here to understand current getFile exceptions
                } catch ( UploadStashFileNotFoundException $e ) {
index 0e3307b..aa91216 100644 (file)
@@ -135,7 +135,7 @@ class ApiQueryTags extends ApiQueryBase {
                        }
                }
 
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'tag' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'tag' );
        }
 
        public function getCacheMode( $params ) {
index 41f7ee7..f6c6356 100644 (file)
@@ -120,7 +120,7 @@ class ApiQueryContributions extends ApiQueryBase {
                        }
                }
 
-               $this->getResult()->setIndexedTagName_internal(
+               $this->getResult()->addIndexedTagName(
                        array( 'query', $this->getModuleName() ),
                        'item'
                );
@@ -421,7 +421,7 @@ class ApiQueryContributions extends ApiQueryBase {
                if ( $this->fld_tags ) {
                        if ( $row->ts_tags ) {
                                $tags = explode( ',', $row->ts_tags );
-                               $this->getResult()->setIndexedTagName( $tags, 'tag' );
+                               ApiResult::setIndexedTagName( $tags, 'tag' );
                                $vals['tags'] = $tags;
                        } else {
                                $vals['tags'] = array();
index 820a360..fd095fc 100644 (file)
@@ -84,26 +84,26 @@ class ApiQueryUserInfo extends ApiQueryBase {
 
                if ( isset( $this->prop['groups'] ) ) {
                        $vals['groups'] = $user->getEffectiveGroups();
-                       $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty
+                       ApiResult::setIndexedTagName( $vals['groups'], 'g' ); // even if empty
                }
 
                if ( isset( $this->prop['implicitgroups'] ) ) {
                        $vals['implicitgroups'] = $user->getAutomaticGroups();
-                       $result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
+                       ApiResult::setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty
                }
 
                if ( isset( $this->prop['rights'] ) ) {
                        // User::getRights() may return duplicate values, strip them
                        $vals['rights'] = array_values( array_unique( $user->getRights() ) );
-                       $result->setIndexedTagName( $vals['rights'], 'r' ); // even if empty
+                       ApiResult::setIndexedTagName( $vals['rights'], 'r' ); // even if empty
                }
 
                if ( isset( $this->prop['changeablegroups'] ) ) {
                        $vals['changeablegroups'] = $user->changeableGroups();
-                       $result->setIndexedTagName( $vals['changeablegroups']['add'], 'g' );
-                       $result->setIndexedTagName( $vals['changeablegroups']['remove'], 'g' );
-                       $result->setIndexedTagName( $vals['changeablegroups']['add-self'], 'g' );
-                       $result->setIndexedTagName( $vals['changeablegroups']['remove-self'], 'g' );
+                       ApiResult::setIndexedTagName( $vals['changeablegroups']['add'], 'g' );
+                       ApiResult::setIndexedTagName( $vals['changeablegroups']['remove'], 'g' );
+                       ApiResult::setIndexedTagName( $vals['changeablegroups']['add-self'], 'g' );
+                       ApiResult::setIndexedTagName( $vals['changeablegroups']['remove-self'], 'g' );
                }
 
                if ( isset( $this->prop['options'] ) ) {
@@ -159,10 +159,10 @@ class ApiQueryUserInfo extends ApiQueryBase {
                        $acceptLang = array();
                        foreach ( $langs as $lang => $val ) {
                                $r = array( 'q' => $val );
-                               ApiResult::setContent( $r, $lang );
+                               ApiResult::setContentValue( $r, 'code', $lang );
                                $acceptLang[] = $r;
                        }
-                       $result->setIndexedTagName( $acceptLang, 'lang' );
+                       ApiResult::setIndexedTagName( $acceptLang, 'lang' );
                        $vals['acceptlang'] = $acceptLang;
                }
 
index 52636cc..e2c47f5 100644 (file)
@@ -256,13 +256,13 @@ class ApiQueryUsers extends ApiQueryBase {
                                }
                        } else {
                                if ( isset( $this->prop['groups'] ) && isset( $data[$u]['groups'] ) ) {
-                                       $result->setIndexedTagName( $data[$u]['groups'], 'g' );
+                                       ApiResult::setIndexedTagName( $data[$u]['groups'], 'g' );
                                }
                                if ( isset( $this->prop['implicitgroups'] ) && isset( $data[$u]['implicitgroups'] ) ) {
-                                       $result->setIndexedTagName( $data[$u]['implicitgroups'], 'g' );
+                                       ApiResult::setIndexedTagName( $data[$u]['implicitgroups'], 'g' );
                                }
                                if ( isset( $this->prop['rights'] ) && isset( $data[$u]['rights'] ) ) {
-                                       $result->setIndexedTagName( $data[$u]['rights'], 'r' );
+                                       ApiResult::setIndexedTagName( $data[$u]['rights'], 'r' );
                                }
                        }
 
@@ -275,7 +275,7 @@ class ApiQueryUsers extends ApiQueryBase {
                        }
                        $done[] = $u;
                }
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'user' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'user' );
        }
 
        public function getCacheMode( $params ) {
index 3857a08..04eea54 100644 (file)
@@ -281,7 +281,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                }
 
                if ( is_null( $resultPageSet ) ) {
-                       $this->getResult()->setIndexedTagName_internal(
+                       $this->getResult()->addIndexedTagName(
                                array( 'query', $this->getModuleName() ),
                                'item'
                        );
index ae3596d..493c192 100644 (file)
@@ -123,7 +123,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
                        }
                }
                if ( is_null( $resultPageSet ) ) {
-                       $this->getResult()->setIndexedTagName_internal( $this->getModuleName(), 'wr' );
+                       $this->getResult()->addIndexedTagName( $this->getModuleName(), 'wr' );
                } else {
                        $resultPageSet->populateFromTitles( $titles );
                }
index 306c478..da28010 100644 (file)
@@ -1,11 +1,5 @@
 <?php
 /**
- *
- *
- * Created on Sep 4, 2006
- *
- * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
- *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
  * Each subarray may either be a dictionary - key-value pairs with unique keys,
  * or lists, where the items are added using $data[] = $value notation.
  *
- * There are three special key values that change how XML output is generated:
- *   '_element'     This key sets the tag name for the rest of the elements in the current array.
- *                  It is only inserted if the formatter returned true for getNeedsRawData()
- *   '_subelements' This key causes the specified elements to be returned as subelements rather than attributes.
- *                  It is only inserted if the formatter returned true for getNeedsRawData()
- *   '*'            This key has special meaning only to the XML formatter, and is outputted as is
- *                  for all others. In XML it becomes the content of the current element.
- *
+ * @since 1.25 this is no longer a subclass of ApiBase
  * @ingroup API
  */
-class ApiResult extends ApiBase {
+class ApiResult implements ApiSerializable {
 
        /**
-        * override existing value in addValue() and setElement()
+        * Override existing value in addValue(), setValue(), and similar functions
         * @since 1.21
         */
        const OVERRIDE = 1;
 
        /**
-        * For addValue() and setElement(), if the value does not exist, add it as the first element.
-        * In case the new value has no name (numerical index), all indexes will be renumbered.
+        * For addValue(), setValue() and similar functions, if the value does not
+        * exist, add it as the first element. In case the new value has no name
+        * (numerical index), all indexes will be renumbered.
         * @since 1.21
         */
        const ADD_ON_TOP = 2;
 
        /**
-        * For addValue() and setElement(), do not check size while adding a value
+        * For addValue() and similar functions, do not check size while adding a value
         * Don't use this unless you REALLY know what you're doing.
-        * Values added while the size checking was disabled will never be counted
+        * Values added while the size checking was disabled will never be counted.
+        * Ignored for setValue() and similar functions.
         * @since 1.24
         */
        const NO_SIZE_CHECK = 4;
 
-       private $mData, $mIsRawMode, $mSize, $mCheckingSize;
+       /**
+        * For addValue(), setValue() and similar functions, do not validate data.
+        * Also disables size checking. If you think you need to use this, you're
+        * probably wrong.
+        * @since 1.25
+        */
+       const NO_VALIDATE = 12;
 
-       private $continueAllModules = array();
-       private $continueGeneratedModules = array();
-       private $continuationData = array();
-       private $generatorContinuationData = array();
-       private $generatorParams = array();
-       private $generatorDone = false;
+       /**
+        * Key for the 'indexed tag name' metadata item. Value is string.
+        * @since 1.25
+        */
+       const META_INDEXED_TAG_NAME = '_element';
 
        /**
-        * @param ApiMain $main
+        * Key for the 'subelements' metadata item. Value is string[].
+        * @since 1.25
         */
-       public function __construct( ApiMain $main ) {
-               parent::__construct( $main, 'result' );
-               $this->mIsRawMode = false;
-               $this->mCheckingSize = true;
-               $this->reset();
-       }
+       const META_SUBELEMENTS = '_subelements';
 
        /**
-        * Clear the current result data.
+        * Key for the 'preserve keys' metadata item. Value is string[].
+        * @since 1.25
         */
-       public function reset() {
-               $this->mData = array();
-               $this->mSize = 0;
-       }
+       const META_PRESERVE_KEYS = '_preservekeys';
 
        /**
-        * Call this function when special elements such as '_element'
-        * are needed by the formatter, for example in XML printing.
-        * @since 1.23 $flag parameter added
-        * @param bool $flag Set the raw mode flag to this state
+        * Key for the 'content' metadata item. Value is string.
+        * @since 1.25
         */
-       public function setRawMode( $flag = true ) {
-               $this->mIsRawMode = $flag;
-       }
+       const META_CONTENT = '_content';
 
        /**
-        * Returns true whether the formatter requested raw data.
-        * @return bool
+        * Key for the 'type' metadata item. Value is one of the following strings:
+        *  - default: Like 'array' if all (non-metadata) keys are numeric with no
+        *    gaps, otherwise like 'assoc'.
+        *  - array: Keys are used for ordering, but are not output. In a format
+        *    like JSON, outputs as [].
+        *  - assoc: In a format like JSON, outputs as {}.
+        *  - kvp: For a format like XML where object keys have a restricted
+        *    character set, use an alternative output format. For example,
+        *    <container><item name="key">value</item></container> rather than
+        *    <container key="value" />
+        *  - BCarray: Like 'array' normally, 'default' in backwards-compatibility mode.
+        *  - BCassoc: Like 'assoc' normally, 'default' in backwards-compatibility mode.
+        *  - BCkvp: Like 'kvp' normally. In backwards-compatibility mode, forces
+        *    the alternative output format for all formats, for example
+        *    [{"name":key,"*":value}] in JSON. META_KVP_KEY_NAME must also be set.
+        * @since 1.25
         */
-       public function getIsRawMode() {
-               return $this->mIsRawMode;
-       }
+       const META_TYPE = '_type';
 
        /**
-        * Get the result's internal data array (read-only)
-        * @return array
+        * Key (rather than "name" or other default) for when META_TYPE is 'kvp' or
+        * 'BCkvp'. Value is string.
+        * @since 1.25
         */
-       public function getData() {
-               return $this->mData;
-       }
+       const META_KVP_KEY_NAME = '_kvpkeyname';
 
        /**
-        * Get the 'real' size of a result item. This means the strlen() of the item,
-        * or the sum of the strlen()s of the elements if the item is an array.
-        * @param mixed $value
-        * @return int
+        * Key for the 'BC bools' metadata item. Value is string[].
+        * Note no setter is provided.
+        * @since 1.25
         */
-       public static function size( $value ) {
-               $s = 0;
-               if ( is_array( $value ) ) {
-                       foreach ( $value as $v ) {
-                               $s += self::size( $v );
-                       }
-               } elseif ( !is_object( $value ) ) {
-                       // Objects can't always be cast to string
-                       $s = strlen( $value );
+       const META_BC_BOOLS = '_BC_bools';
+
+       /**
+        * Key for the 'BC subelements' metadata item. Value is string[].
+        * Note no setter is provided.
+        * @since 1.25
+        */
+       const META_BC_SUBELEMENTS = '_BC_subelements';
+
+       private $data, $size, $maxSize;
+       private $errorFormatter;
+
+       // Deprecated fields
+       private $isRawMode, $checkingSize, $mainForContinuation;
+
+       /**
+        * @param int|bool $maxSize Maximum result "size", or false for no limit
+        * @since 1.25 Takes an integer|bool rather than an ApiMain
+        */
+       public function __construct( $maxSize ) {
+               if ( $maxSize instanceof ApiMain ) {
+                       /// @todo: After fixing Wikidata unit tests, warn
+                       //wfDeprecated( 'Passing ApiMain to ' . __METHOD__ . ' is deprecated', '1.25' );
+                       $this->errorFormatter = $maxSize->getErrorFormatter();
+                       $this->mainForContinuation = $maxSize;
+                       $maxSize = $maxSize->getConfig()->get( 'APIMaxResultSize' );
                }
 
-               return $s;
+               $this->maxSize = $maxSize;
+               $this->isRawMode = false;
+               $this->checkingSize = true;
+               $this->reset();
        }
 
        /**
-        * Get the size of the result, i.e. the amount of bytes in it
-        * @return int
+        * Set the error formatter
+        * @since 1.25
+        * @param ApiErrorFormatter $formatter
         */
-       public function getSize() {
-               return $this->mSize;
+       public function setErrorFormatter( ApiErrorFormatter $formatter ) {
+               $this->errorFormatter = $formatter;
        }
 
        /**
-        * Disable size checking in addValue(). Don't use this unless you
-        * REALLY know what you're doing. Values added while size checking
-        * was disabled will not be counted (ever)
-        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
+        * Allow for adding one ApiResult into another
+        * @since 1.25
+        * @return mixed
         */
-       public function disableSizeCheck() {
-               $this->mCheckingSize = false;
+       public function serializeForApiResult() {
+               return $this->data;
        }
 
+       /************************************************************************//**
+        * @name   Content
+        * @{
+        */
+
        /**
-        * Re-enable size checking in addValue()
-        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
+        * Clear the current result data.
         */
-       public function enableSizeCheck() {
-               $this->mCheckingSize = true;
+       public function reset() {
+               $this->data = array();
+               $this->size = 0;
+       }
+
+       /**
+        * Get the result data array
+        *
+        * The returned value should be considered read-only.
+        *
+        * Transformations include:
+        *
+        * Custom: (callable) Applied before other transformations. Signature is
+        *  function ( &$data, &$metadata ), return value is ignored. Called for
+        *  each nested array.
+        *
+        * BC: (array) This transformation does various adjustments to bring the
+        *  output in line with the pre-1.25 result format. The value array is a
+        *  list of flags: 'nobools', 'no*', 'nosub'.
+        *  - Boolean-valued items are changed to '' if true or removed if false,
+        *    unless listed in META_BC_BOOLS. This may be skipped by including
+        *    'nobools' in the value array.
+        *  - The tag named by META_CONTENT is renamed to '*', and META_CONTENT is
+        *    set to '*'. This may be skipped by including 'no*' in the value
+        *    array.
+        *  - Tags listed in META_BC_SUBELEMENTS will have their values changed to
+        *    array( '*' => $value ). This may be skipped by including 'nosub' in
+        *    the value array.
+        *  - If META_TYPE is 'BCarray', set it to 'default'
+        *  - If META_TYPE is 'BCassoc', set it to 'default'
+        *  - If META_TYPE is 'BCkvp', perform the transformation (even if
+        *    the Types transformation is not being applied).
+        *
+        * Types: (assoc) Apply transformations based on META_TYPE. The values
+        * array is an associative array with the following possible keys:
+        *  - AssocAsObject: (bool) If true, return arrays with META_TYPE 'assoc'
+        *    as objects.
+        *  - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp'
+        *    and 'BCkvp' into arrays of two-element arrays, something like this:
+        *      $output = array();
+        *      foreach ( $input as $key => $value ) {
+        *          $pair = array();
+        *          $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key;
+        *          ApiResult::setContentValue( $pair, 'value', $value );
+        *          $output[] = $pair;
+        *      }
+        *
+        * Strip: (string) Strips metadata keys from the result.
+        *  - 'all': Strip all metadata, recursively
+        *  - 'base': Strip metadata at the top-level only.
+        *  - 'none': Do not strip metadata.
+        *  - 'bc': Like 'all', but leave certain pre-1.25 keys.
+        *
+        * @since 1.25
+        * @param array|string|null $path Path to fetch, see ApiResult::addValue
+        * @param array $transforms See above
+        * @return mixed Result data, or null if not found
+        */
+       public function getResultData( $path = array(), $transforms = array() ) {
+               $path = (array)$path;
+               if ( !$path ) {
+                       return self::applyTransformations( $this->data, $transforms );
+               }
+
+               $last = array_pop( $path );
+               $ret = &$this->path( $path, 'dummy' );
+               if ( !isset( $ret[$last] ) ) {
+                       return null;
+               } elseif ( is_array( $ret[$last] ) ) {
+                       return self::applyTransformations( $ret[$last], $transforms );
+               } else {
+                       return $ret[$last];
+               }
+       }
+
+       /**
+        * Get the size of the result, i.e. the amount of bytes in it
+        * @return int
+        */
+       public function getSize() {
+               return $this->size;
        }
 
        /**
         * Add an output value to the array by name.
+        *
         * Verifies that value with the same name has not been added before.
-        * @param array $arr To add $value to
-        * @param string $name Index of $arr to add $value at
+        *
+        * @since 1.25
+        * @param array &$arr To add $value to
+        * @param string|int|null $name Index of $arr to add $value at,
+        *   or null to use the next numeric index.
         * @param mixed $value
         * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
-        *    This parameter used to be boolean, and the value of OVERRIDE=1 was
-        *    specifically chosen so that it would be backwards compatible with the
-        *    new method signature.
-        *
-        * @since 1.21 int $flags replaced boolean $override
         */
-       public static function setElement( &$arr, $name, $value, $flags = 0 ) {
-               if ( $arr === null || $name === null || $value === null
-                       || !is_array( $arr ) || is_array( $name )
-               ) {
-                       ApiBase::dieDebug( __METHOD__, 'Bad parameter' );
+       public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
+               if ( $name === null ) {
+                       if ( $flags & ApiResult::ADD_ON_TOP ) {
+                               array_unshift( $arr, $value );
+                       } else {
+                               array_push( $arr, $value );
+                       }
+                       return;
+               }
+
+               if ( !( $flags & ApiResult::NO_VALIDATE ) ) {
+                       $value = self::validateValue( $value );
                }
 
                $exists = isset( $arr[$name] );
@@ -193,451 +296,1197 @@ class ApiResult extends ApiBase {
                                $arr[$name] = $value;
                        }
                } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
-                       $merged = array_intersect_key( $arr[$name], $value );
-                       if ( !count( $merged ) ) {
+                       $conflicts = array_intersect_key( $arr[$name], $value );
+                       if ( !$conflicts ) {
                                $arr[$name] += $value;
                        } else {
-                               ApiBase::dieDebug( __METHOD__, "Attempting to merge element $name" );
+                               $keys = join( ', ', array_keys( $conflicts ) );
+                               throw new RuntimeException( "Conflicting keys ($keys) when attempting to merge element $name" );
                        }
                } else {
-                       ApiBase::dieDebug(
-                               __METHOD__,
-                               "Attempting to add element $name=$value, existing value is {$arr[$name]}"
-                       );
+                       throw new RuntimeException( "Attempting to add element $name=$value, existing value is {$arr[$name]}" );
                }
        }
 
        /**
-        * Adds a content element to an array.
-        * Use this function instead of hardcoding the '*' element.
-        * @param array $arr To add the content element to
+        * Validate a value for addition to the result
         * @param mixed $value
-        * @param string $subElemName When present, content element is created
-        *  as a sub item of $arr. Use this parameter to create elements in
-        *  format "<elem>text</elem>" without attributes.
         */
-       public static function setContent( &$arr, $value, $subElemName = null ) {
-               if ( is_array( $value ) ) {
-                       ApiBase::dieDebug( __METHOD__, 'Bad parameter' );
+       private static function validateValue( $value ) {
+               global $wgContLang;
+
+               if ( is_object( $value ) ) {
+                       // Note we use is_callable() here instead of instanceof because
+                       // ApiSerializable is an informal protocol (see docs there for details).
+                       if ( is_callable( array( $value, 'serializeForApiResult' ) ) ) {
+                               $oldValue = $value;
+                               $value = $value->serializeForApiResult();
+                               if ( is_object( $value ) ) {
+                                       throw new UnexpectedValueException(
+                                               get_class( $oldValue ) . "::serializeForApiResult() returned an object of class " .
+                                                       get_class( $value )
+                                       );
+                               }
+
+                               // Recursive call instead of fall-through so we can throw a
+                               // better exception message.
+                               try {
+                                       return self::validateValue( $value );
+                               } catch ( Exception $ex ) {
+                                       throw new UnexpectedValueException(
+                                               get_class( $oldValue ) . "::serializeForApiResult() returned an invalid value: " .
+                                                       $ex->getMessage(),
+                                               0,
+                                               $ex
+                                       );
+                               }
+                       } elseif ( is_callable( array( $value, '__toString' ) ) ) {
+                               $value = (string)$value;
+                       } else {
+                               $value = (array)$value + array( self::META_TYPE => 'assoc' );
+                       }
                }
-               if ( is_null( $subElemName ) ) {
-                       ApiResult::setElement( $arr, '*', $value );
-               } else {
-                       if ( !isset( $arr[$subElemName] ) ) {
-                               $arr[$subElemName] = array();
+               if ( is_array( $value ) ) {
+                       foreach ( $value as $k => $v ) {
+                               $value[$k] = self::validateValue( $v );
                        }
-                       ApiResult::setElement( $arr[$subElemName], '*', $value );
+               } elseif ( is_float( $value ) && !is_finite( $value ) ) {
+                       throw new InvalidArgumentException( "Cannot add non-finite floats to ApiResult" );
+               } elseif ( is_string( $value ) ) {
+                       $value = $wgContLang->normalize( $value );
+               } elseif ( $value !== null && !is_scalar( $value ) ) {
+                       $type = gettype( $value );
+                       if ( is_resource( $value ) ) {
+                               $type .= '(' . get_resource_type( $value ) . ')';
+                       }
+                       throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
                }
+
+               return $value;
        }
 
        /**
-        * Causes the elements with the specified names to be output as
-        * subelements rather than attributes.
-        * @param array $arr
-        * @param array|string $names The element name(s) to be output as subelements
+        * Add value to the output data at the given path.
+        *
+        * Path can be an indexed array, each element specifying the branch at which to add the new
+        * value. Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value.
+        * If $path is null, the value will be inserted at the data root.
+        *
+        * @param array|string|int|null $path
+        * @param string|int|null $name See ApiResult::setValue()
+        * @param mixed $value
+        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
+        *   This parameter used to be boolean, and the value of OVERRIDE=1 was specifically
+        *   chosen so that it would be backwards compatible with the new method signature.
+        * @return bool True if $value fits in the result, false if not
+        * @since 1.21 int $flags replaced boolean $override
         */
-       public function setSubelements( &$arr, $names ) {
-               // In raw mode, add the '_subelements', otherwise just ignore
-               if ( !$this->getIsRawMode() ) {
-                       return;
-               }
-               if ( $arr === null || $names === null || !is_array( $arr ) ) {
-                       ApiBase::dieDebug( __METHOD__, 'Bad parameter' );
-               }
-               if ( !is_array( $names ) ) {
-                       $names = array( $names );
-               }
-               if ( !isset( $arr['_subelements'] ) ) {
-                       $arr['_subelements'] = $names;
-               } else {
-                       $arr['_subelements'] = array_merge( $arr['_subelements'], $names );
+       public function addValue( $path, $name, $value, $flags = 0 ) {
+               $arr = &$this->path( $path, ( $flags & ApiResult::ADD_ON_TOP ) ? 'prepend' : 'append' );
+
+               if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
+                       $newsize = $this->size + self::valueSize( $value );
+                       if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
+                               /// @todo Add i18n message when replacing calls to ->setWarning()
+                               $msg = new ApiRawMessage( 'This result was truncated because it would otherwise ' .
+                                       ' be larger than the limit of $1 bytes', 'truncatedresult' );
+                               $msg->numParams( $this->maxSize );
+                               $this->errorFormatter->addWarning( 'result', $msg );
+                               return false;
+                       }
+                       $this->size = $newsize;
                }
+
+               self::setValue( $arr, $name, $value, $flags );
+               return true;
        }
 
        /**
-        * In case the array contains indexed values (in addition to named),
-        * give all indexed values the given tag name. This function MUST be
-        * called on every array that has numerical indexes.
-        * @param array $arr
-        * @param string $tag Tag name
+        * Remove an output value to the array by name.
+        * @param array &$arr To remove $value from
+        * @param string|int $name Index of $arr to remove
+        * @return mixed Old value, or null
         */
-       public function setIndexedTagName( &$arr, $tag ) {
-               // In raw mode, add the '_element', otherwise just ignore
-               if ( !$this->getIsRawMode() ) {
-                       return;
-               }
-               if ( $arr === null || $tag === null || !is_array( $arr ) || is_array( $tag ) ) {
-                       ApiBase::dieDebug( __METHOD__, 'Bad parameter' );
+       public static function unsetValue( array &$arr, $name ) {
+               $ret = null;
+               if ( isset( $arr[$name] ) ) {
+                       $ret = $arr[$name];
+                       unset( $arr[$name] );
                }
-               // Do not use setElement() as it is ok to call this more than once
-               $arr['_element'] = $tag;
+               return $ret;
        }
 
        /**
-        * Calls setIndexedTagName() on each sub-array of $arr
-        * @param array $arr
-        * @param string $tag Tag name
+        * Remove value from the output data at the given path.
+        *
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param string|int|null $name Index to remove at $path.
+        *   If null, $path itself is removed.
+        * @param int $flags Flags used when adding the value
+        * @return mixed Old value, or null
         */
-       public function setIndexedTagName_recursive( &$arr, $tag ) {
-               if ( !is_array( $arr ) ) {
-                       return;
-               }
-               foreach ( $arr as &$a ) {
-                       if ( !is_array( $a ) ) {
-                               continue;
+       public function removeValue( $path, $name, $flags = 0 ) {
+               $path = (array)$path;
+               if ( $name === null ) {
+                       if ( !$path ) {
+                               throw new InvalidArgumentException( 'Cannot remove the data root' );
                        }
-                       $this->setIndexedTagName( $a, $tag );
-                       $this->setIndexedTagName_recursive( $a, $tag );
+                       $name = array_pop( $path );
+               }
+               $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
+               if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
+                       $newsize = $this->size - self::valueSize( $ret );
+                       $this->size = max( $newsize, 0 );
                }
+               return $ret;
        }
 
        /**
-        * Calls setIndexedTagName() on an array already in the result.
-        * Don't specify a path to a value that's not in the result, or
-        * you'll get nasty errors.
-        * @param array $path Path to the array, like addValue()'s $path
-        * @param string $tag
+        * Add an output value to the array by name and mark as META_CONTENT.
+        *
+        * @since 1.25
+        * @param array &$arr To add $value to
+        * @param string|int $name Index of $arr to add $value at.
+        * @param mixed $value
+        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
         */
-       public function setIndexedTagName_internal( $path, $tag ) {
-               $data = &$this->mData;
-               foreach ( (array)$path as $p ) {
-                       if ( !isset( $data[$p] ) ) {
-                               $data[$p] = array();
-                       }
-                       $data = &$data[$p];
-               }
-               if ( is_null( $data ) ) {
-                       return;
+       public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
+               if ( $name === null ) {
+                       throw new InvalidArgumentException( 'Content value must be named' );
                }
-               $this->setIndexedTagName( $data, $tag );
+               self::setContentField( $arr, $name, $flags );
+               self::setValue( $arr, $name, $value, $flags );
        }
 
        /**
-        * Add value to the output data at the given path.
-        * Path can be an indexed array, each element specifying the branch at which to add the new
-        * value. Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value.
-        * If $path is null, the value will be inserted at the data root.
-        * If $name is empty, the $value is added as a next list element data[] = $value.
+        * Add value to the output data at the given path and mark as META_CONTENT
         *
-        * @param array|string|null $path
-        * @param string $name
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param string|int $name See ApiResult::setValue()
         * @param mixed $value
         * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
-        *   This parameter used to be boolean, and the value of OVERRIDE=1 was specifically
-        *   chosen so that it would be backwards compatible with the new method signature.
         * @return bool True if $value fits in the result, false if not
-        *
-        * @since 1.21 int $flags replaced boolean $override
         */
-       public function addValue( $path, $name, $value, $flags = 0 ) {
-               $data = &$this->mData;
-               if ( $this->mCheckingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
-                       $newsize = $this->mSize + self::size( $value );
-                       $maxResultSize = $this->getConfig()->get( 'APIMaxResultSize' );
-                       if ( $newsize > $maxResultSize ) {
-                               $this->setWarning(
-                                       "This result was truncated because it would otherwise be larger than the " .
-                                               "limit of {$maxResultSize} bytes" );
-
-                               return false;
-                       }
-                       $this->mSize = $newsize;
-               }
-
-               $addOnTop = $flags & ApiResult::ADD_ON_TOP;
-               if ( $path !== null ) {
-                       foreach ( (array)$path as $p ) {
-                               if ( !isset( $data[$p] ) ) {
-                                       if ( $addOnTop ) {
-                                               $data = array( $p => array() ) + $data;
-                                               $addOnTop = false;
-                                       } else {
-                                               $data[$p] = array();
-                                       }
-                               }
-                               $data = &$data[$p];
-                       }
-               }
-
-               if ( !$name ) {
-                       // Add list element
-                       if ( $addOnTop ) {
-                               // This element needs to be inserted in the beginning
-                               // Numerical indexes will be renumbered
-                               array_unshift( $data, $value );
-                       } else {
-                               // Add new value at the end
-                               $data[] = $value;
-                       }
-               } else {
-                       // Add named element
-                       self::setElement( $data, $name, $value, $flags );
+       public function addContentValue( $path, $name, $value, $flags = 0 ) {
+               if ( $name === null ) {
+                       throw new InvalidArgumentException( 'Content value must be named' );
                }
-
-               return true;
+               $this->addContentField( $path, $name, $flags );
+               $this->addValue( $path, $name, $value, $flags );
        }
 
        /**
-        * Add a parsed limit=max to the result.
+        * Add the numeric limit for a limit=max to the result.
         *
+        * @since 1.25
         * @param string $moduleName
         * @param int $limit
         */
-       public function setParsedLimit( $moduleName, $limit ) {
+       public function addParsedLimit( $moduleName, $limit ) {
                // Add value, allowing overwriting
-               $this->addValue( 'limits', $moduleName, $limit, ApiResult::OVERRIDE );
+               $this->addValue( 'limits', $moduleName, $limit,
+                       ApiResult::OVERRIDE | ApiResult::NO_SIZE_CHECK );
        }
 
+       /**@}*/
+
+       /************************************************************************//**
+        * @name   Metadata
+        * @{
+        */
+
        /**
-        * Unset a value previously added to the result set.
-        * Fails silently if the value isn't found.
-        * For parameters, see addValue()
-        * @param array|null $path
-        * @param string $name
+        * Set the name of the content field name (META_CONTENT)
+        *
+        * @since 1.25
+        * @param array &$arr
+        * @param string|int $name Name of the field
+        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
         */
-       public function unsetValue( $path, $name ) {
-               $data = &$this->mData;
-               if ( $path !== null ) {
-                       foreach ( (array)$path as $p ) {
-                               if ( !isset( $data[$p] ) ) {
-                                       return;
-                               }
-                               $data = &$data[$p];
-                       }
+       public static function setContentField( array &$arr, $name, $flags = 0 ) {
+               if ( isset( $arr[self::META_CONTENT] ) &&
+                       isset( $arr[$arr[self::META_CONTENT]] ) &&
+                       !( $flags & self::OVERRIDE )
+               ) {
+                       throw new RuntimeException(
+                               "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
+                                       " is already set as the content element"
+                       );
                }
-               $this->mSize -= self::size( $data[$name] );
-               unset( $data[$name] );
+               $arr[self::META_CONTENT] = $name;
        }
 
        /**
-        * Ensure all values in this result are valid UTF-8.
+        * Set the name of the content field name (META_CONTENT)
+        *
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param string|int $name Name of the field
+        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
         */
-       public function cleanUpUTF8() {
-               array_walk_recursive( $this->mData, array( 'ApiResult', 'cleanUp_helper' ) );
+       public function addContentField( $path, $name, $flags = 0 ) {
+               $arr = &$this->path( $path, ( $flags & ApiResult::ADD_ON_TOP ) ? 'prepend' : 'append' );
+               self::setContentField( $arr, $name, $flags );
        }
 
        /**
-        * Callback function for cleanUpUTF8()
-        *
-        * @param string $s
+        * Causes the elements with the specified names to be output as
+        * subelements rather than attributes.
+        * @since 1.25 is static
+        * @param array &$arr
+        * @param array|string|int $names The element name(s) to be output as subelements
         */
-       private static function cleanUp_helper( &$s ) {
-               if ( !is_string( $s ) ) {
-                       return;
+       public static function setSubelementsList( array &$arr, $names ) {
+               if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
+                       $arr[self::META_SUBELEMENTS] = (array)$names;
+               } else {
+                       $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
                }
-               global $wgContLang;
-               $s = $wgContLang->normalize( $s );
        }
 
        /**
-        * Converts a Status object to an array suitable for addValue
-        * @param Status $status
-        * @param string $errorType
-        * @return array
+        * Causes the elements with the specified names to be output as
+        * subelements rather than attributes.
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param array|string|int $names The element name(s) to be output as subelements
         */
-       public function convertStatusToArray( $status, $errorType = 'error' ) {
-               if ( $status->isGood() ) {
-                       return array();
-               }
+       public function addSubelementsList( $path, $names ) {
+               $arr = &$this->path( $path );
+               self::setSubelementsList( $arr, $names );
+       }
 
-               $result = array();
-               foreach ( $status->getErrorsByType( $errorType ) as $error ) {
-                       $this->setIndexedTagName( $error['params'], 'param' );
-                       $result[] = $error;
+       /**
+        * Causes the elements with the specified names to be output as
+        * attributes (when possible) rather than as subelements.
+        * @since 1.25
+        * @param array &$arr
+        * @param array|string|int $names The element name(s) to not be output as subelements
+        */
+       public static function unsetSubelementsList( array &$arr, $names ) {
+               if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
+                       $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
                }
-               $this->setIndexedTagName( $result, $errorType );
-
-               return $result;
        }
 
-       public function execute() {
-               ApiBase::dieDebug( __METHOD__, 'execute() is not supported on Result object' );
+       /**
+        * Causes the elements with the specified names to be output as
+        * attributes (when possible) rather than as subelements.
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param array|string|int $names The element name(s) to not be output as subelements
+        */
+       public function removeSubelementsList( $path, $names ) {
+               $arr = &$this->path( $path );
+               self::unsetSubelementsList( $arr, $names );
        }
 
        /**
-        * Parse a 'continue' parameter and return status information.
-        *
-        * This must be balanced by a call to endContinuation().
-        *
-        * @since 1.24
-        * @param string|null $continue The "continue" parameter, if any
-        * @param ApiBase[] $allModules Contains ApiBase instances that will be executed
-        * @param array $generatedModules Names of modules that depend on the generator
-        * @return array Two elements: a boolean indicating if the generator is done,
-        *   and an array of modules to actually execute.
+        * Set the tag name for numeric-keyed values in XML format
+        * @since 1.25 is static
+        * @param array &$arr
+        * @param string $tag Tag name
         */
-       public function beginContinuation(
-               $continue, array $allModules = array(), array $generatedModules = array()
-       ) {
-               $this->continueGeneratedModules = $generatedModules
-                       ? array_combine( $generatedModules, $generatedModules )
-                       : array();
-               $this->continuationData = array();
-               $this->generatorContinuationData = array();
-               $this->generatorParams = array();
-
-               $skip = array();
-               if ( is_string( $continue ) && $continue !== '' ) {
-                       $continue = explode( '||', $continue );
-                       $this->dieContinueUsageIf( count( $continue ) !== 2 );
-                       $this->generatorDone = ( $continue[0] === '-' );
-                       $skip = explode( '|', $continue[1] );
-                       if ( !$this->generatorDone ) {
-                               $this->generatorParams = explode( '|', $continue[0] );
-                       } else {
-                               // When the generator is complete, don't run any modules that
-                               // depend on it.
-                               $skip += $this->continueGeneratedModules;
-                       }
-               }
-
-               $this->continueAllModules = array();
-               $runModules = array();
-               foreach ( $allModules as $module ) {
-                       $name = $module->getModuleName();
-                       if ( in_array( $name, $skip ) ) {
-                               $this->continueAllModules[$name] = false;
-                               // Prevent spurious "unused parameter" warnings
-                               $module->extractRequestParams();
-                       } else {
-                               $this->continueAllModules[$name] = true;
-                               $runModules[] = $module;
-                       }
+       public static function setIndexedTagName( array &$arr, $tag ) {
+               if ( !is_string( $tag ) ) {
+                       throw new InvalidArgumentException( 'Bad tag name' );
                }
+               $arr[self::META_INDEXED_TAG_NAME] = $tag;
+       }
 
-               return array(
-                       $this->generatorDone,
-                       $runModules,
-               );
+       /**
+        * Set the tag name for numeric-keyed values in XML format
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param string $tag Tag name
+        */
+       public function addIndexedTagName( $path, $tag ) {
+               $arr = &$this->path( $path );
+               self::setIndexedTagName( $arr, $tag );
        }
 
        /**
-        * Set the continuation parameter for a module
+        * Set indexed tag name on $arr and all subarrays
         *
-        * @since 1.24
-        * @param ApiBase $module
-        * @param string $paramName
-        * @param string|array $paramValue
-        * @throws MWException
+        * @since 1.25
+        * @param array &$arr
+        * @param string $tag Tag name
         */
-       public function setContinueParam( ApiBase $module, $paramName, $paramValue ) {
-               $name = $module->getModuleName();
-               if ( !isset( $this->continueAllModules[$name] ) ) {
-                       throw new MWException(
-                               "Module '$name' called ApiResult::setContinueParam but was not " .
-                               'passed to ApiResult::beginContinuation'
+       public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
+               if ( !is_string( $tag ) ) {
+                       throw new InvalidArgumentException( 'Bad tag name' );
+               }
+               $arr[self::META_INDEXED_TAG_NAME] = $tag;
+               foreach ( $arr as $k => &$v ) {
+                       if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
+                               self::setIndexedTagNameRecursive( $v, $tag );
+                       }
+               }
+       }
+
+       /**
+        * Set indexed tag name on $path and all subarrays
+        *
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param string $tag Tag name
+        */
+       public function addIndexedTagNameRecursive( $path, $tag ) {
+               $arr = &$this->path( $path );
+               self::setIndexedTagNameRecursive( $arr, $tag );
+       }
+
+       /**
+        * Preserve specified keys.
+        *
+        * This prevents XML name mangling and preventing keys from being removed
+        * by self::stripMetadata().
+        *
+        * @since 1.25
+        * @param array &$arr
+        * @param array|string $names The element name(s) to preserve
+        */
+       public static function setPreserveKeysList( array &$arr, $names ) {
+               if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
+                       $arr[self::META_PRESERVE_KEYS] = (array)$names;
+               } else {
+                       $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
+               }
+       }
+
+       /**
+        * Preserve specified keys.
+        * @since 1.25
+        * @see self::setPreserveKeysList()
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param array|string $names The element name(s) to preserve
+        */
+       public function addPreserveKeysList( $path, $names ) {
+               $arr = &$this->path( $path );
+               self::setPreserveKeysList( $arr, $names );
+       }
+
+       /**
+        * Don't preserve specified keys.
+        * @since 1.25
+        * @see self::setPreserveKeysList()
+        * @param array &$arr
+        * @param array|string $names The element name(s) to not preserve
+        */
+       public static function unsetPreserveKeysList( array &$arr, $names ) {
+               if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
+                       $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
+               }
+       }
+
+       /**
+        * Don't preserve specified keys.
+        * @since 1.25
+        * @see self::setPreserveKeysList()
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param array|string $names The element name(s) to not preserve
+        */
+       public function removePreserveKeysList( $path, $names ) {
+               $arr = &$this->path( $path );
+               self::unsetPreserveKeysList( $arr, $names );
+       }
+
+       /**
+        * Set the array data type
+        *
+        * @since 1.25
+        * @param array &$arr
+        * @param string $type See ApiResult::META_TYPE
+        * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
+        */
+       public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
+               if ( !in_array( $type, array( 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp' ), true ) ) {
+                       throw new InvalidArgumentException( 'Bad type' );
+               }
+               $arr[self::META_TYPE] = $type;
+               if ( is_string( $kvpKeyName ) ) {
+                       $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
+               }
+       }
+
+       /**
+        * Set the array data type for a path
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param string $type See ApiResult::META_TYPE
+        * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
+        */
+       public function addArrayType( $path, $tag, $kvpKeyName = null ) {
+               $arr = &$this->path( $path );
+               self::setArrayType( $arr, $tag, $kvpKeyName );
+       }
+
+       /**
+        * Set the array data type recursively
+        * @since 1.25
+        * @param array &$arr
+        * @param string $type See ApiResult::META_TYPE
+        * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
+        */
+       public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
+               self::setArrayType( $arr, $type, $kvpKeyName );
+               foreach ( $arr as $k => &$v ) {
+                       if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
+                               self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
+                       }
+               }
+       }
+
+       /**
+        * Set the array data type for a path recursively
+        * @since 1.25
+        * @param array|string|null $path See ApiResult::addValue()
+        * @param string $type See ApiResult::META_TYPE
+        * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
+        */
+       public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
+               $arr = &$this->path( $path );
+               self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
+       }
+
+       /**@}*/
+
+       /************************************************************************//**
+        * @name   Utility
+        * @{
+        */
+
+       /**
+        * Test whether a key should be considered metadata
+        *
+        * @param string $key
+        * @return bool
+        */
+       public static function isMetadataKey( $key ) {
+               return substr( $key, 0, 1 ) === '_';
+       }
+
+       /**
+        * Apply transformations to an array, returning the transformed array.
+        *
+        * @see ApiResult::getResultData()
+        * @since 1.25
+        * @param array $data
+        * @param array $transforms
+        * @return array|object
+        */
+       protected static function applyTransformations( array $dataIn, array $transforms ) {
+               $strip = isset( $transforms['Strip'] ) ? $transforms['Strip'] : 'none';
+               if ( $strip === 'base' ) {
+                       $transforms['Strip'] = 'none';
+               }
+               $transformTypes = isset( $transforms['Types'] ) ? $transforms['Types'] : null;
+               if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
+               }
+
+               $metadata = array();
+               $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
+
+               if ( isset( $transforms['Custom'] ) ) {
+                       if ( !is_callable( $transforms['Custom'] ) ) {
+                               throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
+                       }
+                       call_user_func_array( $transforms['Custom'], array( &$data, &$metadata ) );
+               }
+
+               if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
+                       isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
+                       !isset( $metadata[self::META_KVP_KEY_NAME] )
+               ) {
+                       throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
+                               'ApiResult::META_KVP_KEY_NAME metadata item' );
+               }
+
+               // BC transformations
+               $boolKeys = null;
+               $forceKVP = false;
+               if ( isset( $transforms['BC'] ) ) {
+                       if ( !is_array( $transforms['BC'] ) ) {
+                               throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
+                       }
+                       if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
+                               $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
+                                       ? array_flip( $metadata[self::META_BC_BOOLS] )
+                                       : array();
+                       }
+
+                       if ( !in_array( 'no*', $transforms['BC'], true ) &&
+                               isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
+                       ) {
+                               $k = $metadata[self::META_CONTENT];
+                               $data['*'] = $data[$k];
+                               unset( $data[$k] );
+                               $metadata[self::META_CONTENT] = '*';
+                       }
+
+                       if ( !in_array( 'nosub', $transforms['BC'], true ) &&
+                               isset( $metadata[self::META_BC_SUBELEMENTS] )
+                       ) {
+                               foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
+                                       $data[$k] = array(
+                                               '*' => $data[$k],
+                                               self::META_CONTENT => '*',
+                                               self::META_TYPE => 'assoc',
+                                       );
+                               }
+                       }
+
+                       if ( isset( $metadata[self::META_TYPE] ) ) {
+                               switch ( $metadata[self::META_TYPE] ) {
+                                       case 'BCarray':
+                                       case 'BCassoc':
+                                               $metadata[self::META_TYPE] = 'default';
+                                               break;
+                                       case 'BCkvp':
+                                               $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
+                                               break;
+                               }
+                       }
+               }
+
+               // Figure out type, do recursive calls, and do boolean transform if necessary
+               $defaultType = 'array';
+               $maxKey = -1;
+               foreach ( $data as $k => &$v ) {
+                       $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
+                       if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
+                               if ( !$v ) {
+                                       unset( $data[$k] );
+                                       continue;
+                               }
+                               $v = '';
+                       }
+                       if ( is_string( $k ) ) {
+                               $defaultType = 'assoc';
+                       } elseif ( $k > $maxKey ) {
+                               $maxKey = $k;
+                       }
+               }
+               unset( $v );
+
+               // Determine which metadata to keep
+               switch ( $strip ) {
+                       case 'all':
+                       case 'base':
+                               $keepMetadata = array();
+                               break;
+                       case 'none':
+                               $keepMetadata = &$metadata;
+                               break;
+                       case 'bc':
+                               $keepMetadata = array_intersect_key( $metadata, array(
+                                       self::META_INDEXED_TAG_NAME => 1,
+                                       self::META_SUBELEMENTS => 1,
+                               ) );
+                               break;
+                       default:
+                               throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
+               }
+
+               // Type transformation
+               if ( $transformTypes !== null ) {
+                       if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
+                               $defaultType = 'assoc';
+                       }
+
+                       // Override type, if provided
+                       $type = $defaultType;
+                       if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
+                               $type = $metadata[self::META_TYPE];
+                       }
+                       if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
+                               empty( $transformTypes['ArmorKVP'] )
+                       ) {
+                               $type = 'assoc';
+                       } elseif ( $type === 'BCarray' ) {
+                               $type = 'array';
+                       } elseif ( $type === 'BCassoc' ) {
+                               $type = 'assoc';
+                       }
+
+                       // Apply transformation
+                       switch ( $type ) {
+                               case 'assoc':
+                                       $metadata[self::META_TYPE] = 'assoc';
+                                       $data += $keepMetadata;
+                                       return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
+
+                               case 'array':
+                                       ksort( $data );
+                                       $data = array_values( $data );
+                                       $metadata[self::META_TYPE] = 'array';
+                                       return $data + $keepMetadata;
+
+                               case 'kvp':
+                               case 'BCkvp':
+                                       $key = isset( $metadata[self::META_KVP_KEY_NAME] )
+                                               ? $metadata[self::META_KVP_KEY_NAME]
+                                               : $transformTypes['ArmorKVP'];
+                                       $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
+                                       $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
+
+                                       $ret = array();
+                                       foreach ( $data as $k => $v ) {
+                                               $item = array(
+                                                       $key => $k,
+                                                       $valKey => $v,
+                                               );
+                                               if ( $strip === 'none' ) {
+                                                       $item += array(
+                                                               self::META_PRESERVE_KEYS => array( $key ),
+                                                               self::META_CONTENT => $valKey,
+                                                               self::META_TYPE => 'assoc',
+                                                       );
+                                               }
+                                               $ret[] = $assocAsObject ? (object)$item : $item;
+                                       }
+                                       $metadata[self::META_TYPE] = 'array';
+
+                                       return $ret + $keepMetadata;
+
+                               default:
+                                       throw new UnexpectedValueException( "Unknown type '$type'" );
+                       }
+               } else {
+                       return $data + $keepMetadata;
+               }
+       }
+
+       /**
+        * Recursively remove metadata keys from a data array or object
+        *
+        * Note this removes all potential metadata keys, not just the defined
+        * ones.
+        *
+        * @since 1.25
+        * @param array|object $data
+        * @return array|object
+        */
+       public static function stripMetadata( $data ) {
+               if ( is_array( $data ) || is_object( $data ) ) {
+                       $isObj = is_object( $data );
+                       if ( $isObj ) {
+                               $data = (array)$data;
+                       }
+                       $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
+                               ? (array)$data[self::META_PRESERVE_KEYS]
+                               : array();
+                       foreach ( $data as $k => $v ) {
+                               if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
+                                       unset( $data[$k] );
+                               } elseif ( is_array( $v ) || is_object( $v ) ) {
+                                       $data[$k] = self::stripMetadata( $v );
+                               }
+                       }
+                       if ( $isObj ) {
+                               $data = (object)$data;
+                       }
+               }
+               return $data;
+       }
+
+       /**
+        * Remove metadata keys from a data array or object, non-recursive
+        *
+        * Note this removes all potential metadata keys, not just the defined
+        * ones.
+        *
+        * @since 1.25
+        * @param array|object $data
+        * @param array &$metadata Store metadata here, if provided
+        * @return array|object
+        */
+       public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
+               if ( !is_array( $metadata ) ) {
+                       $metadata = array();
+               }
+               if ( is_array( $data ) || is_object( $data ) ) {
+                       $isObj = is_object( $data );
+                       if ( $isObj ) {
+                               $data = (array)$data;
+                       }
+                       $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
+                               ? (array)$data[self::META_PRESERVE_KEYS]
+                               : array();
+                       foreach ( $data as $k => $v ) {
+                               if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
+                                       $metadata[$k] = $v;
+                                       unset( $data[$k] );
+                               }
+                       }
+                       if ( $isObj ) {
+                               $data = (object)$data;
+                       }
+               }
+               return $data;
+       }
+
+       /**
+        * Get the 'real' size of a result item. This means the strlen() of the item,
+        * or the sum of the strlen()s of the elements if the item is an array.
+        * @note Once the deprecated public self::size is removed, we can rename this back to a less awkward name.
+        * @param mixed $value
+        * @return int
+        */
+       private static function valueSize( $value ) {
+               $s = 0;
+               if ( is_array( $value ) ||
+                       is_object( $value ) && !is_callable( array( $value, '__toString' ) )
+               ) {
+                       foreach ( $value as $k => $v ) {
+                               if ( !self::isMetadataKey( $s ) ) {
+                                       $s += self::valueSize( $v );
+                               }
+                       }
+               } elseif ( is_scalar( $value ) ) {
+                       $s = strlen( $value );
+               }
+
+               return $s;
+       }
+
+       /**
+        * Return a reference to the internal data at $path
+        *
+        * @param array|string|null $path
+        * @param string $create
+        *   If 'append', append empty arrays.
+        *   If 'prepend', prepend empty arrays.
+        *   If 'dummy', return a dummy array.
+        *   Else, raise an error.
+        * @return array
+        */
+       private function &path( $path, $create = 'append' ) {
+               $path = (array)$path;
+               $ret = &$this->data;
+               foreach ( $path as $i => $k ) {
+                       if ( !isset( $ret[$k] ) ) {
+                               switch ( $create ) {
+                                       case 'append':
+                                               $ret[$k] = array();
+                                               break;
+                                       case 'prepend':
+                                               $ret = array( $k => array() ) + $ret;
+                                               break;
+                                       case 'dummy':
+                                               $tmp = array();
+                                               return $tmp;
+                                       default:
+                                               $fail = join( '.', array_slice( $path, 0, $i + 1 ) );
+                                               throw new InvalidArgumentException( "Path $fail does not exist" );
+                               }
+                       }
+                       if ( !is_array( $ret[$k] ) ) {
+                               $fail = join( '.', array_slice( $path, 0, $i + 1 ) );
+                               throw new InvalidArgumentException( "Path $fail is not an array" );
+                       }
+                       $ret = &$ret[$k];
+               }
+               return $ret;
+       }
+
+       /**@}*/
+
+       /************************************************************************//**
+        * @name   Deprecated
+        * @{
+        */
+
+       /**
+        * Call this function when special elements such as '_element'
+        * are needed by the formatter, for example in XML printing.
+        * @deprecated since 1.25, you shouldn't have been using it in the first place
+        * @since 1.23 $flag parameter added
+        * @param bool $flag Set the raw mode flag to this state
+        */
+       public function setRawMode( $flag = true ) {
+               // Can't wfDeprecated() here, since we need to set this flag from
+               // ApiMain for BC with stuff using self::getIsRawMode as
+               // "self::getIsXMLMode".
+               $this->isRawMode = $flag;
+       }
+
+       /**
+        * Returns true whether the formatter requested raw data.
+        * @deprecated since 1.25, you shouldn't have been using it in the first place
+        * @return bool
+        */
+       public function getIsRawMode() {
+               /// @todo: After Wikibase stops calling this, warn
+               return $this->isRawMode;
+       }
+
+       /**
+        * Get the result's internal data array (read-only)
+        * @deprecated since 1.25, use $this->getResultData() instead
+        * @return array
+        */
+       public function getData() {
+               /// @todo: Warn after fixing remaining callers: Wikibase, Gather
+               return $this->getResultData( null, array(
+                       'BC' => array(),
+                       'Types' => array(),
+                       'Strip' => $this->isRawMode ? 'bc' : 'all',
+               ) );
+       }
+
+       /**
+        * Disable size checking in addValue(). Don't use this unless you
+        * REALLY know what you're doing. Values added while size checking
+        * was disabled will not be counted (ever)
+        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
+        */
+       public function disableSizeCheck() {
+               wfDeprecated( __METHOD__, '1.24' );
+               $this->checkingSize = false;
+       }
+
+       /**
+        * Re-enable size checking in addValue()
+        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
+        */
+       public function enableSizeCheck() {
+               wfDeprecated( __METHOD__, '1.24' );
+               $this->checkingSize = true;
+       }
+
+       /**
+        * Alias for self::setValue()
+        *
+        * @since 1.21 int $flags replaced boolean $override
+        * @deprecated since 1.25, use self::setValue() instead
+        * @param array $arr To add $value to
+        * @param string $name Index of $arr to add $value at
+        * @param mixed $value
+        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
+        *    This parameter used to be boolean, and the value of OVERRIDE=1 was
+        *    specifically chosen so that it would be backwards compatible with the
+        *    new method signature.
+        */
+       public static function setElement( &$arr, $name, $value, $flags = 0 ) {
+               /// @todo: Warn after fixing remaining callers: Wikibase
+               return self::setValue( $arr, $name, $value, $flags );
+       }
+
+       /**
+        * Adds a content element to an array.
+        * Use this function instead of hardcoding the '*' element.
+        * @deprecated since 1.25, use self::setContentValue() instead
+        * @param array $arr To add the content element to
+        * @param mixed $value
+        * @param string $subElemName When present, content element is created
+        *  as a sub item of $arr. Use this parameter to create elements in
+        *  format "<elem>text</elem>" without attributes.
+        */
+       public static function setContent( &$arr, $value, $subElemName = null ) {
+               /// @todo: Warn after fixing remaining callers: Wikibase
+               if ( is_array( $value ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ': Bad parameter' );
+               }
+               if ( is_null( $subElemName ) ) {
+                       self::setContentValue( $arr, 'content', $value );
+               } else {
+                       if ( !isset( $arr[$subElemName] ) ) {
+                               $arr[$subElemName] = array();
+                       }
+                       self::setContentValue( $arr[$subElemName], 'content', $value );
+               }
+       }
+
+       /**
+        * Set indexed tag name on all subarrays of $arr
+        *
+        * Does not set the tag name for $arr itself.
+        *
+        * @deprecated since 1.25, use self::setIndexedTagNameRecursive() instead
+        * @param array $arr
+        * @param string $tag Tag name
+        */
+       public function setIndexedTagName_recursive( &$arr, $tag ) {
+               /// @todo: Warn after fixing remaining callers: Wikibase
+               if ( !is_array( $arr ) ) {
+                       return;
+               }
+               self::setIndexedTagNameOnSubarrays( $arr, $tag );
+       }
+
+       /**
+        * Set indexed tag name on all subarrays of $arr
+        *
+        * Does not set the tag name for $arr itself.
+        *
+        * @since 1.25
+        * @deprecated For backwards compatibility, do not use
+        * @todo: Remove after updating callers to use self::setIndexedTagNameRecursive
+        * @param array &$arr
+        * @param string $tag Tag name
+        */
+       public static function setIndexedTagNameOnSubarrays( array &$arr, $tag ) {
+               if ( !is_string( $tag ) ) {
+                       throw new InvalidArgumentException( 'Bad tag name' );
+               }
+               foreach ( $arr as $k => &$v ) {
+                       if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
+                               $v[self::META_INDEXED_TAG_NAME] = $tag;
+                               self::setIndexedTagNameOnSubarrays( $v, $tag );
+                       }
+               }
+       }
+
+       /**
+        * Alias for self::defineIndexedTagName()
+        * @deprecated since 1.25, use $this->addIndexedTagName() instead
+        * @param array $path Path to the array, like addValue()'s $path
+        * @param string $tag
+        */
+       public function setIndexedTagName_internal( $path, $tag ) {
+               /// @todo: Warn after fixing remaining callers: Wikibase, Gather
+               $this->addIndexedTagName( $path, $tag );
+       }
+
+       /**
+        * Alias for self::addParsedLimit()
+        * @deprecated since 1.25, use $this->addParsedLimit() instead
+        * @param string $moduleName
+        * @param int $limit
+        */
+       public function setParsedLimit( $moduleName, $limit ) {
+               wfDeprecated( __METHOD__, '1.25' );
+               $this->addParsedLimit( $moduleName, $limit );
+       }
+
+       /**
+        * Set the ApiMain for use by $this->beginContinuation()
+        * @since 1.25
+        * @deprecated for backwards compatibility only, do not use
+        * @param ApiMain $main
+        */
+       public function setMainForContinuation( ApiMain $main ) {
+               $this->mainForContinuation = $main;
+       }
+
+       /**
+        * Parse a 'continue' parameter and return status information.
+        *
+        * This must be balanced by a call to endContinuation().
+        *
+        * @since 1.24
+        * @deprecated since 1.25, use ApiContinuationManager instead
+        * @param string|null $continue
+        * @param ApiBase[] $allModules
+        * @param array $generatedModules
+        * @return array
+        */
+       public function beginContinuation(
+               $continue, array $allModules = array(), array $generatedModules = array()
+       ) {
+               /// @todo: Warn after fixing remaining callers: Gather
+               if ( $this->mainForContinuation->getContinuationManager() ) {
+                       throw new UnexpectedValueException(
+                               __METHOD__ . ': Continuation already in progress from ' .
+                               $this->mainForContinuation->getContinuationManager()->getSource()
                        );
                }
-               if ( !$this->continueAllModules[$name] ) {
-                       throw new MWException(
-                               "Module '$name' was not supposed to have been executed, but " .
-                               'it was executed anyway'
+
+               // Ugh. If $continue doesn't match that in the request, temporarily
+               // replace the request when creating the ApiContinuationManager.
+               if ( $continue === null ) {
+                       $continue = '';
+               }
+               if ( $this->mainForContinuation->getVal( 'continue', '' ) !== $continue ) {
+                       $oldCtx = $this->mainForContinuation->getContext();
+                       $newCtx = new DerivativeContext( $oldCtx );
+                       $newCtx->setRequest( new DerivativeRequest(
+                               $oldCtx->getRequest(),
+                               array( 'continue' => $continue ) + $oldCtx->getRequest()->getValues(),
+                               $oldCtx->getRequest()->wasPosted()
+                       ) );
+                       $this->mainForContinuation->setContext( $newCtx );
+                       $reset = new ScopedCallback(
+                               array( $this->mainForContinuation, 'setContext' ),
+                               array( $oldCtx )
                        );
                }
-               $paramName = $module->encodeParamName( $paramName );
-               if ( is_array( $paramValue ) ) {
-                       $paramValue = join( '|', $paramValue );
+               $manager = new ApiContinuationManager(
+                       $this->mainForContinuation, $allModules, $generatedModules
+               );
+               $reset = null;
+
+               $this->mainForContinuation->setContinuationManager( $manager );
+
+               return array(
+                       $manager->isGeneratorDone(),
+                       $manager->getRunModules(),
+               );
+       }
+
+       /**
+        * @since 1.24
+        * @deprecated since 1.25, use ApiContinuationManager instead
+        * @param ApiBase $module
+        * @param string $paramName
+        * @param string|array $paramValue
+        */
+       public function setContinueParam( ApiBase $module, $paramName, $paramValue ) {
+               wfDeprecated( __METHOD__, '1.25' );
+               if ( $this->mainForContinuation->getContinuationManager() ) {
+                       $this->mainForContinuation->getContinuationManager()->addContinueParam(
+                               $module, $paramName, $paramValue
+                       );
                }
-               $this->continuationData[$name][$paramName] = $paramValue;
        }
 
        /**
-        * Set the continuation parameter for the generator module
-        *
         * @since 1.24
+        * @deprecated since 1.25, use ApiContinuationManager instead
         * @param ApiBase $module
         * @param string $paramName
         * @param string|array $paramValue
         */
        public function setGeneratorContinueParam( ApiBase $module, $paramName, $paramValue ) {
-               $name = $module->getModuleName();
-               $paramName = $module->encodeParamName( $paramName );
-               if ( is_array( $paramValue ) ) {
-                       $paramValue = join( '|', $paramValue );
+               wfDeprecated( __METHOD__, '1.25' );
+               if ( $this->mainForContinuation->getContinuationManager() ) {
+                       $this->mainForContinuation->getContinuationManager()->addGeneratorContinueParam(
+                               $module, $paramName, $paramValue
+                       );
                }
-               $this->generatorContinuationData[$name][$paramName] = $paramValue;
        }
 
        /**
         * Close continuation, writing the data into the result
-        *
         * @since 1.24
+        * @deprecated since 1.25, use ApiContinuationManager instead
         * @param string $style 'standard' for the new style since 1.21, 'raw' for
         *   the style used in 1.20 and earlier.
         */
        public function endContinuation( $style = 'standard' ) {
+               /// @todo: Warn after fixing remaining callers: Gather
+               if ( !$this->mainForContinuation->getContinuationManager() ) {
+                       return;
+               }
+
                if ( $style === 'raw' ) {
-                       $key = 'query-continue';
-                       $data = array_merge_recursive(
-                               $this->continuationData, $this->generatorContinuationData
-                       );
+                       $data = $this->mainForContinuation->getContinuationManager()->getRawContinuation();
+                       if ( $data ) {
+                               $this->addValue( null, 'query-continue', $data, self::ADD_ON_TOP | self::NO_SIZE_CHECK );
+                       }
                } else {
-                       $key = 'continue';
-                       $data = array();
-                       $batchcomplete = false;
+                       $this->mainForContinuation->getContinuationManager()->setContinuationIntoResult( $this );
+               }
+       }
 
-                       $finishedModules = array_diff(
-                               array_keys( $this->continueAllModules ),
-                               array_keys( $this->continuationData )
-                       );
+       /**
+        * No-op, this is now checked on insert.
+        * @deprecated since 1.25
+        */
+       public function cleanUpUTF8() {
+               wfDeprecated( __METHOD__, '1.25' );
+       }
 
-                       // First, grab the non-generator-using continuation data
-                       $continuationData = array_diff_key(
-                               $this->continuationData, $this->continueGeneratedModules
-                       );
-                       foreach ( $continuationData as $module => $kvp ) {
-                               $data += $kvp;
-                       }
+       /**
+        * Get the 'real' size of a result item. This means the strlen() of the item,
+        * or the sum of the strlen()s of the elements if the item is an array.
+        * @deprecated since 1.25, no external users known and there doesn't seem
+        *  to be any case for such use over just checking the return value from the
+        *  add/set methods.
+        * @param mixed $value
+        * @return int
+        */
+       public static function size( $value ) {
+               wfDeprecated( __METHOD__, '1.25' );
+               return self::valueSize( $value );
+       }
 
-                       // Next, handle the generator-using continuation data
-                       $continuationData = array_intersect_key(
-                               $this->continuationData, $this->continueGeneratedModules
-                       );
-                       if ( $continuationData ) {
-                               // Some modules are unfinished: include those params, and copy
-                               // the generator params.
-                               foreach ( $continuationData as $module => $kvp ) {
-                                       $data += $kvp;
-                               }
-                               $data += array_intersect_key(
-                                       $this->getMain()->getRequest()->getValues(),
-                                       array_flip( $this->generatorParams )
-                               );
-                       } elseif ( $this->generatorContinuationData ) {
-                               // All the generator-using modules are complete, but the
-                               // generator isn't. Continue the generator and restart the
-                               // generator-using modules
-                               $this->generatorParams = array();
-                               foreach ( $this->generatorContinuationData as $kvp ) {
-                                       $this->generatorParams = array_merge(
-                                               $this->generatorParams, array_keys( $kvp )
-                                       );
-                                       $data += $kvp;
-                               }
-                               $finishedModules = array_diff(
-                                       $finishedModules, $this->continueGeneratedModules
-                               );
-                               $batchcomplete = true;
-                       } else {
-                               // Generator and prop modules are all done. Mark it so.
-                               $this->generatorDone = true;
-                               $batchcomplete = true;
-                       }
+       /**
+        * Converts a Status object to an array suitable for addValue
+        * @deprecated since 1.25, use ApiErrorFormatter::arrayFromStatus()
+        * @param Status $status
+        * @param string $errorType
+        * @return array
+        */
+       public function convertStatusToArray( $status, $errorType = 'error' ) {
+               /// @todo: Warn after fixing remaining callers: CentralAuth
+               return $this->errorFormatter->arrayFromStatus( $status, $errorType );
+       }
 
-                       // Set 'continue' if any continuation data is set or if the generator
-                       // still needs to run
-                       if ( $data || !$this->generatorDone ) {
-                               $data['continue'] =
-                                       ( $this->generatorDone ? '-' : join( '|', $this->generatorParams ) ) .
-                                       '||' . join( '|', $finishedModules );
-                       }
+       /**
+        * Alias for self::addIndexedTagName
+        *
+        * A bunch of extensions were updated for an earlier version of this
+        * extension which used this name.
+        * @deprecated For backwards compatibility, do not use
+        * @todo: Remove after updating callers to use self::addIndexedTagName
+        */
+       public function defineIndexedTagName( $path, $tag ) {
+               return $this->addIndexedTagName( $path, $tag );
+       }
 
-                       if ( $batchcomplete ) {
-                               $this->addValue( null, 'batchcomplete', '', ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
-                       }
+       /**
+        * Alias for self::stripMetadata
+        *
+        * A bunch of extensions were updated for an earlier version of this
+        * extension which used this name.
+        * @deprecated For backwards compatibility, do not use
+        * @todo: Remove after updating callers to use self::stripMetadata
+        */
+       public static function removeMetadata( $data ) {
+               return self::stripMetadata( $data );
+       }
+
+       /**
+        * Alias for self::stripMetadataNonRecursive
+        *
+        * A bunch of extensions were updated for an earlier version of this
+        * extension which used this name.
+        * @deprecated For backwards compatibility, do not use
+        * @todo: Remove after updating callers to use self::stripMetadataNonRecursive
+        */
+       public static function removeMetadataNonRecursive( $data, &$metadata = null ) {
+               self::stripMetadataNonRecursive( $data, $metadata );
+       }
+
+       /**
+        * @deprecated For backwards compatibility, do not use
+        * @todo: Remove after updating callers
+        */
+       public static function transformForBC( array $data ) {
+               return self::applyTransformations( $data, array(
+                       'BC' => array(),
+               ) );
+       }
+
+       /**
+        * @deprecated For backwards compatibility, do not use
+        * @todo: Remove after updating callers
+        */
+       public static function transformForTypes( $data, $options = array() ) {
+               $transforms = array(
+                       'Types' => array(),
+               );
+               if ( isset( $options['assocAsObject'] ) ) {
+                       $transforms['Types']['AssocAsObject'] = $options['assocAsObject'];
                }
-               if ( $data ) {
-                       $this->addValue( null, $key, $data, ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+               if ( isset( $options['armorKVP'] ) ) {
+                       $transforms['Types']['ArmorKVP'] = $options['armorKVP'];
                }
+               if ( !empty( $options['BC'] ) ) {
+                       $transforms['BC'] = array( 'nobool', 'no*', 'nosub' );
+               }
+               return self::applyTransformations( $data, $transforms );
        }
+
+       /**@}*/
 }
+
+/**
+ * For really cool vim folding this needs to be at the end:
+ * vim: foldmarker=@{,@} foldmethod=marker
+ */
index 783a39b..2896231 100644 (file)
@@ -111,7 +111,7 @@ class ApiRevisionDelete extends ApiBase {
                // @codingStandardsIgnoreEnd
 
                $data['items'] = array_values( $data['items'] );
-               $result->setIndexedTagName( $data['items'], 'i' );
+               ApiResult::setIndexedTagName( $data['items'], 'i' );
                $result->addValue( null, $this->getModuleName(), $data );
        }
 
@@ -121,12 +121,12 @@ class ApiRevisionDelete extends ApiBase {
                );
                $errors = $this->formatStatusMessages( $status->getErrorsByType( 'error' ) );
                if ( $errors ) {
-                       $this->getResult()->setIndexedTagName( $errors, 'e' );
+                       ApiResult::setIndexedTagName( $errors, 'e' );
                        $ret['errors'] = $errors;
                }
                $warnings = $this->formatStatusMessages( $status->getErrorsByType( 'warning' ) );
                if ( $warnings ) {
-                       $this->getResult()->setIndexedTagName( $warnings, 'w' );
+                       ApiResult::setIndexedTagName( $warnings, 'w' );
                        $ret['warnings'] = $warnings;
                }
 
@@ -146,14 +146,14 @@ class ApiRevisionDelete extends ApiBase {
                                $message = array( 'message' => $msg->getKey() );
                                if ( $msg->getParams() ) {
                                        $message['params'] = $msg->getParams();
-                                       $result->setIndexedTagName( $message['params'], 'p' );
+                                       ApiResult::setIndexedTagName( $message['params'], 'p' );
                                }
                        } else {
                                $message = array( 'message' => $m['message'] );
                                $msg = wfMessage( $m['message'] );
                                if ( isset( $m['params'] ) ) {
                                        $message['params'] = $m['params'];
-                                       $result->setIndexedTagName( $message['params'], 'p' );
+                                       ApiResult::setIndexedTagName( $message['params'], 'p' );
                                        $msg->params( $m['params'] );
                                }
                        }
index f28e610..d466112 100644 (file)
@@ -37,12 +37,15 @@ class ApiRsd extends ApiBase {
                $result->addValue( null, 'version', '1.0' );
                $result->addValue( null, 'xmlns', 'http://archipelago.phrasewise.com/rsd' );
 
-               $service = array( 'apis' => $this->formatRsdApiList() );
-               ApiResult::setContent( $service, 'MediaWiki', 'engineName' );
-               ApiResult::setContent( $service, 'https://www.mediawiki.org/', 'engineLink' );
-               ApiResult::setContent( $service, Title::newMainPage()->getCanonicalURL(), 'homePageLink' );
+               $service = array(
+                       'apis' => $this->formatRsdApiList(),
+                       'engineName' => 'MediaWiki',
+                       'engineLink' => 'https://www.mediawiki.org/',
+                       'homePageLink' => Title::newMainPage()->getCanonicalURL(),
+               );
 
-               $result->setIndexedTagName( $service['apis'], 'api' );
+               ApiResult::setSubelementsList( $service, array( 'engineName', 'engineLink', 'homePageLink' ) );
+               ApiResult::setIndexedTagName( $service['apis'], 'api' );
 
                $result->addValue( null, 'service', $service );
        }
@@ -123,7 +126,8 @@ class ApiRsd extends ApiBase {
                        );
                        $settings = array();
                        if ( isset( $info['docs'] ) ) {
-                               ApiResult::setContent( $settings, $info['docs'], 'docs' );
+                               $settings['docs'] = $info['docs'];
+                               ApiResult::setSubelementsList( $settings, 'docs' );
                        }
                        if ( isset( $info['settings'] ) ) {
                                foreach ( $info['settings'] as $setting => $val ) {
@@ -133,12 +137,12 @@ class ApiRsd extends ApiBase {
                                                $xmlVal = $val;
                                        }
                                        $setting = array( 'name' => $setting );
-                                       ApiResult::setContent( $setting, $xmlVal );
+                                       ApiResult::setContentValue( $setting, 'value', $xmlVal );
                                        $settings[] = $setting;
                                }
                        }
                        if ( count( $settings ) ) {
-                               $this->getResult()->setIndexedTagName( $settings, 'setting' );
+                               ApiResult::setIndexedTagName( $settings, 'setting' );
                                $data['settings'] = $settings;
                        }
                        $outputData[] = $data;
@@ -157,4 +161,9 @@ class ApiFormatXmlRsd extends ApiFormatXml {
        public function getMimeType() {
                return 'application/rsd+xml';
        }
+
+       public static function recXmlPrint( $name, $value, $indent, $attributes = array() ) {
+               unset( $attributes['_idx'] );
+               return parent::recXmlPrint( $name, $value, $indent, $attributes );
+       }
 }
diff --git a/includes/api/ApiSerializable.php b/includes/api/ApiSerializable.php
new file mode 100644 (file)
index 0000000..70e93a6
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Created on Feb 25, 2015
+ *
+ * Copyright © 2015 Brad Jorsch "bjorsch@wikimedia.org"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * This interface allows for overriding the default conversion applied by
+ * ApiResult::validateValue().
+ *
+ * @note This is currently an informal interface; it need not be explicitly
+ *   implemented, as long as the method is provided. This allows for extension
+ *   code to maintain compatibility with older MediaWiki while still taking
+ *   advantage of this where it exists.
+ *
+ * @ingroup API
+ * @since 1.25
+ */
+interface ApiSerializable {
+       /**
+        * Return the value to be added to ApiResult in place of this object.
+        *
+        * The returned value must not be an object, and must pass
+        * all checks done by ApiResult::validateValue().
+        *
+        * @return mixed
+        */
+       public function serializeForApiResult();
+}
index dec64cc..e41ee07 100644 (file)
@@ -46,7 +46,8 @@ class ApiSetNotificationTimestamp extends ApiBase {
                $params = $this->extractRequestParams();
                $this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' );
 
-               $this->getResult()->beginContinuation( $params['continue'], array(), array() );
+               $continuationManager = new ApiContinuationManager( $this, array(), array() );
+               $this->setContinuationManager( $continuationManager );
 
                $pageSet = $this->getPageSet();
                if ( $params['entirewatchlist'] && $pageSet->getDataSource() !== null ) {
@@ -176,11 +177,12 @@ class ApiSetNotificationTimestamp extends ApiBase {
                                }
                        }
 
-                       $apiResult->setIndexedTagName( $result, 'page' );
+                       ApiResult::setIndexedTagName( $result, 'page' );
                }
                $apiResult->addValue( null, $this->getModuleName(), $result );
 
-               $apiResult->endContinuation();
+               $this->setContinuationManager( null );
+               $continuationManager->setContinuationIntoResult( $apiResult );
        }
 
        /**
index 78a4971..74ae05a 100644 (file)
@@ -514,20 +514,20 @@ class ApiUpload extends ApiBase {
                                        'filetype' => $verification['finalExt'],
                                        'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
                                );
-                               $this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' );
+                               ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
 
                                $msg = "Filetype not permitted: ";
                                if ( isset( $verification['blacklistedExt'] ) ) {
                                        $msg .= join( ', ', $verification['blacklistedExt'] );
                                        $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
-                                       $this->getResult()->setIndexedTagName( $extradata['blacklisted'], 'ext' );
+                                       ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
                                } else {
                                        $msg .= $verification['finalExt'];
                                }
                                $this->dieUsage( $msg, 'filetype-banned', 0, $extradata );
                                break;
                        case UploadBase::VERIFICATION_ERROR:
-                               $this->getResult()->setIndexedTagName( $verification['details'], 'detail' );
+                               ApiResult::setIndexedTagName( $verification['details'], 'detail' );
                                $this->dieUsage( 'This file did not pass file verification', 'verification-error',
                                        0, array( 'details' => $verification['details'] ) );
                                break;
@@ -559,7 +559,7 @@ class ApiUpload extends ApiBase {
                if ( $warnings ) {
                        // Add indices
                        $result = $this->getResult();
-                       $result->setIndexedTagName( $warnings, 'warning' );
+                       ApiResult::setIndexedTagName( $warnings, 'warning' );
 
                        if ( isset( $warnings['duplicate'] ) ) {
                                $dupes = array();
@@ -567,7 +567,7 @@ class ApiUpload extends ApiBase {
                                foreach ( $warnings['duplicate'] as $dupe ) {
                                        $dupes[] = $dupe->getName();
                                }
-                               $result->setIndexedTagName( $dupes, 'duplicate' );
+                               ApiResult::setIndexedTagName( $dupes, 'duplicate' );
                                $warnings['duplicate'] = $dupes;
                        }
 
@@ -696,7 +696,7 @@ class ApiUpload extends ApiBase {
                                        );
                                }
 
-                               $this->getResult()->setIndexedTagName( $error, 'error' );
+                               ApiResult::setIndexedTagName( $error, 'error' );
                                $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error );
                        }
                        $result['result'] = 'Success';
index cf8ed5a..3ccdde2 100644 (file)
@@ -63,8 +63,8 @@ class ApiUserrights extends ApiBase {
                );
 
                $result = $this->getResult();
-               $result->setIndexedTagName( $r['added'], 'group' );
-               $result->setIndexedTagName( $r['removed'], 'group' );
+               ApiResult::setIndexedTagName( $r['added'], 'group' );
+               ApiResult::setIndexedTagName( $r['removed'], 'group' );
                $result->addValue( null, $this->getModuleName(), $r );
        }
 
index 09638f3..6a585d4 100644 (file)
@@ -44,7 +44,8 @@ class ApiWatch extends ApiBase {
 
                $params = $this->extractRequestParams();
 
-               $this->getResult()->beginContinuation( $params['continue'], array(), array() );
+               $continuationManager = new ApiContinuationManager( $this, array(), array() );
+               $this->setContinuationManager( $continuationManager );
 
                $pageSet = $this->getPageSet();
                // by default we use pageset to extract the page to work on.
@@ -69,7 +70,7 @@ class ApiWatch extends ApiBase {
                                $r = $this->watchTitle( $title, $user, $params );
                                $res[] = $r;
                        }
-                       $this->getResult()->setIndexedTagName( $res, 'w' );
+                       ApiResult::setIndexedTagName( $res, 'w' );
                } else {
                        // dont allow use of old title parameter with new pageset parameters.
                        $extraParams = array_keys( array_filter( $pageSet->extractRequestParams(), function ( $x ) {
@@ -92,7 +93,9 @@ class ApiWatch extends ApiBase {
                        $res = $this->watchTitle( $title, $user, $params, true );
                }
                $this->getResult()->addValue( null, $this->getModuleName(), $res );
-               $this->getResult()->endContinuation();
+
+               $this->setContinuationManager( null );
+               $continuationManager->setContinuationIntoResult( $this->getResult() );
        }
 
        private function watchTitle( Title $title, User $user, array $params,
index 4055386..ee66238 100644 (file)
        "apihelp-dumpfm-description": "Output data in PHP's <code>var_dump()</code> format (pretty-print in HTML).",
        "apihelp-json-description": "Output data in JSON format.",
        "apihelp-json-param-callback": "If specified, wraps the output into a given function call. For safety, all user-specific data will be restricted.",
-       "apihelp-json-param-utf8": "If specified, encodes most (but not all) non-ASCII characters as UTF-8 instead of replacing them with hexadecimal escape sequences.",
+       "apihelp-json-param-utf8": "If specified, encodes most (but not all) non-ASCII characters as UTF-8 instead of replacing them with hexadecimal escape sequences. Default when <var>formatversion</var> is not <kbd>1</kbd>.",
+       "apihelp-json-param-ascii": "If specified, encodes all non-ASCII using hexadecimal escape sequences. Default when <var>formatversion</var> is <kbd>1</kbd>.",
+       "apihelp-json-param-formatversion": "Output formatting:\n;1:Backwards-compatible format (XML-style booleans, <samp>*</samp> keys for content nodes, etc.).\n;2:Experimental modern format. Details may change!\n;latest:Use the latest format (currently <kbd>2</kbd>), may change without warning.",
        "apihelp-jsonfm-description": "Output data in JSON format (pretty-print in HTML).",
        "apihelp-none-description": "Output nothing.",
        "apihelp-php-description": "Output data in serialized PHP format.",
+       "apihelp-php-param-formatversion": "Output formatting:\n;1:Backwards-compatible format (XML-style booleans, <samp>*</samp> keys for content nodes, etc.).\n;2:Experimental modern format. Details may change!\n;latest:Use the latest format (currently <kbd>2</kbd>), may change without warning.",
        "apihelp-phpfm-description": "Output data in serialized PHP format (pretty-print in HTML).",
        "apihelp-rawfm-description": "Output data with the debugging elements in JSON format (pretty-print in HTML).",
        "apihelp-txt-description": "Output data in PHP's <code>print_r()</code> format.",
index 2d09844..d2c6ec9 100644 (file)
        "apihelp-json-description": "{{doc-apihelp-description|json|seealso=* {{msg-mw|apihelp-jsonfm-description}}}}",
        "apihelp-json-param-callback": "{{doc-apihelp-param|json|callback}}",
        "apihelp-json-param-utf8": "{{doc-apihelp-param|json|utf8}}",
+       "apihelp-json-param-ascii": "{{doc-apihelp-param|json|ascii}}",
+       "apihelp-json-param-formatversion": "{{doc-apihelp-param|json|formatversion}}",
        "apihelp-jsonfm-description": "{{doc-apihelp-description|jsonfm|seealso=* {{msg-mw|apihelp-json-description}}}}",
        "apihelp-none-description": "{{doc-apihelp-description|none}}",
        "apihelp-php-description": "{{doc-apihelp-description|php|seealso=* {{msg-mw|apihelp-phpfm-description}}}}",
+       "apihelp-php-param-formatversion": "{{doc-apihelp-param|json|formatversion}}",
        "apihelp-phpfm-description": "{{doc-apihelp-description|phpfm|seealso=* {{msg-mw|apihelp-php-description}}}}",
        "apihelp-rawfm-description": "{{doc-apihelp-description|rawfm|seealso=* {{msg-mw|apihelp-raw-description}}}}",
        "apihelp-txt-description": "{{doc-apihelp-description|txt|seealso=* {{msg-mw|apihelp-txtfm-description}}}}",
index 1939e06..ae2d995 100644 (file)
@@ -540,11 +540,11 @@ class MWDebug {
                MWDebug::log( 'MWDebug output complete' );
                $debugInfo = self::getDebugInfo( $context );
 
-               $result->setIndexedTagName( $debugInfo, 'debuginfo' );
-               $result->setIndexedTagName( $debugInfo['log'], 'line' );
-               $result->setIndexedTagName( $debugInfo['debugLog'], 'msg' );
-               $result->setIndexedTagName( $debugInfo['queries'], 'query' );
-               $result->setIndexedTagName( $debugInfo['includes'], 'queries' );
+               ApiResult::setIndexedTagName( $debugInfo, 'debuginfo' );
+               ApiResult::setIndexedTagName( $debugInfo['log'], 'line' );
+               ApiResult::setIndexedTagName( $debugInfo['debugLog'], 'msg' );
+               ApiResult::setIndexedTagName( $debugInfo['queries'], 'query' );
+               ApiResult::setIndexedTagName( $debugInfo['includes'], 'queries' );
                $result->addValue( null, 'debuginfo', $debugInfo );
        }
 
diff --git a/tests/phpunit/includes/api/ApiContinuationManagerTest.php b/tests/phpunit/includes/api/ApiContinuationManagerTest.php
new file mode 100644 (file)
index 0000000..ea08c02
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+
+/**
+ * @covers ApiContinuationManager
+ * @group API
+ */
+class ApiContinuationManagerTest extends MediaWikiTestCase {
+
+       private static function getManager( $continue, $allModules, $generatedModules ) {
+               $context = new DerivativeContext( RequestContext::getMain() );
+               $context->setRequest( new FauxRequest( array( 'continue' => $continue ) ) );
+               $main = new ApiMain( $context );
+               return new ApiContinuationManager( $main, $allModules, $generatedModules );
+       }
+
+       public function testContinuation() {
+               $allModules = array(
+                       new MockApiQueryBase( 'mock1' ),
+                       new MockApiQueryBase( 'mock2' ),
+                       new MockApiQueryBase( 'mocklist' ),
+               );
+               $generator = new MockApiQueryBase( 'generator' );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( 'ApiMain', $manager->getSource() );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $this->assertSame( array( array(
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ), false ), $manager->getContinuation() );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+                       'generator' => array( 'gcontinue' => 3 ),
+               ), $manager->getRawContinuation() );
+
+               $result = new ApiResult( 0 );
+               $manager->setContinuationIntoResult( $result );
+               $this->assertSame( array(
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', array( 3, 4 ) );
+               $this->assertSame( array( array(
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2|mocklist',
+               ), false ), $manager->getContinuation() );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+                       'generator' => array( 'gcontinue' => '3|4' ),
+               ), $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $this->assertSame( array( array(
+                       'mlcontinue' => 2,
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||',
+               ), true ), $manager->getContinuation() );
+               $this->assertSame( array(
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+                       'generator' => array( 'gcontinue' => 3 ),
+               ), $manager->getRawContinuation() );
+
+               $result = new ApiResult( 0 );
+               $manager->setContinuationIntoResult( $result );
+               $this->assertSame( array(
+                       'mlcontinue' => 2,
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( '', $result->getResultData( 'batchcomplete' ) );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $this->assertSame( array( array(
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||mocklist',
+               ), true ), $manager->getContinuation() );
+               $this->assertSame( array(
+                       'generator' => array( 'gcontinue' => 3 ),
+               ), $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $this->assertSame( array( array(
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ), false ), $manager->getContinuation() );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+               ), $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $this->assertSame( array( array(
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2|mocklist',
+               ), false ), $manager->getContinuation() );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+               ), $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $this->assertSame( array( array(
+                       'mlcontinue' => 2,
+                       'continue' => '-||mock1|mock2',
+               ), true ), $manager->getContinuation() );
+               $this->assertSame( array(
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+               ), $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $this->assertSame( array( array(), true ), $manager->getContinuation() );
+               $this->assertSame( array(), $manager->getRawContinuation() );
+
+               $manager = self::getManager( '||mock2', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame(
+                       array_values( array_diff_key( $allModules, array( 1 => 1 ) ) ),
+                       $manager->getRunModules()
+               );
+
+               $manager = self::getManager( '-||', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( true, $manager->isGeneratorDone() );
+               $this->assertSame(
+                       array_values( array_diff_key( $allModules, array( 0 => 0, 1 => 1 ) ) ),
+                       $manager->getRunModules()
+               );
+
+               try {
+                       self::getManager( 'foo', $allModules, array( 'mock1', 'mock2' ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UsageException $ex ) {
+                       $this->assertSame(
+                               'Invalid continue param. You should pass the original value returned by the previous query',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $manager = self::getManager( '||mock2', array_slice( $allModules, 0, 2 ), array( 'mock1', 'mock2' ) );
+               try {
+                       $manager->addContinueParam( $allModules[1], 'm2continue', 1 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Module \'mock2\' was not supposed to have been executed, but it was executed anyway',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $manager->addContinueParam( $allModules[2], 'mlcontinue', 1 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Module \'mocklist\' called ApiContinuationManager::addContinueParam but was not passed to ApiContinuationManager::__construct',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+       }
+
+}
diff --git a/tests/phpunit/includes/api/ApiErrorFormatterTest.php b/tests/phpunit/includes/api/ApiErrorFormatterTest.php
new file mode 100644 (file)
index 0000000..344af62
--- /dev/null
@@ -0,0 +1,343 @@
+<?php
+
+/**
+ * @group API
+ */
+class ApiErrorFormatterTest extends MediaWikiTestCase {
+
+       /**
+        * @covers ApiErrorFormatter
+        * @dataProvider provideErrorFormatter
+        */
+       public function testErrorFormatter( $format, $lang, $useDB,
+               $expect1, $expect2, $expect3
+       ) {
+               $result = new ApiResult( 8388608 );
+               $formatter = new ApiErrorFormatter( $result, Language::factory( $lang ), $format, $useDB );
+
+               $formatter->addWarning( 'string', 'mainpage' );
+               $formatter->addError( 'err', 'mainpage' );
+               $this->assertSame( $expect1, $result->getResultData(), 'Simple test' );
+
+               $result->reset();
+               $formatter->addWarning( 'foo', 'mainpage' );
+               $formatter->addWarning( 'foo', 'mainpage' );
+               $formatter->addWarning( 'foo', array( 'parentheses', 'foobar' ) );
+               $msg1 = wfMessage( 'mainpage' );
+               $formatter->addWarning( 'message', $msg1 );
+               $msg2 = new ApiMessage( 'mainpage', 'overriddenCode', array( 'overriddenData' => true ) );
+               $formatter->addWarning( 'messageWithData', $msg2 );
+               $formatter->addError( 'errWithData', $msg2 );
+               $this->assertSame( $expect2, $result->getResultData(), 'Complex test' );
+
+               $result->reset();
+               $status = Status::newGood();
+               $status->warning( 'mainpage' );
+               $status->warning( 'parentheses', 'foobar' );
+               $status->warning( $msg1 );
+               $status->warning( $msg2 );
+               $status->error( 'mainpage' );
+               $status->error( 'parentheses', 'foobar' );
+               $formatter->addMessagesFromStatus( 'status', $status );
+               $this->assertSame( $expect3, $result->getResultData(), 'Status test' );
+
+               $this->assertSame(
+                       $expect3['errors']['status'],
+                       $formatter->arrayFromStatus( $status, 'error' ),
+                       'arrayFromStatus test for error'
+               );
+               $this->assertSame(
+                       $expect3['warnings']['status'],
+                       $formatter->arrayFromStatus( $status, 'warning' ),
+                       'arrayFromStatus test for warning'
+               );
+       }
+
+       public static function provideErrorFormatter() {
+               $mainpagePlain = wfMessage( 'mainpage' )->useDatabase( false )->plain();
+               $parensPlain = wfMessage( 'parentheses', 'foobar' )->useDatabase( false )->plain();
+               $mainpageText = wfMessage( 'mainpage' )->inLanguage( 'de' )->text();
+               $parensText = wfMessage( 'parentheses', 'foobar' )->inLanguage( 'de' )->text();
+               $C = ApiResult::META_CONTENT;
+               $I = ApiResult::META_INDEXED_TAG_NAME;
+
+               return array(
+                       array( 'wikitext', 'de', true,
+                               array(
+                                       'errors' => array(
+                                               'err' => array(
+                                                       array( 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'string' => array(
+                                                       array( 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'errors' => array(
+                                               'errWithData' => array(
+                                                       array( 'code' => 'overriddenCode', 'text' => $mainpageText,
+                                                               'overriddenData' => true, $C => 'text' ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'messageWithData' => array(
+                                                       array( 'code' => 'overriddenCode', 'text' => $mainpageText,
+                                                               'overriddenData' => true, $C => 'text' ),
+                                                       $I => 'warning',
+                                               ),
+                                               'message' => array(
+                                                       array( 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ),
+                                                       $I => 'warning',
+                                               ),
+                                               'foo' => array(
+                                                       array( 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ),
+                                                       array( 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'errors' => array(
+                                               'status' => array(
+                                                       array( 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ),
+                                                       array( 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'status' => array(
+                                                       array( 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ),
+                                                       array( 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ),
+                                                       array( 'code' => 'overriddenCode', 'text' => $mainpageText,
+                                                               'overriddenData' => true, $C => 'text' ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                       ),
+                       array( 'raw', 'fr', true,
+                               array(
+                                       'errors' => array(
+                                               'err' => array(
+                                                       array( 'code' => 'mainpage', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'string' => array(
+                                                       array( 'code' => 'mainpage', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'errors' => array(
+                                               'errWithData' => array(
+                                                       array( 'code' => 'overriddenCode', 'message' => 'mainpage', 'params' => array( $I => 'param' ),
+                                                               'overriddenData' => true ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'messageWithData' => array(
+                                                       array( 'code' => 'overriddenCode', 'message' => 'mainpage', 'params' => array( $I => 'param' ),
+                                                               'overriddenData' => true ),
+                                                       $I => 'warning',
+                                               ),
+                                               'message' => array(
+                                                       array( 'code' => 'mainpage', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                                                       $I => 'warning',
+                                               ),
+                                               'foo' => array(
+                                                       array( 'code' => 'mainpage', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                                                       array( 'code' => 'parentheses', 'message' => 'parentheses', 'params' => array( 'foobar', $I => 'param' ) ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'errors' => array(
+                                               'status' => array(
+                                                       array( 'code' => 'mainpage', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                                                       array( 'code' => 'parentheses', 'message' => 'parentheses', 'params' => array( 'foobar', $I => 'param' ) ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'status' => array(
+                                                       array( 'code' => 'mainpage', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                                                       array( 'code' => 'parentheses', 'message' => 'parentheses', 'params' => array( 'foobar', $I => 'param' ) ),
+                                                       array( 'code' => 'overriddenCode', 'message' => 'mainpage', 'params' => array( $I => 'param' ),
+                                                               'overriddenData' => true ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                       ),
+                       array( 'none', 'fr', true,
+                               array(
+                                       'errors' => array(
+                                               'err' => array(
+                                                       array( 'code' => 'mainpage' ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'string' => array(
+                                                       array( 'code' => 'mainpage' ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'errors' => array(
+                                               'errWithData' => array(
+                                                       array( 'code' => 'overriddenCode', 'overriddenData' => true ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'messageWithData' => array(
+                                                       array( 'code' => 'overriddenCode', 'overriddenData' => true ),
+                                                       $I => 'warning',
+                                               ),
+                                               'message' => array(
+                                                       array( 'code' => 'mainpage' ),
+                                                       $I => 'warning',
+                                               ),
+                                               'foo' => array(
+                                                       array( 'code' => 'mainpage' ),
+                                                       array( 'code' => 'parentheses' ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'errors' => array(
+                                               'status' => array(
+                                                       array( 'code' => 'mainpage' ),
+                                                       array( 'code' => 'parentheses' ),
+                                                       $I => 'error',
+                                               ),
+                                       ),
+                                       'warnings' => array(
+                                               'status' => array(
+                                                       array( 'code' => 'mainpage' ),
+                                                       array( 'code' => 'parentheses' ),
+                                                       array( 'code' => 'overriddenCode', 'overriddenData' => true ),
+                                                       $I => 'warning',
+                                               ),
+                                       ),
+                               ),
+                       ),
+               );
+       }
+
+       /**
+        * @covers ApiErrorFormatter_BackCompat
+        */
+       public function testErrorFormatterBC() {
+               $mainpagePlain = wfMessage( 'mainpage' )->useDatabase( false )->plain();
+               $parensPlain = wfMessage( 'parentheses', 'foobar' )->useDatabase( false )->plain();
+
+               $result = new ApiResult( 8388608 );
+               $formatter = new ApiErrorFormatter_BackCompat( $result );
+
+               $formatter->addWarning( 'string', 'mainpage' );
+               $formatter->addError( 'err', 'mainpage' );
+               $this->assertSame( array(
+                       'error' => array(
+                               'code' => 'mainpage',
+                               'info' => $mainpagePlain,
+                       ),
+                       'warnings' => array(
+                               'string' => array(
+                                       'warnings' => $mainpagePlain,
+                                       ApiResult::META_CONTENT => 'warnings',
+                               ),
+                       ),
+               ), $result->getResultData(), 'Simple test' );
+
+               $result->reset();
+               $formatter->addWarning( 'foo', 'mainpage' );
+               $formatter->addWarning( 'foo', 'mainpage' );
+               $formatter->addWarning( 'foo', array( 'parentheses', 'foobar' ) );
+               $msg1 = wfMessage( 'mainpage' );
+               $formatter->addWarning( 'message', $msg1 );
+               $msg2 = new ApiMessage( 'mainpage', 'overriddenCode', array( 'overriddenData' => true ) );
+               $formatter->addWarning( 'messageWithData', $msg2 );
+               $formatter->addError( 'errWithData', $msg2 );
+               $this->assertSame( array(
+                       'error' => array(
+                               'code' => 'overriddenCode',
+                               'info' => $mainpagePlain,
+                               'overriddenData' => true,
+                       ),
+                       'warnings' => array(
+                               'messageWithData' => array(
+                                       'warnings' => $mainpagePlain,
+                                       ApiResult::META_CONTENT => 'warnings',
+                               ),
+                               'message' => array(
+                                       'warnings' => $mainpagePlain,
+                                       ApiResult::META_CONTENT => 'warnings',
+                               ),
+                               'foo' => array(
+                                       'warnings' => "$mainpagePlain\n$parensPlain",
+                                       ApiResult::META_CONTENT => 'warnings',
+                               ),
+                       ),
+               ), $result->getResultData(), 'Complex test' );
+
+               $result->reset();
+               $status = Status::newGood();
+               $status->warning( 'mainpage' );
+               $status->warning( 'parentheses', 'foobar' );
+               $status->warning( $msg1 );
+               $status->warning( $msg2 );
+               $status->error( 'mainpage' );
+               $status->error( 'parentheses', 'foobar' );
+               $formatter->addMessagesFromStatus( 'status', $status );
+               $this->assertSame( array(
+                       'error' => array(
+                               'code' => 'parentheses',
+                               'info' => $parensPlain,
+                       ),
+                       'warnings' => array(
+                               'status' => array(
+                                       'warnings' => "$mainpagePlain\n$parensPlain",
+                                       ApiResult::META_CONTENT => 'warnings',
+                               ),
+                       ),
+               ), $result->getResultData(), 'Status test' );
+
+               $I = ApiResult::META_INDEXED_TAG_NAME;
+               $this->assertSame(
+                       array(
+                               array( 'type' => 'error', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                               array( 'type' => 'error', 'message' => 'parentheses', 'params' => array( 'foobar', $I => 'param' ) ),
+                               $I => 'error',
+                       ),
+                       $formatter->arrayFromStatus( $status, 'error' ),
+                       'arrayFromStatus test for error'
+               );
+               $this->assertSame(
+                       array(
+                               array( 'type' => 'warning', 'message' => 'mainpage', 'params' => array( $I => 'param' ) ),
+                               array( 'type' => 'warning', 'message' => 'parentheses', 'params' => array( 'foobar', $I => 'param' ) ),
+                               array( 'message' => 'mainpage', 'params' => array( $I => 'param' ), 'type' => 'warning' ),
+                               array( 'message' => 'mainpage', 'params' => array( $I => 'param' ), 'type' => 'warning' ),
+                               $I => 'warning',
+                       ),
+                       $formatter->arrayFromStatus( $status, 'warning' ),
+                       'arrayFromStatus test for warning'
+               );
+       }
+
+}
index 7a03f7d..e8ef180 100644 (file)
@@ -16,7 +16,7 @@ class ApiMainTest extends ApiTestCase {
                        new FauxRequest( array( 'action' => 'query', 'meta' => 'siteinfo' ) )
                );
                $api->execute();
-               $data = $api->getResultData();
+               $data = $api->getResult()->getResultData();
                $this->assertInternalType( 'array', $data );
                $this->assertArrayHasKey( 'query', $data );
        }
diff --git a/tests/phpunit/includes/api/ApiMessageTest.php b/tests/phpunit/includes/api/ApiMessageTest.php
new file mode 100644 (file)
index 0000000..6c3ce60
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @group API
+ */
+class ApiMessageTest extends MediaWikiTestCase {
+
+       private function compareMessages( $msg, $msg2 ) {
+               $this->assertSame( $msg->getKey(), $msg2->getKey(), 'getKey' );
+               $this->assertSame( $msg->getKeysToTry(), $msg2->getKeysToTry(), 'getKeysToTry' );
+               $this->assertSame( $msg->getParams(), $msg2->getParams(), 'getParams' );
+               $this->assertSame( $msg->getFormat(), $msg2->getFormat(), 'getFormat' );
+               $this->assertSame( $msg->getLanguage(), $msg2->getLanguage(), 'getLanguage' );
+
+               $msg = TestingAccessWrapper::newFromObject( $msg );
+               $msg2 = TestingAccessWrapper::newFromObject( $msg2 );
+               foreach ( array( 'interface', 'useDatabase', 'title' ) as $key ) {
+                       $this->assertSame( $msg->$key, $msg2->$key, $key );
+               }
+       }
+
+       /**
+        * @covers ApiMessage
+        */
+       public function testApiMessage() {
+               $msg = new Message( array( 'foo', 'bar' ), array( 'baz' ) );
+               $msg->inLanguage( 'de' )->title( Title::newMainPage() );
+               $msg2 = new ApiMessage( $msg, 'code', array( 'data' ) );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+
+               $msg = new Message( array( 'foo', 'bar' ), array( 'baz' ) );
+               $msg2 = new ApiMessage( array( array( 'foo', 'bar' ), 'baz' ), 'code', array( 'data' ) );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+
+               $msg = new Message( 'foo' );
+               $msg2 = new ApiMessage( 'foo' );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'foo', $msg2->getApiCode() );
+               $this->assertEquals( array(), $msg2->getApiData() );
+
+               $msg2->setApiCode( 'code', array( 'data' ) );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+               $msg2->setApiCode( null );
+               $this->assertEquals( 'foo', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+               $msg2->setApiData( array( 'data2' ) );
+               $this->assertEquals( array( 'data2' ), $msg2->getApiData() );
+       }
+
+       /**
+        * @covers ApiRawMessage
+        */
+       public function testApiRawMessage() {
+               $msg = new RawMessage( 'foo', array( 'baz' ) );
+               $msg->inLanguage( 'de' )->title( Title::newMainPage() );
+               $msg2 = new ApiRawMessage( $msg, 'code', array( 'data' ) );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+
+               $msg = new RawMessage( 'foo', array( 'baz' ) );
+               $msg2 = new ApiRawMessage( array( 'foo', 'baz' ), 'code', array( 'data' ) );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+
+               $msg = new RawMessage( 'foo' );
+               $msg2 = new ApiRawMessage( 'foo', 'code', array( 'data' ) );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+
+               $msg2->setApiCode( 'code', array( 'data' ) );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+               $msg2->setApiCode( null );
+               $this->assertEquals( 'foo', $msg2->getApiCode() );
+               $this->assertEquals( array( 'data' ), $msg2->getApiData() );
+               $msg2->setApiData( array( 'data2' ) );
+               $this->assertEquals( array( 'data2' ), $msg2->getApiData() );
+       }
+
+       /**
+        * @covers ApiMessage::create
+        */
+       public function testApiMessageCreate() {
+               $this->assertInstanceOf( 'ApiMessage', ApiMessage::create( new Message( 'mainpage' ) ) );
+               $this->assertInstanceOf( 'ApiRawMessage', ApiMessage::create( new RawMessage( 'mainpage' ) ) );
+               $this->assertInstanceOf( 'ApiMessage', ApiMessage::create( 'mainpage' ) );
+
+               $msg = new ApiMessage( 'mainpage' );
+               $this->assertSame( $msg, ApiMessage::create( $msg ) );
+
+               $msg = new ApiRawMessage( 'mainpage' );
+               $this->assertSame( $msg, ApiMessage::create( $msg ) );
+       }
+
+}
index bd34018..51154ae 100644 (file)
@@ -138,7 +138,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) );
                $this->mTested->execute();
 
-               return $this->mTested->getResult()->getData();
+               return $this->mTested->getResult()->getResultData( null, array( 'Strip' => 'all' ) );
        }
 
        /**
@@ -396,7 +396,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                        'options' => 'success',
                        'warnings' => array(
                                'options' => array(
-                                       '*' => "Validation error for 'special': cannot be set by this module"
+                                       'warnings' => "Validation error for 'special': cannot be set by this module"
                                )
                        )
                ), $response );
@@ -419,7 +419,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                        'options' => 'success',
                        'warnings' => array(
                                'options' => array(
-                                       '*' => "Validation error for 'unknownOption': not a valid preference"
+                                       'warnings' => "Validation error for 'unknownOption': not a valid preference"
                                )
                        )
                ), $response );
diff --git a/tests/phpunit/includes/api/ApiResultTest.php b/tests/phpunit/includes/api/ApiResultTest.php
new file mode 100644 (file)
index 0000000..7e43a24
--- /dev/null
@@ -0,0 +1,1446 @@
+<?php
+
+/**
+ * @covers ApiResult
+ * @group API
+ */
+class ApiResultTest extends MediaWikiTestCase {
+
+       /**
+        * @covers ApiResult
+        */
+       public function testStaticDataMethods() {
+               $arr = array();
+
+               ApiResult::setValue( $arr, 'setValue', '1' );
+
+               ApiResult::setValue( $arr, null, 'unnamed 1' );
+               ApiResult::setValue( $arr, null, 'unnamed 2' );
+
+               ApiResult::setValue( $arr, 'deleteValue', '2' );
+               ApiResult::unsetValue( $arr, 'deleteValue' );
+
+               ApiResult::setContentValue( $arr, 'setContentValue', '3' );
+
+               $this->assertSame( array(
+                       'setValue' => '1',
+                       'unnamed 1',
+                       'unnamed 2',
+                       ApiResult::META_CONTENT => 'setContentValue',
+                       'setContentValue' => '3',
+               ), $arr );
+
+               try {
+                       ApiResult::setValue( $arr, 'setValue', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to add element setValue=99, existing value is 1',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               try {
+                       ApiResult::setContentValue( $arr, 'setContentValue2', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to set content element as setContentValue2 when setContentValue ' .
+                                       'is already set as the content element',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               ApiResult::setValue( $arr, 'setValue', '99', ApiResult::OVERRIDE );
+               $this->assertSame( '99', $arr['setValue'] );
+
+               ApiResult::setContentValue( $arr, 'setContentValue2', '99', ApiResult::OVERRIDE );
+               $this->assertSame( 'setContentValue2', $arr[ApiResult::META_CONTENT] );
+
+               $arr = array( 'foo' => 1, 'bar' => 1 );
+               ApiResult::setValue( $arr, 'top', '2', ApiResult::ADD_ON_TOP );
+               ApiResult::setValue( $arr, null, '2', ApiResult::ADD_ON_TOP );
+               ApiResult::setValue( $arr, 'bottom', '2' );
+               ApiResult::setValue( $arr, 'foo', '2', ApiResult::OVERRIDE );
+               ApiResult::setValue( $arr, 'bar', '2', ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
+               $this->assertSame( array( 0, 'top', 'foo', 'bar', 'bottom' ), array_keys( $arr ) );
+
+               $arr = array();
+               ApiResult::setValue( $arr, 'sub', array( 'foo' => 1 ) );
+               ApiResult::setValue( $arr, 'sub', array( 'bar' => 1 ) );
+               $this->assertSame( array( 'sub' => array( 'foo' => 1, 'bar' => 1 ) ), $arr );
+
+               try {
+                       ApiResult::setValue( $arr, 'sub', array( 'foo' => 2, 'baz' => 2 ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Conflicting keys (foo) when attempting to merge element sub',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $arr = array();
+               $title = Title::newFromText( "MediaWiki:Foobar" );
+               $obj = new stdClass;
+               $obj->foo = 1;
+               $obj->bar = 2;
+               ApiResult::setValue( $arr, 'title', $title );
+               ApiResult::setValue( $arr, 'obj', $obj );
+               $this->assertSame( array(
+                       'title' => (string)$title,
+                       'obj' => array( 'foo' => 1, 'bar' => 2, ApiResult::META_TYPE => 'assoc' ),
+               ), $arr );
+
+               $fh = tmpfile();
+               try {
+                       ApiResult::setValue( $arr, 'file', $fh );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $obj->file = $fh;
+                       ApiResult::setValue( $arr, 'sub', $obj );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               fclose( $fh );
+
+               try {
+                       ApiResult::setValue( $arr, 'inf', INF );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       ApiResult::setValue( $arr, 'nan', NAN );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $arr = array();
+               $result2 = new ApiResult( 8388608 );
+               $result2->addValue( null, 'foo', 'bar' );
+               ApiResult::setValue( $arr, 'baz', $result2 );
+               $this->assertSame( array( 'baz' => array( 'foo' => 'bar' ) ), $arr );
+
+               $arr = array();
+               ApiResult::setValue( $arr, 'foo', "foo\x80bar" );
+               ApiResult::setValue( $arr, 'bar', "a\xcc\x81" );
+               ApiResult::setValue( $arr, 'baz', 74 );
+               $this->assertSame( array(
+                       'foo' => "foo\xef\xbf\xbdbar",
+                       'bar' => "\xc3\xa1",
+                       'baz' => 74,
+               ), $arr );
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testInstanceDataMethods() {
+               $result = new ApiResult( 8388608 );
+
+               $result->addValue( null, 'setValue', '1' );
+
+               $result->addValue( null, null, 'unnamed 1' );
+               $result->addValue( null, null, 'unnamed 2' );
+
+               $result->addValue( null, 'deleteValue', '2' );
+               $result->removeValue( null, 'deleteValue' );
+
+               $result->addValue( array( 'a', 'b' ), 'deleteValue', '3' );
+               $result->removeValue( array( 'a', 'b', 'deleteValue' ), null, '3' );
+
+               $result->addContentValue( null, 'setContentValue', '3' );
+
+               $this->assertSame( array(
+                       'setValue' => '1',
+                       'unnamed 1',
+                       'unnamed 2',
+                       'a' => array( 'b' => array() ),
+                       'setContentValue' => '3',
+                       ApiResult::META_CONTENT => 'setContentValue',
+               ), $result->getResultData() );
+               $this->assertSame( 20, $result->getSize() );
+
+               try {
+                       $result->addValue( null, 'setValue', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to add element setValue=99, existing value is 1',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               try {
+                       $result->addContentValue( null, 'setContentValue2', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to set content element as setContentValue2 when setContentValue ' .
+                                       'is already set as the content element',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result->addValue( null, 'setValue', '99', ApiResult::OVERRIDE );
+               $this->assertSame( '99', $result->getResultData( array( 'setValue' ) ) );
+
+               $result->addContentValue( null, 'setContentValue2', '99', ApiResult::OVERRIDE );
+               $this->assertSame( 'setContentValue2',
+                       $result->getResultData( array( ApiResult::META_CONTENT ) ) );
+
+               $result->reset();
+               $this->assertSame( array(), $result->getResultData() );
+               $this->assertSame( 0, $result->getSize() );
+
+               $result->addValue( null, 'foo', 1 );
+               $result->addValue( null, 'bar', 1 );
+               $result->addValue( null, 'top', '2', ApiResult::ADD_ON_TOP );
+               $result->addValue( null, null, '2', ApiResult::ADD_ON_TOP );
+               $result->addValue( null, 'bottom', '2' );
+               $result->addValue( null, 'foo', '2', ApiResult::OVERRIDE );
+               $result->addValue( null, 'bar', '2', ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
+               $this->assertSame( array( 0, 'top', 'foo', 'bar', 'bottom' ),
+                       array_keys( $result->getResultData() ) );
+
+               $result->reset();
+               $result->addValue( null, 'foo', array( 'bar' => 1 ) );
+               $result->addValue( array( 'foo', 'top' ), 'x', 2, ApiResult::ADD_ON_TOP );
+               $result->addValue( array( 'foo', 'bottom' ), 'x', 2 );
+               $this->assertSame( array( 'top', 'bar', 'bottom' ),
+                       array_keys( $result->getResultData( array( 'foo' ) ) ) );
+
+               $result->reset();
+               $result->addValue( null, 'sub', array( 'foo' => 1 ) );
+               $result->addValue( null, 'sub', array( 'bar' => 1 ) );
+               $this->assertSame( array( 'sub' => array( 'foo' => 1, 'bar' => 1 ) ),
+                       $result->getResultData() );
+
+               try {
+                       $result->addValue( null, 'sub', array( 'foo' => 2, 'baz' => 2 ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Conflicting keys (foo) when attempting to merge element sub',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result->reset();
+               $title = Title::newFromText( "MediaWiki:Foobar" );
+               $obj = new stdClass;
+               $obj->foo = 1;
+               $obj->bar = 2;
+               $result->addValue( null, 'title', $title );
+               $result->addValue( null, 'obj', $obj );
+               $this->assertSame( array(
+                       'title' => (string)$title,
+                       'obj' => array( 'foo' => 1, 'bar' => 2, ApiResult::META_TYPE => 'assoc' ),
+               ), $result->getResultData() );
+
+               $fh = tmpfile();
+               try {
+                       $result->addValue( null, 'file', $fh );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $obj->file = $fh;
+                       $result->addValue( null, 'sub', $obj );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               fclose( $fh );
+
+               try {
+                       $result->addValue( null, 'inf', INF );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $result->addValue( null, 'nan', NAN );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result->reset();
+               $result->addParsedLimit( 'foo', 12 );
+               $this->assertSame( array( 'limits' => array( 'foo' => 12 ) ), $result->getResultData() );
+               $result->addParsedLimit( 'foo', 13 );
+               $this->assertSame( array( 'limits' => array( 'foo' => 13 ) ), $result->getResultData() );
+               $this->assertSame( null, $result->getResultData( array( 'foo', 'bar', 'baz' ) ) );
+               $this->assertSame( 13, $result->getResultData( array( 'limits', 'foo' ) ) );
+               try {
+                       $result->getResultData( array( 'limits', 'foo', 'bar' ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Path limits.foo is not an array',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result = new ApiResult( 10 );
+               $formatter = new ApiErrorFormatter( $result, Language::factory( 'en' ), 'none', false );
+               $result->setErrorFormatter( $formatter );
+               $this->assertFalse( $result->addValue( null, 'foo', '12345678901' ) );
+               $this->assertTrue( $result->addValue( null, 'foo', '12345678901', ApiResult::NO_SIZE_CHECK ) );
+               $this->assertSame( 0, $result->getSize() );
+               $result->reset();
+               $this->assertTrue( $result->addValue( null, 'foo', '1234567890' ) );
+               $this->assertFalse( $result->addValue( null, 'foo', '1' ) );
+               $result->removeValue( null, 'foo' );
+               $this->assertTrue( $result->addValue( null, 'foo', '1' ) );
+
+               $result = new ApiResult( 8388608 );
+               $result2 = new ApiResult( 8388608 );
+               $result2->addValue( null, 'foo', 'bar' );
+               $result->addValue( null, 'baz', $result2 );
+               $this->assertSame( array( 'baz' => array( 'foo' => 'bar' ) ), $result->getResultData() );
+
+               $result = new ApiResult( 8388608 );
+               $result->addValue( null, 'foo', "foo\x80bar" );
+               $result->addValue( null, 'bar', "a\xcc\x81" );
+               $result->addValue( null, 'baz', 74 );
+               $this->assertSame( array(
+                       'foo' => "foo\xef\xbf\xbdbar",
+                       'bar' => "\xc3\xa1",
+                       'baz' => 74,
+               ), $result->getResultData() );
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testMetadata() {
+               $arr = array( 'foo' => array( 'bar' => array() ) );
+               $result = new ApiResult( 8388608 );
+               $result->addValue( null, 'foo', array( 'bar' => array() ) );
+
+               $expect = array(
+                       'foo' => array(
+                               'bar' => array(
+                                       ApiResult::META_INDEXED_TAG_NAME => 'ritn',
+                                       ApiResult::META_TYPE => 'default',
+                               ),
+                               ApiResult::META_INDEXED_TAG_NAME => 'ritn',
+                               ApiResult::META_TYPE => 'default',
+                       ),
+                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => array( 'foo', 'bar' ),
+                       ApiResult::META_TYPE => 'array',
+               );
+
+               ApiResult::setSubelementsList( $arr, 'foo' );
+               ApiResult::setSubelementsList( $arr, array( 'bar', 'baz' ) );
+               ApiResult::unsetSubelementsList( $arr, 'baz' );
+               ApiResult::setIndexedTagNameRecursive( $arr, 'ritn' );
+               ApiResult::setIndexedTagName( $arr, 'itn' );
+               ApiResult::setPreserveKeysList( $arr, 'foo' );
+               ApiResult::setPreserveKeysList( $arr, array( 'bar', 'baz' ) );
+               ApiResult::unsetPreserveKeysList( $arr, 'baz' );
+               ApiResult::setArrayTypeRecursive( $arr, 'default' );
+               ApiResult::setArrayType( $arr, 'array' );
+               $this->assertSame( $expect, $arr );
+
+               $result->addSubelementsList( null, 'foo' );
+               $result->addSubelementsList( null, array( 'bar', 'baz' ) );
+               $result->removeSubelementsList( null, 'baz' );
+               $result->addIndexedTagNameRecursive( null, 'ritn' );
+               $result->addIndexedTagName( null, 'itn' );
+               $result->addPreserveKeysList( null, 'foo' );
+               $result->addPreserveKeysList( null, array( 'bar', 'baz' ) );
+               $result->removePreserveKeysList( null, 'baz' );
+               $result->addArrayTypeRecursive( null, 'default' );
+               $result->addArrayType( null, 'array' );
+               $this->assertSame( $expect, $result->getResultData() );
+
+               $arr = array( 'foo' => array( 'bar' => array() ) );
+               $expect = array(
+                       'foo' => array(
+                               'bar' => array(
+                                       ApiResult::META_TYPE => 'kvp',
+                                       ApiResult::META_KVP_KEY_NAME => 'key',
+                               ),
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                       ),
+                       ApiResult::META_TYPE => 'BCkvp',
+                       ApiResult::META_KVP_KEY_NAME => 'bc',
+               );
+               ApiResult::setArrayTypeRecursive( $arr, 'kvp', 'key' );
+               ApiResult::setArrayType( $arr, 'BCkvp', 'bc' );
+               $this->assertSame( $expect, $arr );
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testUtilityFunctions() {
+               $arr = array(
+                       'foo' => array(
+                               'bar' => array( '_dummy' => 'foobaz' ),
+                               'bar2' => (object)array( '_dummy' => 'foobaz' ),
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ),
+                       'foo2' => (object)array(
+                               'bar' => array( '_dummy' => 'foobaz' ),
+                               'bar2' => (object)array( '_dummy' => 'foobaz' ),
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ),
+                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => array( 'foo', 'bar', '_dummy2', 0 ),
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+                       '_dummy2' => 'foobaz!',
+               );
+               $this->assertEquals( array(
+                       'foo' => array(
+                               'bar' => array(),
+                               'bar2' => (object)array(),
+                               'x' => 'ok',
+                       ),
+                       'foo2' => (object)array(
+                               'bar' => array(),
+                               'bar2' => (object)array(),
+                               'x' => 'ok',
+                       ),
+                       '_dummy2' => 'foobaz!',
+               ), ApiResult::stripMetadata( $arr ), 'ApiResult::stripMetadata' );
+
+               $metadata = array();
+               $data = ApiResult::stripMetadataNonRecursive( $arr, $metadata );
+               $this->assertEquals( array(
+                       'foo' => array(
+                               'bar' => array( '_dummy' => 'foobaz' ),
+                               'bar2' => (object)array( '_dummy' => 'foobaz' ),
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ),
+                       'foo2' => (object)array(
+                               'bar' => array( '_dummy' => 'foobaz' ),
+                               'bar2' => (object)array( '_dummy' => 'foobaz' ),
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ),
+                       '_dummy2' => 'foobaz!',
+               ), $data, 'ApiResult::stripMetadataNonRecursive ($data)' );
+               $this->assertEquals( array(
+                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => array( 'foo', 'bar', '_dummy2', 0 ),
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+               ), $metadata, 'ApiResult::stripMetadataNonRecursive ($metadata)' );
+
+               $metadata = null;
+               $data = ApiResult::stripMetadataNonRecursive( (object)$arr, $metadata );
+               $this->assertEquals( (object)array(
+                       'foo' => array(
+                               'bar' => array( '_dummy' => 'foobaz' ),
+                               'bar2' => (object)array( '_dummy' => 'foobaz' ),
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ),
+                       'foo2' => (object)array(
+                               'bar' => array( '_dummy' => 'foobaz' ),
+                               'bar2' => (object)array( '_dummy' => 'foobaz' ),
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ),
+                       '_dummy2' => 'foobaz!',
+               ), $data, 'ApiResult::stripMetadataNonRecursive on object ($data)' );
+               $this->assertEquals( array(
+                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => array( 'foo', 'bar', '_dummy2', 0 ),
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+               ), $metadata, 'ApiResult::stripMetadataNonRecursive on object ($metadata)' );
+       }
+
+       /**
+        * @covers ApiResult
+        * @dataProvider provideTransformations
+        * @param string $label
+        * @param array $input
+        * @param array $transforms
+        * @param array|Exception $expect
+        */
+       public function testTransformations( $label, $input, $transforms, $expect ) {
+               $result = new ApiResult( false );
+               $result->addValue( null, 'test', $input );
+
+               if ( $expect instanceof Exception ) {
+                       try {
+                               $output = $result->getResultData( 'test', $transforms );
+                               $this->fail( 'Expected exception not thrown', $label );
+                       } catch ( Exception $ex ) {
+                               $this->assertEquals( $ex, $expect, $label );
+                       }
+               } else {
+                       $output = $result->getResultData( 'test', $transforms );
+                       $this->assertEquals( $expect, $output, $label );
+               }
+       }
+
+       public function provideTransformations() {
+               $kvp = function ( $keyKey, $key, $valKey, $value ) {
+                       return array(
+                               $keyKey => $key,
+                               $valKey => $value,
+                               ApiResult::META_PRESERVE_KEYS => array( $keyKey ),
+                               ApiResult::META_CONTENT => $valKey,
+                               ApiResult::META_TYPE => 'assoc',
+                       );
+               };
+               $typeArr = array(
+                       'defaultArray' => array( 2 => 'a', 0 => 'b', 1 => 'c' ),
+                       'defaultAssoc' => array( 'x' => 'a', 1 => 'b', 0 => 'c' ),
+                       'defaultAssoc2' => array( 2 => 'a', 3 => 'b', 0 => 'c' ),
+                       'array' => array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'array' ),
+                       'BCarray' => array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'BCarray' ),
+                       'BCassoc' => array( 'a', 'b', 'c', ApiResult::META_TYPE => 'BCassoc' ),
+                       'assoc' => array( 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                       'kvp' => array( 'x' => 'a', 'y' => 'b', 'z' => array( 'c' ), ApiResult::META_TYPE => 'kvp' ),
+                       'BCkvp' => array( 'x' => 'a', 'y' => 'b',
+                               ApiResult::META_TYPE => 'BCkvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                       ),
+                       'emptyDefault' => array( '_dummy' => 1 ),
+                       'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ),
+                       '_dummy' => 1,
+                       ApiResult::META_PRESERVE_KEYS => array( '_dummy' ),
+               );
+               $stripArr = array(
+                       'foo' => array(
+                               'bar' => array( '_dummy' => 'foobaz' ),
+                               'baz' => array(
+                                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                                       ApiResult::META_PRESERVE_KEYS => array( 'foo', 'bar', '_dummy2', 0 ),
+                                       ApiResult::META_TYPE => 'array',
+                               ),
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ),
+                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => array( 'foo', 'bar', '_dummy2', 0 ),
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+                       '_dummy2' => 'foobaz!',
+               );
+
+               return array(
+                       array(
+                               'BC: META_BC_BOOLS',
+                               array(
+                                       'BCtrue' => true,
+                                       'BCfalse' => false,
+                                       'true' => true,
+                                       'false' => false,
+                                       ApiResult::META_BC_BOOLS => array( 0, 'true', 'false' ),
+                               ),
+                               array( 'BC' => array() ),
+                               array(
+                                       'BCtrue' => '',
+                                       'true' => true,
+                                       'false' => false,
+                                       ApiResult::META_BC_BOOLS => array( 0, 'true', 'false' ),
+                               )
+                       ),
+                       array(
+                               'BC: META_BC_SUBELEMENTS',
+                               array(
+                                       'bc' => 'foo',
+                                       'nobc' => 'bar',
+                                       ApiResult::META_BC_SUBELEMENTS => array( 'bc' ),
+                               ),
+                               array( 'BC' => array() ),
+                               array(
+                                       'bc' => array(
+                                               '*' => 'foo',
+                                               ApiResult::META_CONTENT => '*',
+                                               ApiResult::META_TYPE => 'assoc',
+                                       ),
+                                       'nobc' => 'bar',
+                                       ApiResult::META_BC_SUBELEMENTS => array( 'bc' ),
+                               ),
+                       ),
+                       array(
+                               'BC: META_CONTENT',
+                               array(
+                                       'content' => '!!!',
+                                       ApiResult::META_CONTENT => 'content',
+                               ),
+                               array( 'BC' => array() ),
+                               array(
+                                       '*' => '!!!',
+                                       ApiResult::META_CONTENT => '*',
+                               ),
+                       ),
+                       array(
+                               'BC: BCkvp type',
+                               array(
+                                       'foo' => 'foo value',
+                                       'bar' => 'bar value',
+                                       '_baz' => 'baz value',
+                                       ApiResult::META_TYPE => 'BCkvp',
+                                       ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ApiResult::META_PRESERVE_KEYS => array( '_baz' ),
+                               ),
+                               array( 'BC' => array() ),
+                               array(
+                                       $kvp( 'key', 'foo', '*', 'foo value' ),
+                                       $kvp( 'key', 'bar', '*', 'bar value' ),
+                                       $kvp( 'key', '_baz', '*', 'baz value' ),
+                                       ApiResult::META_TYPE => 'array',
+                                       ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ApiResult::META_PRESERVE_KEYS => array( '_baz' ),
+                               ),
+                       ),
+                       array(
+                               'BC: BCarray type',
+                               array(
+                                       ApiResult::META_TYPE => 'BCarray',
+                               ),
+                               array( 'BC' => array() ),
+                               array(
+                                       ApiResult::META_TYPE => 'default',
+                               ),
+                       ),
+                       array(
+                               'BC: BCassoc type',
+                               array(
+                                       ApiResult::META_TYPE => 'BCassoc',
+                               ),
+                               array( 'BC' => array() ),
+                               array(
+                                       ApiResult::META_TYPE => 'default',
+                               ),
+                       ),
+                       array(
+                               'BC: BCkvp exception',
+                               array(
+                                       ApiResult::META_TYPE => 'BCkvp',
+                               ),
+                               array( 'BC' => array() ),
+                               new UnexpectedValueException(
+                                       'Type "BCkvp" used without setting ApiResult::META_KVP_KEY_NAME metadata item'
+                               ),
+                       ),
+                       array(
+                               'BC: nobool, no*, nosub',
+                               array(
+                                       'true' => true,
+                                       'false' => false,
+                                       'content' => 'content',
+                                       ApiResult::META_CONTENT => 'content',
+                                       'bc' => 'foo',
+                                       ApiResult::META_BC_SUBELEMENTS => array( 'bc' ),
+                                       'BCarray' => array( ApiResult::META_TYPE => 'BCarray' ),
+                                       'BCassoc' => array( ApiResult::META_TYPE => 'BCassoc' ),
+                                       'BCkvp' => array(
+                                               'foo' => 'foo value',
+                                               'bar' => 'bar value',
+                                               '_baz' => 'baz value',
+                                               ApiResult::META_TYPE => 'BCkvp',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                               ApiResult::META_PRESERVE_KEYS => array( '_baz' ),
+                                       ),
+                               ),
+                               array( 'BC' => array( 'nobool', 'no*', 'nosub' ) ),
+                               array(
+                                       'true' => true,
+                                       'false' => false,
+                                       'content' => 'content',
+                                       'bc' => 'foo',
+                                       'BCarray' => array( ApiResult::META_TYPE => 'default' ),
+                                       'BCassoc' => array( ApiResult::META_TYPE => 'default' ),
+                                       'BCkvp' => array(
+                                               $kvp( 'key', 'foo', '*', 'foo value' ),
+                                               $kvp( 'key', 'bar', '*', 'bar value' ),
+                                               $kvp( 'key', '_baz', '*', 'baz value' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                               ApiResult::META_PRESERVE_KEYS => array( '_baz' ),
+                                       ),
+                                       ApiResult::META_CONTENT => 'content',
+                                       ApiResult::META_BC_SUBELEMENTS => array( 'bc' ),
+                               ),
+                       ),
+
+                       array(
+                               'Types: Normal transform',
+                               $typeArr,
+                               array( 'Types' => array() ),
+                               array(
+                                       'defaultArray' => array( 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ),
+                                       'defaultAssoc' => array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'defaultAssoc2' => array( 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'array' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCarray' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCassoc' => array( 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'assoc' => array( 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'kvp' => array( 'x' => 'a', 'y' => 'b',
+                                               'z' => array( 'c', ApiResult::META_TYPE => 'array' ),
+                                               ApiResult::META_TYPE => 'assoc'
+                                       ),
+                                       'BCkvp' => array( 'x' => 'a', 'y' => 'b',
+                                               ApiResult::META_TYPE => 'assoc',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ),
+                                       'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ),
+                                       'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ),
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => array( '_dummy' ),
+                                       ApiResult::META_TYPE => 'assoc',
+                               ),
+                       ),
+                       array(
+                               'Types: AssocAsObject',
+                               $typeArr,
+                               array( 'Types' => array( 'AssocAsObject' => true ) ),
+                               (object)array(
+                                       'defaultArray' => array( 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ),
+                                       'defaultAssoc' => (object)array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'defaultAssoc2' => (object)array( 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'array' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCarray' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCassoc' => (object)array( 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'assoc' => (object)array( 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'kvp' => (object)array( 'x' => 'a', 'y' => 'b',
+                                               'z' => array( 'c', ApiResult::META_TYPE => 'array' ),
+                                               ApiResult::META_TYPE => 'assoc'
+                                       ),
+                                       'BCkvp' => (object)array( 'x' => 'a', 'y' => 'b',
+                                               ApiResult::META_TYPE => 'assoc',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ),
+                                       'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ),
+                                       'emptyAssoc' => (object)array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ),
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => array( '_dummy' ),
+                                       ApiResult::META_TYPE => 'assoc',
+                               ),
+                       ),
+                       array(
+                               'Types: ArmorKVP',
+                               $typeArr,
+                               array( 'Types' => array( 'ArmorKVP' => 'name' ) ),
+                               array(
+                                       'defaultArray' => array( 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ),
+                                       'defaultAssoc' => array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'defaultAssoc2' => array( 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'array' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCarray' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCassoc' => array( 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'assoc' => array( 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'kvp' => array(
+                                               $kvp( 'name', 'x', 'value', 'a' ),
+                                               $kvp( 'name', 'y', 'value', 'b' ),
+                                               $kvp( 'name', 'z', 'value', array( 'c', ApiResult::META_TYPE => 'array' ) ),
+                                               ApiResult::META_TYPE => 'array'
+                                       ),
+                                       'BCkvp' => array(
+                                               $kvp( 'key', 'x', 'value', 'a' ),
+                                               $kvp( 'key', 'y', 'value', 'b' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ),
+                                       'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ),
+                                       'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ),
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => array( '_dummy' ),
+                                       ApiResult::META_TYPE => 'assoc',
+                               ),
+                       ),
+                       array(
+                               'Types: ArmorKVP + BC',
+                               $typeArr,
+                               array( 'BC' => array(), 'Types' => array( 'ArmorKVP' => 'name' ) ),
+                               array(
+                                       'defaultArray' => array( 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ),
+                                       'defaultAssoc' => array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'defaultAssoc2' => array( 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'array' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCarray' => array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'BCassoc' => array( 'a', 'b', 'c', ApiResult::META_TYPE => 'array' ),
+                                       'assoc' => array( 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'kvp' => array(
+                                               $kvp( 'name', 'x', '*', 'a' ),
+                                               $kvp( 'name', 'y', '*', 'b' ),
+                                               $kvp( 'name', 'z', '*', array( 'c', ApiResult::META_TYPE => 'array' ) ),
+                                               ApiResult::META_TYPE => 'array'
+                                       ),
+                                       'BCkvp' => array(
+                                               $kvp( 'key', 'x', '*', 'a' ),
+                                               $kvp( 'key', 'y', '*', 'b' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ),
+                                       'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ),
+                                       'emptyAssoc' => array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ),
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => array( '_dummy' ),
+                                       ApiResult::META_TYPE => 'assoc',
+                               ),
+                       ),
+                       array(
+                               'Types: ArmorKVP + AssocAsObject',
+                               $typeArr,
+                               array( 'Types' => array( 'ArmorKVP' => 'name', 'AssocAsObject' => true ) ),
+                               (object)array(
+                                       'defaultArray' => array( 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ),
+                                       'defaultAssoc' => (object)array( 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'defaultAssoc2' => (object)array( 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'array' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCarray' => array( 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ),
+                                       'BCassoc' => (object)array( 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'assoc' => (object)array( 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ),
+                                       'kvp' => array(
+                                               (object)$kvp( 'name', 'x', 'value', 'a' ),
+                                               (object)$kvp( 'name', 'y', 'value', 'b' ),
+                                               (object)$kvp( 'name', 'z', 'value', array( 'c', ApiResult::META_TYPE => 'array' ) ),
+                                               ApiResult::META_TYPE => 'array'
+                                       ),
+                                       'BCkvp' => array(
+                                               (object)$kvp( 'key', 'x', 'value', 'a' ),
+                                               (object)$kvp( 'key', 'y', 'value', 'b' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ),
+                                       'emptyDefault' => array( '_dummy' => 1, ApiResult::META_TYPE => 'array' ),
+                                       'emptyAssoc' => (object)array( '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ),
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => array( '_dummy' ),
+                                       ApiResult::META_TYPE => 'assoc',
+                               ),
+                       ),
+                       array(
+                               'Types: BCkvp exception',
+                               array(
+                                       ApiResult::META_TYPE => 'BCkvp',
+                               ),
+                               array( 'Types' => array() ),
+                               new UnexpectedValueException(
+                                       'Type "BCkvp" used without setting ApiResult::META_KVP_KEY_NAME metadata item'
+                               ),
+                       ),
+
+                       array(
+                               'Strip: With ArmorKVP + AssocAsObject transforms',
+                               $typeArr,
+                               array( 'Types' => array( 'ArmorKVP' => 'name', 'AssocAsObject' => true ), 'Strip' => 'all' ),
+                               (object)array(
+                                       'defaultArray' => array( 'b', 'c', 'a' ),
+                                       'defaultAssoc' => (object)array( 'x' => 'a', 1 => 'b', 0 => 'c' ),
+                                       'defaultAssoc2' => (object)array( 2 => 'a', 3 => 'b', 0 => 'c' ),
+                                       'array' => array( 'a', 'c', 'b' ),
+                                       'BCarray' => array( 'a', 'c', 'b' ),
+                                       'BCassoc' => (object)array( 'a', 'b', 'c' ),
+                                       'assoc' => (object)array( 2 => 'a', 0 => 'b', 1 => 'c' ),
+                                       'kvp' => array(
+                                               (object)array( 'name' => 'x', 'value' => 'a' ),
+                                               (object)array( 'name' => 'y', 'value' => 'b' ),
+                                               (object)array( 'name' => 'z', 'value' => array( 'c' ) ),
+                                       ),
+                                       'BCkvp' => array(
+                                               (object)array( 'key' => 'x', 'value' => 'a' ),
+                                               (object)array( 'key' => 'y', 'value' => 'b' ),
+                                       ),
+                                       'emptyDefault' => array(),
+                                       'emptyAssoc' => (object)array(),
+                                       '_dummy' => 1,
+                               ),
+                       ),
+
+                       array(
+                               'Strip: all',
+                               $stripArr,
+                               array( 'Strip' => 'all' ),
+                               array(
+                                       'foo' => array(
+                                               'bar' => array(),
+                                               'baz' => array(),
+                                               'x' => 'ok',
+                                       ),
+                                       '_dummy2' => 'foobaz!',
+                               ),
+                       ),
+                       array(
+                               'Strip: base',
+                               $stripArr,
+                               array( 'Strip' => 'base' ),
+                               array(
+                                       'foo' => array(
+                                               'bar' => array( '_dummy' => 'foobaz' ),
+                                               'baz' => array(
+                                                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                                                       ApiResult::META_PRESERVE_KEYS => array( 'foo', 'bar', '_dummy2', 0 ),
+                                                       ApiResult::META_TYPE => 'array',
+                                               ),
+                                               'x' => 'ok',
+                                               '_dummy' => 'foobaz',
+                                       ),
+                                       '_dummy2' => 'foobaz!',
+                               ),
+                       ),
+                       array(
+                               'Strip: bc',
+                               $stripArr,
+                               array( 'Strip' => 'bc' ),
+                               array(
+                                       'foo' => array(
+                                               'bar' => array(),
+                                               'baz' => array(
+                                                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                                               ),
+                                               'x' => 'ok',
+                                       ),
+                                       '_dummy2' => 'foobaz!',
+                                       ApiResult::META_SUBELEMENTS => array( 'foo', 'bar' ),
+                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                               ),
+                       ),
+
+                       array(
+                               'Custom transform',
+                               array(
+                                       'foo' => '?',
+                                       'bar' => '?',
+                                       '_dummy' => '?',
+                                       '_dummy2' => '?',
+                                       '_dummy3' => '?',
+                                       ApiResult::META_CONTENT => 'foo',
+                                       ApiResult::META_PRESERVE_KEYS => array( '_dummy2', '_dummy3' ),
+                               ),
+                               array(
+                                       'Custom' => array( $this, 'customTransform' ),
+                                       'BC' => array(),
+                                       'Types' => array(),
+                                       'Strip' => 'all'
+                               ),
+                               array(
+                                       '*' => 'FOO',
+                                       'bar' => 'BAR',
+                                       'baz' => array( 'a', 'b' ),
+                                       '_dummy2' => '_DUMMY2',
+                                       '_dummy3' => '_DUMMY3',
+                                       ApiResult::META_CONTENT => 'bar',
+                               ),
+                       ),
+               );
+
+       }
+
+       /**
+        * Custom transformer for testTransformations
+        * @param array &$data
+        * @param array &$metadata
+        */
+       public function customTransform( &$data, &$metadata ) {
+               // Prevent recursion
+               if ( isset( $metadata['_added'] ) ) {
+                       $metadata[ApiResult::META_TYPE] = 'array';
+                       return;
+               }
+
+               foreach ( $data as $k => $v ) {
+                       $data[$k] = strtoupper( $k );
+               }
+               $data['baz'] = array( '_added' => 1, 'z' => 'b', 'y' => 'a' );
+               $metadata[ApiResult::META_PRESERVE_KEYS][0] = '_dummy';
+               $data[ApiResult::META_CONTENT] = 'bar';
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testDeprecatedFunctions() {
+               // Ignore ApiResult deprecation warnings during this test
+               set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
+                       if ( preg_match( '/Use of ApiResult::\S+ was deprecated in MediaWiki \d+.\d+\./', $errstr ) ) {
+                               return true;
+                       }
+                       return false;
+               } );
+               $reset = new ScopedCallback( 'restore_error_handler' );
+
+               $context = new DerivativeContext( RequestContext::getMain() );
+               $context->setConfig( new HashConfig( array(
+                       'APIModules' => array(),
+                       'APIFormatModules' => array(),
+                       'APIMaxResultSize' => 42,
+               ) ) );
+               $main = new ApiMain( $context );
+               $result = TestingAccessWrapper::newFromObject( new ApiResult( $main ) );
+               $this->assertSame( 42, $result->maxSize );
+               $this->assertSame( $main->getErrorFormatter(), $result->errorFormatter );
+               $this->assertSame( $main, $result->mainForContinuation );
+
+               $result = new ApiResult( 8388608 );
+
+               $result->addContentValue( null, 'test', 'content' );
+               $result->addContentValue( array( 'foo', 'bar' ), 'test', 'content' );
+               $result->addIndexedTagName( null, 'itn' );
+               $result->addSubelementsList( null, array( 'sub' ) );
+               $this->assertSame( array(
+                       'foo' => array(
+                               'bar' => array(
+                                       '*' => 'content',
+                               ),
+                       ),
+                       '*' => 'content',
+               ), $result->getData() );
+               $result->setRawMode();
+               $this->assertSame( array(
+                       'foo' => array(
+                               'bar' => array(
+                                       '*' => 'content',
+                               ),
+                       ),
+                       '*' => 'content',
+                       '_element' => 'itn',
+                       '_subelements' => array( 'sub' ),
+               ), $result->getData() );
+
+               $arr = array();
+               ApiResult::setContent( $arr, 'value' );
+               ApiResult::setContent( $arr, 'value2', 'foobar' );
+               $this->assertSame( array(
+                       ApiResult::META_CONTENT => 'content',
+                       'content' => 'value',
+                       'foobar' => array(
+                               ApiResult::META_CONTENT => 'content',
+                               'content' => 'value2',
+                       ),
+               ), $arr );
+
+               $result = new ApiResult( 3 );
+               $formatter = new ApiErrorFormatter_BackCompat( $result );
+               $result->setErrorFormatter( $formatter );
+               $result->disableSizeCheck();
+               $this->assertTrue( $result->addValue( null, 'foo', '1234567890' ) );
+               $result->enableSizeCheck();
+               $this->assertSame( 0, $result->getSize() );
+               $this->assertFalse( $result->addValue( null, 'foo', '1234567890' ) );
+
+               $arr = array( 'foo' => array( 'bar' => 1 ) );
+               $result->setIndexedTagName_recursive( $arr, 'itn' );
+               $this->assertSame( array(
+                       'foo' => array(
+                               'bar' => 1,
+                               ApiResult::META_INDEXED_TAG_NAME => 'itn'
+                       ),
+               ), $arr );
+
+               $status = Status::newGood();
+               $status->fatal( 'parentheses', '1' );
+               $status->fatal( 'parentheses', '2' );
+               $status->warning( 'parentheses', '3' );
+               $status->warning( 'parentheses', '4' );
+               $this->assertSame( array(
+                       array(
+                               'type' => 'error',
+                               'message' => 'parentheses',
+                               'params' => array(
+                                       0 => '1',
+                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
+                               ),
+                       ),
+                       array(
+                               'type' => 'error',
+                               'message' => 'parentheses',
+                               'params' => array(
+                                       0 => '2',
+                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
+                               ),
+                       ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'error',
+               ), $result->convertStatusToArray( $status, 'error' ) );
+               $this->assertSame( array(
+                       array(
+                               'type' => 'warning',
+                               'message' => 'parentheses',
+                               'params' => array(
+                                       0 => '3',
+                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
+                               ),
+                       ),
+                       array(
+                               'type' => 'warning',
+                               'message' => 'parentheses',
+                               'params' => array(
+                                       0 => '4',
+                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
+                               ),
+                       ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'warning',
+               ), $result->convertStatusToArray( $status, 'warning' ) );
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testDeprecatedContinuation() {
+               // Ignore ApiResult deprecation warnings during this test
+               set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
+                       if ( preg_match( '/Use of ApiResult::\S+ was deprecated in MediaWiki \d+.\d+\./', $errstr ) ) {
+                               return true;
+                       }
+                       return false;
+               } );
+
+               $reset = new ScopedCallback( 'restore_error_handler' );
+               $allModules = array(
+                       new MockApiQueryBase( 'mock1' ),
+                       new MockApiQueryBase( 'mock2' ),
+                       new MockApiQueryBase( 'mocklist' ),
+               );
+               $generator = new MockApiQueryBase( 'generator' );
+
+               $main = new ApiMain( RequestContext::getMain() );
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->setContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $result->setGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( array(
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+                       'generator' => array( 'gcontinue' => 3 ),
+               ), $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->setContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $result->setGeneratorContinueParam( $generator, 'gcontinue', array( 3, 4 ) );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( array(
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2|mocklist',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+                       'generator' => array( 'gcontinue' => '3|4' ),
+               ), $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $result->setGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( array(
+                       'mlcontinue' => 2,
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( '', $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( array(
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+                       'generator' => array( 'gcontinue' => 3 ),
+               ), $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->setGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( array(
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||mocklist',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( '', $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( array(
+                       'generator' => array( 'gcontinue' => 3 ),
+               ), $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->setContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( array(
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+               ), $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->setContinueParam( $allModules[0], 'm1continue', array( 1, 2 ) );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( array(
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2|mocklist',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( array(
+                       'mock1' => array( 'm1continue' => '1|2' ),
+               ), $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( array(
+                       'mlcontinue' => 2,
+                       'continue' => '-||mock1|mock2',
+               ), $result->getResultData( 'continue' ) );
+               $this->assertSame( '', $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( array(
+                       'mocklist' => array( 'mlcontinue' => 2 ),
+               ), $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( null, $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame( array( false, $allModules ), $ret );
+               $result->endContinuation( 'raw' );
+               $result->endContinuation( 'standard' );
+               $this->assertSame( null, $result->getResultData( 'continue' ) );
+               $this->assertSame( '', $result->getResultData( 'batchcomplete' ) );
+               $this->assertSame( null, $result->getResultData( 'query-continue' ) );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( '||mock2', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame(
+                       array( false, array_values( array_diff_key( $allModules, array( 1 => 1 ) ) ) ),
+                       $ret
+               );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $ret = $result->beginContinuation( '-||', $allModules, array( 'mock1', 'mock2' ) );
+               $this->assertSame(
+                       array( true, array_values( array_diff_key( $allModules, array( 0 => 0, 1 => 1 ) ) ) ),
+                       $ret
+               );
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               try {
+                       $result->beginContinuation( 'foo', $allModules, array( 'mock1', 'mock2' ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UsageException $ex ) {
+                       $this->assertSame(
+                               'Invalid continue param. You should pass the original value returned by the previous query',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               $main->setContinuationManager( null );
+
+               $result = new ApiResult( 8388608 );
+               $result->setMainForContinuation( $main );
+               $result->beginContinuation( '||mock2', array_slice( $allModules, 0, 2 ), array( 'mock1', 'mock2' ) );
+               try {
+                       $result->setContinueParam( $allModules[1], 'm2continue', 1 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Module \'mock2\' was not supposed to have been executed, but it was executed anyway',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $result->setContinueParam( $allModules[2], 'mlcontinue', 1 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Module \'mocklist\' called ApiContinuationManager::addContinueParam but was not passed to ApiContinuationManager::__construct',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               $main->setContinuationManager( null );
+
+       }
+
+       public function testObjectSerialization() {
+               $arr = array();
+               ApiResult::setValue( $arr, 'foo', (object)array( 'a' => 1, 'b' => 2 ) );
+               $this->assertSame( array(
+                       'a' => 1,
+                       'b' => 2,
+                       ApiResult::META_TYPE => 'assoc',
+               ), $arr['foo'] );
+
+               $arr = array();
+               ApiResult::setValue( $arr, 'foo', new ApiResultTestStringifiableObject() );
+               $this->assertSame( 'Ok', $arr['foo'] );
+
+               $arr = array();
+               ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject( 'Ok' ) );
+               $this->assertSame( 'Ok', $arr['foo'] );
+
+               try {
+                       $arr = array();
+                       ApiResult::setValue( $arr, 'foo',  new ApiResultTestSerializableObject(
+                               new ApiResultTestStringifiableObject()
+                       ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'ApiResultTestSerializableObject::serializeForApiResult() returned an object of class ApiResultTestStringifiableObject',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               try {
+                       $arr = array();
+                       ApiResult::setValue( $arr, 'foo',  new ApiResultTestSerializableObject( NAN ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'ApiResultTestSerializableObject::serializeForApiResult() returned an invalid value: Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $arr = array();
+               ApiResult::setValue( $arr, 'foo',  new ApiResultTestSerializableObject(
+                       array(
+                               'one' => new ApiResultTestStringifiableObject( '1' ),
+                               'two' => new ApiResultTestSerializableObject( 2 ),
+                       )
+               ) );
+               $this->assertSame( array(
+                       'one' => '1',
+                       'two' => 2,
+               ), $arr['foo'] );
+       }
+
+}
+
+class ApiResultTestStringifiableObject {
+       private $ret;
+
+       public function __construct( $ret = 'Ok' ) {
+               $this->ret = $ret;
+       }
+
+       public function __toString() {
+               return $this->ret;
+       }
+}
+
+class ApiResultTestSerializableObject {
+       private $ret;
+
+       public function __construct( $ret ) {
+               $this->ret = $ret;
+       }
+
+       public function __toString() {
+               return "Fail";
+       }
+
+       public function serializeForApiResult() {
+               return $this->ret;
+       }
+}
index 8c27b10..da62bb0 100644 (file)
@@ -116,7 +116,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
 
                // construct result
                $results = array(
-                       $module->getResultData(),
+                       $module->getResult()->getResultData( null, array( 'Strip' => 'all' ) ),
                        $context->getRequest(),
                        $context->getRequest()->getSessionArray()
                );
index d94aa2c..516da0c 100644 (file)
@@ -4,9 +4,6 @@ class MockApi extends ApiBase {
        public function execute() {
        }
 
-       public function getVersion() {
-       }
-
        public function __construct() {
        }
 
index 4bede51..f5b50e5 100644 (file)
@@ -1,11 +1,15 @@
 <?php
 class MockApiQueryBase extends ApiQueryBase {
+       private $name;
+
        public function execute() {
        }
 
-       public function getVersion() {
+       public function __construct( $name = 'mock' ) {
+               $this->name = $name;
        }
 
-       public function __construct() {
+       public function getModuleName() {
+               return $this->name;
        }
 }
index 1e4ea53..3fcfc73 100644 (file)
@@ -16,8 +16,12 @@ class ApiFormatDbgTest extends ApiFormatTestBase {
                return array(
                        // Basic types
                        array( array( null ), "array ({$warning}\n  0 => NULL,\n)" ),
-                       array( array( true ), "array ({$warning}\n  0 => true,\n)" ),
-                       array( array( false ), "array ({$warning}\n  0 => false,\n)" ),
+                       array( array( true ), "array ({$warning}\n  0 => '',\n)" ),
+                       array( array( false ), "array ({$warning}\n)" ),
+                       array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "array ({$warning}\n  0 => true,\n)" ),
+                       array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "array ({$warning}\n  0 => false,\n)" ),
                        array( array( 42 ), "array ({$warning}\n  0 => 42,\n)" ),
                        array( array( 42.5 ), "array ({$warning}\n  0 => 42.5,\n)" ),
                        array( array( 1e42 ), "array ({$warning}\n  0 => 1.0E+42,\n)" ),
@@ -29,9 +33,22 @@ class ApiFormatDbgTest extends ApiFormatTestBase {
                        array( array( array( 1 ) ), "array ({$warning}\n  0 => \n  array (\n    0 => 1,\n  ),\n)" ),
                        array( array( array( 'x' => 1 ) ), "array ({$warning}\n  0 => \n  array (\n    'x' => 1,\n  ),\n)" ),
                        array( array( array( 2 => 1 ) ), "array ({$warning}\n  0 => \n  array (\n    2 => 1,\n  ),\n)" ),
+                       array( array( (object)array() ), "array ({$warning}\n  0 => \n  array (\n  ),\n)" ),
+                       array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), "array ({$warning}\n  0 => \n  array (\n    0 => 1,\n  ),\n)" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), "array ({$warning}\n  0 => \n  array (\n    0 => 1,\n  ),\n)" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), "array ({$warning}\n  0 => \n  array (\n    'x' => 1,\n  ),\n)" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                               "array ({$warning}\n  0 => \n  array (\n    0 => \n    array (\n      'key' => 'x',\n      '*' => 1,\n    ),\n  ),\n)" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), "array ({$warning}\n  0 => \n  array (\n    'x' => 1,\n  ),\n)" ),
+                       array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), "array ({$warning}\n  0 => \n  array (\n    0 => 'a',\n    1 => 'b',\n  ),\n)" ),
 
                        // Content
-                       array( array( '*' => 'foo' ), "array ({$warning}\n  '*' => 'foo',\n)" ),
+                       array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                               "array ({$warning}\n  '*' => 'foo',\n)" ),
+
+                       // BC Subelements
+                       array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                               "array ({$warning}\n  'foo' => \n  array (\n    '*' => 'foo',\n  ),\n)" ),
                );
        }
 
index 2800d2d..c0f67f8 100644 (file)
@@ -24,8 +24,12 @@ class ApiFormatDumpTest extends ApiFormatTestBase {
                return array(
                        // Basic types
                        array( array( null ), "array(2) {{$warning}\n  [0]=>\n  NULL\n}\n" ),
-                       array( array( true ), "array(2) {{$warning}\n  [0]=>\n  bool(true)\n}\n" ),
-                       array( array( false ), "array(2) {{$warning}\n  [0]=>\n  bool(false)\n}\n" ),
+                       array( array( true ), "array(2) {{$warning}\n  [0]=>\n  string(0) \"\"\n}\n" ),
+                       array( array( false ), "array(1) {{$warning}\n}\n" ),
+                       array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "array(2) {{$warning}\n  [0]=>\n  bool(true)\n}\n" ),
+                       array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "array(2) {{$warning}\n  [0]=>\n  bool(false)\n}\n" ),
                        array( array( 42 ), "array(2) {{$warning}\n  [0]=>\n  int(42)\n}\n" ),
                        array( array( 42.5 ), "array(2) {{$warning}\n  [0]=>\n  float(42.5)\n}\n" ),
                        array( array( 1e42 ), "array(2) {{$warning}\n  [0]=>\n  float(1.0E+42)\n}\n" ),
@@ -37,9 +41,22 @@ class ApiFormatDumpTest extends ApiFormatTestBase {
                        array( array( array( 1 ) ), "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [0]=>\n    int(1)\n  }\n}\n" ),
                        array( array( array( 'x' => 1 ) ), "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [\"x\"]=>\n    int(1)\n  }\n}\n" ),
                        array( array( array( 2 => 1 ) ), "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [2]=>\n    int(1)\n  }\n}\n" ),
+                       array( array( (object)array() ), "array(2) {{$warning}\n  [0]=>\n  array(0) {\n  }\n}\n" ),
+                       array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [0]=>\n    int(1)\n  }\n}\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [0]=>\n    int(1)\n  }\n}\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [\"x\"]=>\n    int(1)\n  }\n}\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                               "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [0]=>\n    array(2) {\n      [\"key\"]=>\n      string(1) \"x\"\n      [\"*\"]=>\n      int(1)\n    }\n  }\n}\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), "array(2) {{$warning}\n  [0]=>\n  array(1) {\n    [\"x\"]=>\n    int(1)\n  }\n}\n" ),
+                       array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), "array(2) {{$warning}\n  [0]=>\n  array(2) {\n    [0]=>\n    string(1) \"a\"\n    [1]=>\n    string(1) \"b\"\n  }\n}\n" ),
 
                        // Content
-                       array( array( '*' => 'foo' ), "array(2) {{$warning}\n  [\"*\"]=>\n  string(3) \"foo\"\n}\n" ),
+                       array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                               "array(2) {{$warning}\n  [\"*\"]=>\n  string(3) \"foo\"\n}\n" ),
+
+                       // BC Subelements
+                       array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                               "array(2) {{$warning}\n  [\"foo\"]=>\n  array(1) {\n    [\"*\"]=>\n    string(3) \"foo\"\n  }\n}\n" ),
                );
        }
 
index bdf3f13..3dfcaf0 100644 (file)
@@ -8,34 +8,102 @@ class ApiFormatJsonTest extends ApiFormatTestBase {
 
        protected $printerName = 'json';
 
+       private static function addFormatVersion( $format, $arr ) {
+               foreach ( $arr as &$p ) {
+                       if ( !isset( $p[2] ) ) {
+                               $p[2] = array( 'formatversion' => $format );
+                       } else {
+                               $p[2]['formatversion'] = $format;
+                       }
+               }
+               return $arr;
+       }
+
        public static function provideGeneralEncoding() {
-               return array(
-                       // Basic types
-                       array( array( null ), '[null]' ),
-                       array( array( true ), '[true]' ),
-                       array( array( false ), '[false]' ),
-                       array( array( 42 ), '[42]' ),
-                       array( array( 42.5 ), '[42.5]' ),
-                       array( array( 1e42 ), '[1.0e+42]' ),
-                       array( array( 'foo' ), '["foo"]' ),
-                       array( array( 'fóo' ), '["f\u00f3o"]' ),
-                       array( array( 'fóo' ), '["fóo"]', array( 'utf8' => 1 ) ),
-
-                       // Arrays and objects
-                       array( array( array() ), '[[]]' ),
-                       array( array( array( 1 ) ), '[[1]]' ),
-                       array( array( array( 'x' => 1 ) ), '[{"x":1}]' ),
-                       array( array( array( 2 => 1 ) ), '[{"2":1}]' ),
-                       array( array( (object)array() ), '[{}]' ),
-
-                       // Content
-                       array( array( '*' => 'foo' ), '{"*":"foo"}' ),
-
-                       // Callbacks
-                       array( array( 1 ), '/**/myCallback([1])', array( 'callback' => 'myCallback' ) ),
-
-                       // Cross-domain mangling
-                       array( array( '< Cross-Domain-Policy >' ), '["\u003C Cross-Domain-Policy \u003E"]' ),
+               return array_merge(
+                       self::addFormatVersion( 1, array(
+                               // Basic types
+                               array( array( null ), '[null]' ),
+                               array( array( true ), '[""]' ),
+                               array( array( false ), '[]' ),
+                               array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ), '[true]' ),
+                               array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ), '[false]' ),
+                               array( array( 42 ), '[42]' ),
+                               array( array( 42.5 ), '[42.5]' ),
+                               array( array( 1e42 ), '[1.0e+42]' ),
+                               array( array( 'foo' ), '["foo"]' ),
+                               array( array( 'fóo' ), '["f\u00f3o"]' ),
+                               array( array( 'fóo' ), '["fóo"]', array( 'utf8' => 1 ) ),
+
+                               // Arrays and objects
+                               array( array( array() ), '[[]]' ),
+                               array( array( array( 1 ) ), '[[1]]' ),
+                               array( array( array( 'x' => 1 ) ), '[{"x":1}]' ),
+                               array( array( array( 2 => 1 ) ), '[{"2":1}]' ),
+                               array( array( (object)array() ), '[{}]' ),
+                               array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), '[{"0":1}]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), '[[1]]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), '[{"x":1}]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                                       '[[{"key":"x","*":1}]]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), '[{"x":1}]' ),
+                               array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), '[["a","b"]]' ),
+
+                               // Content
+                               array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                                       '{"*":"foo"}' ),
+
+                               // BC Subelements
+                               array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                                       '{"foo":{"*":"foo"}}' ),
+
+                               // Callbacks
+                               array( array( 1 ), '/**/myCallback([1])', array( 'callback' => 'myCallback' ) ),
+
+                               // Cross-domain mangling
+                               array( array( '< Cross-Domain-Policy >' ), '["\u003C Cross-Domain-Policy \u003E"]' ),
+                       ) ),
+                       self::addFormatVersion( 2, array(
+                               // Basic types
+                               array( array( null ), '[null]' ),
+                               array( array( true ), '[true]' ),
+                               array( array( false ), '[false]' ),
+                               array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ), '[true]' ),
+                               array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ), '[false]' ),
+                               array( array( 42 ), '[42]' ),
+                               array( array( 42.5 ), '[42.5]' ),
+                               array( array( 1e42 ), '[1.0e+42]' ),
+                               array( array( 'foo' ), '["foo"]' ),
+                               array( array( 'fóo' ), '["fóo"]' ),
+                               array( array( 'fóo' ), '["f\u00f3o"]', array( 'ascii' => 1 ) ),
+
+                               // Arrays and objects
+                               array( array( array() ), '[[]]' ),
+                               array( array( array( 'x' => 1 ) ), '[{"x":1}]' ),
+                               array( array( array( 2 => 1 ) ), '[{"2":1}]' ),
+                               array( array( (object)array() ), '[{}]' ),
+                               array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), '[{"0":1}]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), '[[1]]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), '[{"x":1}]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                                       '[{"x":1}]' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), '[[1]]' ),
+                               array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), '[{"0":"a","1":"b"}]' ),
+
+                               // Content
+                               array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                                       '{"content":"foo"}' ),
+
+                               // BC Subelements
+                               array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                                       '{"foo":"foo"}' ),
+
+                               // Callbacks
+                               array( array( 1 ), '/**/myCallback([1])', array( 'callback' => 'myCallback' ) ),
+
+                               // Cross-domain mangling
+                               array( array( '< Cross-Domain-Policy >' ), '["\u003C Cross-Domain-Policy \u003E"]' ),
+                       ) )
                );
        }
 
index 1487ad0..8f81a41 100644 (file)
@@ -25,9 +25,19 @@ class ApiFormatNoneTest extends ApiFormatTestBase {
                        array( array( array( 1 ) ), '' ),
                        array( array( array( 'x' => 1 ) ), '' ),
                        array( array( array( 2 => 1 ) ), '' ),
+                       array( array( (object)array() ), '' ),
+                       array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), '' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), '' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), '' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ), '' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), '' ),
+                       array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), '' ),
 
                        // Content
                        array( array( '*' => 'foo' ), '' ),
+
+                       // BC Subelements
+                       array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ), '' ),
                );
        }
 
index 469346c..0cb44e9 100644 (file)
@@ -8,26 +8,93 @@ class ApiFormatPhpTest extends ApiFormatTestBase {
 
        protected $printerName = 'php';
 
+       private static function addFormatVersion( $format, $arr ) {
+               foreach ( $arr as &$p ) {
+                       if ( !isset( $p[2] ) ) {
+                               $p[2] = array( 'formatversion' => $format );
+                       } else {
+                               $p[2]['formatversion'] = $format;
+                       }
+               }
+               return $arr;
+       }
+
        public static function provideGeneralEncoding() {
-               return array(
-                       // Basic types
-                       array( array( null ), 'a:1:{i:0;N;}' ),
-                       array( array( true ), 'a:1:{i:0;b:1;}' ),
-                       array( array( false ), 'a:1:{i:0;b:0;}' ),
-                       array( array( 42 ), 'a:1:{i:0;i:42;}' ),
-                       array( array( 42.5 ), 'a:1:{i:0;d:42.5;}' ),
-                       array( array( 1e42 ), 'a:1:{i:0;d:1.0E+42;}' ),
-                       array( array( 'foo' ), 'a:1:{i:0;s:3:"foo";}' ),
-                       array( array( 'fóo' ), 'a:1:{i:0;s:4:"fóo";}' ),
+               return array_merge(
+                       self::addFormatVersion( 1, array(
+                               // Basic types
+                               array( array( null ), 'a:1:{i:0;N;}' ),
+                               array( array( true ), 'a:1:{i:0;s:0:"";}' ),
+                               array( array( false ), 'a:0:{}' ),
+                               array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                                       'a:1:{i:0;b:1;}' ),
+                               array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                                       'a:1:{i:0;b:0;}' ),
+                               array( array( 42 ), 'a:1:{i:0;i:42;}' ),
+                               array( array( 42.5 ), 'a:1:{i:0;d:42.5;}' ),
+                               array( array( 1e42 ), 'a:1:{i:0;d:1.0E+42;}' ),
+                               array( array( 'foo' ), 'a:1:{i:0;s:3:"foo";}' ),
+                               array( array( 'fóo' ), 'a:1:{i:0;s:4:"fóo";}' ),
+
+                               // Arrays and objects
+                               array( array( array() ), 'a:1:{i:0;a:0:{}}' ),
+                               array( array( array( 1 ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
+                               array( array( array( 'x' => 1 ) ), 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ),
+                               array( array( array( 2 => 1 ) ), 'a:1:{i:0;a:1:{i:2;i:1;}}' ),
+                               array( array( (object)array() ), 'a:1:{i:0;a:0:{}}' ),
+                               array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                                       'a:1:{i:0;a:1:{i:0;a:2:{s:3:"key";s:1:"x";s:1:"*";i:1;}}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ),
+                               array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), 'a:1:{i:0;a:2:{i:0;s:1:"a";i:1;s:1:"b";}}' ),
+
+                               // Content
+                               array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                                       'a:1:{s:1:"*";s:3:"foo";}' ),
+
+                               // BC Subelements
+                               array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                                       'a:1:{s:3:"foo";a:1:{s:1:"*";s:3:"foo";}}' ),
+                       ) ),
+                       self::addFormatVersion( 2, array(
+                               // Basic types
+                               array( array( null ), 'a:1:{i:0;N;}' ),
+                               array( array( true ), 'a:1:{i:0;b:1;}' ),
+                               array( array( false ), 'a:1:{i:0;b:0;}' ),
+                               array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                                       'a:1:{i:0;b:1;}' ),
+                               array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                                       'a:1:{i:0;b:0;}' ),
+                               array( array( 42 ), 'a:1:{i:0;i:42;}' ),
+                               array( array( 42.5 ), 'a:1:{i:0;d:42.5;}' ),
+                               array( array( 1e42 ), 'a:1:{i:0;d:1.0E+42;}' ),
+                               array( array( 'foo' ), 'a:1:{i:0;s:3:"foo";}' ),
+                               array( array( 'fóo' ), 'a:1:{i:0;s:4:"fóo";}' ),
+
+                               // Arrays and objects
+                               array( array( array() ), 'a:1:{i:0;a:0:{}}' ),
+                               array( array( array( 1 ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
+                               array( array( array( 'x' => 1 ) ), 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ),
+                               array( array( array( 2 => 1 ) ), 'a:1:{i:0;a:1:{i:2;i:1;}}' ),
+                               array( array( (object)array() ), 'a:1:{i:0;a:0:{}}' ),
+                               array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                                       'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ),
+                               array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
+                               array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), 'a:1:{i:0;a:2:{i:0;s:1:"a";i:1;s:1:"b";}}' ),
 
-                       // Arrays and objects
-                       array( array( array() ), 'a:1:{i:0;a:0:{}}' ),
-                       array( array( array( 1 ) ), 'a:1:{i:0;a:1:{i:0;i:1;}}' ),
-                       array( array( array( 'x' => 1 ) ), 'a:1:{i:0;a:1:{s:1:"x";i:1;}}' ),
-                       array( array( array( 2 => 1 ) ), 'a:1:{i:0;a:1:{i:2;i:1;}}' ),
+                               // Content
+                               array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                                       'a:1:{s:7:"content";s:3:"foo";}' ),
 
-                       // Content
-                       array( array( '*' => 'foo' ), 'a:1:{s:1:"*";s:3:"foo";}' ),
+                               // BC Subelements
+                               array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                                       'a:1:{s:3:"foo";s:3:"foo";}' ),
+                       ) )
                );
        }
 
index 06e9204..b0a2a96 100644 (file)
@@ -16,8 +16,12 @@ class ApiFormatTxtTest extends ApiFormatTestBase {
                return array(
                        // Basic types
                        array( array( null ), "Array\n({$warning}\n    [0] => \n)\n" ),
-                       array( array( true ), "Array\n({$warning}\n    [0] => 1\n)\n" ),
-                       array( array( false ), "Array\n({$warning}\n    [0] => \n)\n" ),
+                       array( array( true ), "Array\n({$warning}\n    [0] => \n)\n" ),
+                       array( array( false ), "Array\n({$warning}\n)\n" ),
+                       array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "Array\n({$warning}\n    [0] => 1\n)\n" ),
+                       array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "Array\n({$warning}\n    [0] => \n)\n" ),
                        array( array( 42 ), "Array\n({$warning}\n    [0] => 42\n)\n" ),
                        array( array( 42.5 ), "Array\n({$warning}\n    [0] => 42.5\n)\n" ),
                        array( array( 1e42 ), "Array\n({$warning}\n    [0] => 1.0E+42\n)\n" ),
@@ -29,9 +33,22 @@ class ApiFormatTxtTest extends ApiFormatTestBase {
                        array( array( array( 1 ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [0] => 1\n        )\n\n)\n" ),
                        array( array( array( 'x' => 1 ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [x] => 1\n        )\n\n)\n" ),
                        array( array( array( 2 => 1 ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [2] => 1\n        )\n\n)\n" ),
+                       array( array( (object)array() ), "Array\n({$warning}\n    [0] => Array\n        (\n        )\n\n)\n" ),
+                       array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [0] => 1\n        )\n\n)\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [0] => 1\n        )\n\n)\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [x] => 1\n        )\n\n)\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                               "Array\n({$warning}\n    [0] => Array\n        (\n            [0] => Array\n                (\n                    [key] => x\n                    [*] => 1\n                )\n\n        )\n\n)\n" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [x] => 1\n        )\n\n)\n" ),
+                       array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), "Array\n({$warning}\n    [0] => Array\n        (\n            [0] => a\n            [1] => b\n        )\n\n)\n" ),
 
                        // Content
-                       array( array( '*' => 'foo' ), "Array\n({$warning}\n    [*] => foo\n)\n" ),
+                       array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                               "Array\n({$warning}\n    [*] => foo\n)\n" ),
+
+                       // BC Subelements
+                       array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                               "Array\n({$warning}\n    [foo] => Array\n        (\n            [*] => foo\n        )\n\n)\n" ),
                );
        }
 
index 81676e0..0711130 100644 (file)
@@ -24,8 +24,12 @@ class ApiFormatWddxTest extends ApiFormatTestBase {
                return array(
                        // Basic types
                        array( array( null ), "{$p}<var name='0'><null/></var>{$s}" ),
-                       array( array( true ), "{$p}<var name='0'><boolean value='true'/></var>{$s}" ),
-                       array( array( false ), "{$p}<var name='0'><boolean value='false'/></var>{$s}" ),
+                       array( array( true ), "{$p}<var name='0'><string></string></var>{$s}" ),
+                       array( array( false ), "{$p}{$s}" ),
+                       array( array( true, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "{$p}<var name='0'><boolean value='true'/></var>{$s}" ),
+                       array( array( false, ApiResult::META_BC_BOOLS => array( 0 ) ),
+                               "{$p}<var name='0'><boolean value='false'/></var>{$s}" ),
                        array( array( 42 ), "{$p}<var name='0'><number>42</number></var>{$s}" ),
                        array( array( 42.5 ), "{$p}<var name='0'><number>42.5</number></var>{$s}" ),
                        array( array( 1e42 ), "{$p}<var name='0'><number>1.0E+42</number></var>{$s}" ),
@@ -37,9 +41,22 @@ class ApiFormatWddxTest extends ApiFormatTestBase {
                        array( array( array( 1 ) ), "{$p}<var name='0'><array length='1'><number>1</number></array></var>{$s}" ),
                        array( array( array( 'x' => 1 ) ), "{$p}<var name='0'><struct><var name='x'><number>1</number></var></struct></var>{$s}" ),
                        array( array( array( 2 => 1 ) ), "{$p}<var name='0'><struct><var name='2'><number>1</number></var></struct></var>{$s}" ),
+                       array( array( (object)array() ), "{$p}<var name='0'><struct></struct></var>{$s}" ),
+                       array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), "{$p}<var name='0'><struct><var name='0'><number>1</number></var></struct></var>{$s}" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), "{$p}<var name='0'><array length='1'><number>1</number></array></var>{$s}" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp' ) ), "{$p}<var name='0'><struct><var name='x'><number>1</number></var></struct></var>{$s}" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                               "{$p}<var name='0'><array length='1'><struct><var name='key'><string>x</string></var><var name='*'><number>1</number></var></struct></array></var>{$s}" ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), "{$p}<var name='0'><struct><var name='x'><number>1</number></var></struct></var>{$s}" ),
+                       array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), "{$p}<var name='0'><array length='2'><string>a</string><string>b</string></array></var>{$s}" ),
 
                        // Content
-                       array( array( '*' => 'foo' ), "{$p}<var name='*'><string>foo</string></var>{$s}" ),
+                       array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                               "{$p}<var name='*'><string>foo</string></var>{$s}" ),
+
+                       // BC Subelements
+                       array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                               "{$p}<var name='foo'><struct><var name='*'><string>foo</string></var></struct></var>{$s}" ),
                );
        }
 
index afb47e7..0c31b95 100644 (file)
@@ -9,8 +9,8 @@ class ApiFormatXmlTest extends ApiFormatTestBase {
 
        protected $printerName = 'xml';
 
-       protected function setUp() {
-               parent::setUp();
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
                $page = WikiPage::factory( Title::newFromText( 'MediaWiki:ApiFormatXmlTest.xsl' ) );
                $page->doEditContent( new WikitextContent(
                        '<?xml version="1.0"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" />'
@@ -22,29 +22,79 @@ class ApiFormatXmlTest extends ApiFormatTestBase {
        }
 
        public static function provideGeneralEncoding() {
-               $tests = array(
+               return array(
                        // Basic types
-                       array( array( null ), '<?xml version="1.0"?><api><x /></api>' ),
-                       array( array( true, 'a' => true ), '<?xml version="1.0"?><api a=""><x>1</x></api>' ),
-                       array( array( false, 'a' => false ), '<?xml version="1.0"?><api><x></x></api>' ),
-                       array( array( 42, 'a' => 42 ), '<?xml version="1.0"?><api a="42"><x>42</x></api>' ),
-                       array( array( 42.5, 'a' => 42.5 ), '<?xml version="1.0"?><api a="42.5"><x>42.5</x></api>' ),
-                       array( array( 1e42, 'a' => 1e42 ), '<?xml version="1.0"?><api a="1.0E+42"><x>1.0E+42</x></api>' ),
-                       array( array( 'foo', 'a' => 'foo' ), '<?xml version="1.0"?><api a="foo"><x>foo</x></api>' ),
-                       array( array( 'fóo', 'a' => 'fóo' ), '<?xml version="1.0"?><api a="fóo"><x>fóo</x></api>' ),
+                       array( array( null, 'a' => null ), '<?xml version="1.0"?><api><_v _idx="0" /></api>' ),
+                       array( array( true, 'a' => true ), '<?xml version="1.0"?><api a=""><_v _idx="0">true</_v></api>' ),
+                       array( array( false, 'a' => false ), '<?xml version="1.0"?><api><_v _idx="0">false</_v></api>' ),
+                       array( array( true, 'a' => true, ApiResult::META_BC_BOOLS => array( 0, 'a' ) ),
+                               '<?xml version="1.0"?><api a=""><_v _idx="0">1</_v></api>' ),
+                       array( array( false, 'a' => false, ApiResult::META_BC_BOOLS => array( 0, 'a' ) ),
+                               '<?xml version="1.0"?><api><_v _idx="0"></_v></api>' ),
+                       array( array( 42, 'a' => 42 ), '<?xml version="1.0"?><api a="42"><_v _idx="0">42</_v></api>' ),
+                       array( array( 42.5, 'a' => 42.5 ), '<?xml version="1.0"?><api a="42.5"><_v _idx="0">42.5</_v></api>' ),
+                       array( array( 1e42, 'a' => 1e42 ), '<?xml version="1.0"?><api a="1.0E+42"><_v _idx="0">1.0E+42</_v></api>' ),
+                       array( array( 'foo', 'a' => 'foo' ), '<?xml version="1.0"?><api a="foo"><_v _idx="0">foo</_v></api>' ),
+                       array( array( 'fóo', 'a' => 'fóo' ), '<?xml version="1.0"?><api a="fóo"><_v _idx="0">fóo</_v></api>' ),
 
                        // Arrays and objects
-                       array( array( array() ), '<?xml version="1.0"?><api><x /></api>' ),
-                       array( array( array( 'x' => 1 ) ), '<?xml version="1.0"?><api><x x="1" /></api>' ),
-                       array( array( array( 2 => 1, '_element' => 'x' ) ), '<?xml version="1.0"?><api><x><x>1</x></x></api>' ),
+                       array( array( array() ), '<?xml version="1.0"?><api><_v /></api>' ),
+                       array( array( array( 'x' => 1 ) ), '<?xml version="1.0"?><api><_v x="1" /></api>' ),
+                       array( array( array( 2 => 1 ) ), '<?xml version="1.0"?><api><_v><_v _idx="2">1</_v></_v></api>' ),
+                       array( array( (object)array() ), '<?xml version="1.0"?><api><_v /></api>' ),
+                       array( array( array( 1, ApiResult::META_TYPE => 'assoc' ) ), '<?xml version="1.0"?><api><_v><_v _idx="0">1</_v></_v></api>' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'array' ) ), '<?xml version="1.0"?><api><_v><_v>1</_v></_v></api>' ),
+                       array( array( array( 'x' => 1, 'y' => array( 'z' => 1 ), ApiResult::META_TYPE => 'kvp' ) ),
+                               '<?xml version="1.0"?><api><_v><_v _name="x" xml:space="preserve">1</_v><_v _name="y"><z xml:space="preserve">1</z></_v></_v></api>' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'kvp', ApiResult::META_INDEXED_TAG_NAME => 'i', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                               '<?xml version="1.0"?><api><_v><i key="x" xml:space="preserve">1</i></_v></api>' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCkvp', ApiResult::META_KVP_KEY_NAME => 'key' ) ),
+                               '<?xml version="1.0"?><api><_v><_v key="x" xml:space="preserve">1</_v></_v></api>' ),
+                       array( array( array( 'x' => 1, ApiResult::META_TYPE => 'BCarray' ) ), '<?xml version="1.0"?><api><_v x="1" /></api>' ),
+                       array( array( array( 'a', 'b', ApiResult::META_TYPE => 'BCassoc' ) ), '<?xml version="1.0"?><api><_v><_v _idx="0">a</_v><_v _idx="1">b</_v></_v></api>' ),
 
                        // Content
-                       array( array( '*' => 'foo' ), '<?xml version="1.0"?><api xml:space="preserve">foo</api>' ),
+                       array( array( 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                               '<?xml version="1.0"?><api xml:space="preserve">foo</api>' ),
+
+                       // Specified element name
+                       array( array( 'foo', 'bar', ApiResult::META_INDEXED_TAG_NAME => 'itn' ),
+                               '<?xml version="1.0"?><api><itn>foo</itn><itn>bar</itn></api>' ),
 
                        // Subelements
                        array( array( 'a' => 1, 's' => 1, '_subelements' => array( 's' ) ),
                                '<?xml version="1.0"?><api a="1"><s xml:space="preserve">1</s></api>' ),
 
+                       // Content and subelement
+                       array( array( 'a' => 1, 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                               '<?xml version="1.0"?><api a="1" xml:space="preserve">foo</api>' ),
+                       array( array( 's' => array(), 'content' => 'foo', ApiResult::META_CONTENT => 'content' ),
+                               '<?xml version="1.0"?><api><s /><content xml:space="preserve">foo</content></api>' ),
+                       array(
+                               array(
+                                       's' => 1,
+                                       'content' => 'foo',
+                                       ApiResult::META_CONTENT => 'content',
+                                       ApiResult::META_SUBELEMENTS => array( 's' )
+                               ),
+                               '<?xml version="1.0"?><api><s xml:space="preserve">1</s><content xml:space="preserve">foo</content></api>'
+                       ),
+
+                       // BC Subelements
+                       array( array( 'foo' => 'foo', ApiResult::META_BC_SUBELEMENTS => array( 'foo' ) ),
+                               '<?xml version="1.0"?><api><foo xml:space="preserve">foo</foo></api>' ),
+
+                       // Name mangling
+                       array( array( 'foo.bar' => 1 ), '<?xml version="1.0"?><api foo.bar="1" />' ),
+                       array( array( '' => 1 ), '<?xml version="1.0"?><api _="1" />' ),
+                       array( array( 'foo bar' => 1 ), '<?xml version="1.0"?><api _foo.20.bar="1" />' ),
+                       array( array( 'foo:bar' => 1 ), '<?xml version="1.0"?><api _foo.3A.bar="1" />' ),
+                       array( array( 'foo%.bar' => 1 ), '<?xml version="1.0"?><api _foo.25..2E.bar="1" />' ),
+                       array( array( '4foo' => 1, 'foo4' => 1 ), '<?xml version="1.0"?><api _4foo="1" foo4="1" />' ),
+                       array( array( "foo\xe3\x80\x80bar" => 1 ), '<?xml version="1.0"?><api _foo.3000.bar="1" />' ),
+                       array( array( 'foo:bar' => 1, ApiResult::META_PRESERVE_KEYS => array( 'foo:bar' ) ),
+                               '<?xml version="1.0"?><api foo:bar="1" />' ),
+
                        // includenamespace param
                        array( array( 'x' => 'foo' ), '<?xml version="1.0"?><api x="foo" xmlns="http://www.mediawiki.org/xml/api/" />',
                                array( 'includexmlnamespace' => 1 ) ),
@@ -62,40 +112,6 @@ class ApiFormatXmlTest extends ApiFormatTestBase {
                                        '" type="text/xsl" ?><api />',
                                array( 'xslt' => 'MediaWiki:ApiFormatXmlTest.xsl' ) ),
                );
-
-               // Add in the needed "_element" for all indexed arrays
-               $ret = array();
-               foreach ( $tests as $v ) {
-                       $v[0] += array( '_element' => 'x' );
-                       $ret[] = $v;
-               }
-               return $ret;
-       }
-
-       /**
-        * @dataProvider provideXmlFail
-        */
-       public function testXmlFail( array $data, $expect, array $params = array() ) {
-               try {
-                       echo $this->encodeData( $params, $data ) . "\n";
-                       $this->fail( "Expected exception not thrown" );
-               } catch ( MWException $ex ) {
-                       $this->assertSame( $expect, $ex->getMessage(), 'Expected exception' );
-               }
-       }
-
-       public static function provideXmlFail() {
-               return array(
-                       // Array without _element
-                       array( array( 1 ), 'Internal error in ApiFormatXml::recXmlPrint: (api, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName().' ),
-                       // Content and subelement
-                       array( array( 1, 's' => array(), '*' => 2, '_element' => 'x' ), 'Internal error in ApiFormatXml::recXmlPrint: (api, ...) has content and subelements' ),
-                       array( array( 1, 's' => 1, '*' => 2, '_element' => 'x', '_subelements' => array( 's' ) ), 'Internal error in ApiFormatXml::recXmlPrint: (api, ...) has content and subelements' ),
-                       // These should fail but don't because of a long-standing bug (see T57371#639713)
-                       //array( array( 1, '*' => 2, '_element' => 'x' ), 'Internal error in ApiFormatXml::recXmlPrint: (api, ...) has content and subelements' ),
-                       //array( array( 's' => array(), '*' => 2 ), 'Internal error in ApiFormatXml::recXmlPrint: (api, ...) has content and subelements' ),
-                       //array( array( 's' => 1, '*' => 2, '_subelements' => array( 's' ) ), 'Internal error in ApiFormatXml::recXmlPrint: (api, ...) has content and subelements' ),
-               );
        }
 
 }
index 06951b7..1abb47e 100644 (file)
@@ -96,12 +96,11 @@ class MWDebugTest extends MediaWikiTestCase {
                $apiMain = new ApiMain( $context );
 
                $result = new ApiResult( $apiMain );
-               $result->setRawMode( true );
 
                MWDebug::appendDebugInfoToApiResult( $context, $result );
 
                $this->assertInstanceOf( 'ApiResult', $result );
-               $data = $result->getData();
+               $data = $result->getResultData();
 
                $expectedKeys = array( 'mwVersion', 'phpEngine', 'phpVersion', 'gitRevision', 'gitBranch',
                        'gitViewUrl', 'time', 'log', 'debugLog', 'queries', 'request', 'memory',
index ec56b63..b749662 100644 (file)
@@ -35,7 +35,10 @@ class UploadFromUrlTest extends ApiTestCase {
 
                wfSetupSession( $sessionId );
 
-               return array( $module->getResultData(), $req );
+               return array(
+                       $module->getResult()->getResultData( null, array( 'Strip' => 'all' ) ),
+                       $req
+               );
        }
 
        /**