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)
15 files changed:
1  2 
RELEASE-NOTES-1.25
autoload.php
includes/api/ApiBase.php
includes/api/ApiEditPage.php
includes/api/ApiHelp.php
includes/api/ApiMain.php
includes/api/ApiParamInfo.php
includes/api/ApiParse.php
includes/api/ApiProtect.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryUserInfo.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json

diff --combined RELEASE-NOTES-1.25
@@@ -94,7 -94,8 +94,7 @@@ production
  * Update QUnit from v1.14.0 to v1.16.0.
  * Update Moment.js from v2.8.3 to v2.8.4.
  * Special:Tags now allows for manipulating the list of user-modifiable change
 -  tags. Actually modifying the tagging of a revision or log entry is not
 -  implemented yet.
 +  tags.
  * Added 'managetags' user right and 'ChangeTagCanCreate', 'ChangeTagCanDelete',
    and 'ChangeTagCanCreate' hooks to allow for managing user-modifiable change
    tags.
    proper, published library, which is now tagged as v1.0.0.
  * A new message (defaulting to blank), 'editnotice-notext', can be shown to users
    when they are editing if no edit notices apply to the page being edited.
 +* (T94536) You can now make the sitenotice appear to logged-in users only by
 +  editing MediaWiki:Anonnotice and replacing its content with "". Setting it to
 +  "-" (default) will continue disable it and fallback to MediaWiki:Sitenotice.
 +* Modifying the tagging of a revision or log entry is now available via
 +  Special:EditTags, generally accessed via the revision-deletion-like interface
 +  on history pages and Special:Log is likely to be more useful.
 +* Added 'applychangetags' and 'changetags' user rights.
  
  ==== External libraries ====
  * MediaWiki now requires certain external libraries to be installed. In the past
    HTML validation.
  * $wgUseTidy is now set when parserTests are run with the tidy option to match
    output on wiki.
 +* (T37472) update.php will purge ResourceLoader cache unless --nopurge is passed to it.
  
  === Action API changes in 1.25 ===
  * (T67403) XML tag highlighting is now only performed for formats
    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.
    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
    * 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:
@@@ -431,11 -450,6 +457,11 @@@ changes to languages because of Bugzill
    retrievedfrom, thisisdeleted, viewsourcelink, lastmodifiedat, laggedslavemode,
    protect-summary-cascade
  * All BloomCache related code has been removed. This was largely experimental.
 +* $wgResourceModuleSkinStyles no longer supports per-module local or remote paths. They
 +  can only be set for the entire skin.
 +* Removed global function swap(). (deprecated since 1.24)
 +* The global importScript and importStylesheet functions, as well as the loadedScripts object,
 +  from wikibits.js (deprecated since 1.17) now emit warnings through mw.log.warn when accessed.
  
  == Compatibility ==
  
diff --combined autoload.php
@@@ -21,11 -21,14 +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 +56,7 @@@
        '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',
        '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',
        'ApiTokens' => __DIR__ . '/includes/api/ApiTokens.php',
        'ApiUnblock' => __DIR__ . '/includes/api/ApiUnblock.php',
        'ApiUndelete' => __DIR__ . '/includes/api/ApiUndelete.php',
        'CgzCopyTransaction' => __DIR__ . '/maintenance/storage/recompressTracked.php',
        'ChangePassword' => __DIR__ . '/maintenance/changePassword.php',
        'ChangeTags' => __DIR__ . '/includes/ChangeTags.php',
 +      'ChangeTagsList' => __DIR__ . '/includes/changetags/ChangeTagsList.php',
 +      'ChangeTagsLogItem' => __DIR__ . '/includes/changetags/ChangeTagsLogItem.php',
 +      'ChangeTagsLogList' => __DIR__ . '/includes/changetags/ChangeTagsLogList.php',
 +      'ChangeTagsRevisionItem' => __DIR__ . '/includes/changetags/ChangeTagsRevisionItem.php',
 +      'ChangeTagsRevisionList' => __DIR__ . '/includes/changetags/ChangeTagsRevisionList.php',
        'ChangesFeed' => __DIR__ . '/includes/changes/ChangesFeed.php',
        'ChangesList' => __DIR__ . '/includes/changes/ChangesList.php',
        'ChangesListSpecialPage' => __DIR__ . '/includes/specialpage/ChangesListSpecialPage.php',
        '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',
        'MWLoggerFactory' => __DIR__ . '/includes/debug/logger/Shims.php',
        'MWLoggerLegacyLogger' => __DIR__ . '/includes/debug/logger/Shims.php',
        'MWLoggerLegacySpi' => __DIR__ . '/includes/debug/logger/Shims.php',
 -      'MWLoggerMonologHandler' => __DIR__ . '/includes/debug/logger/Shims.php',
 -      'MWLoggerMonologLegacyFormatter' => __DIR__ . '/includes/debug/logger/Shims.php',
 -      'MWLoggerMonologProcessor' => __DIR__ . '/includes/debug/logger/Shims.php',
 -      'MWLoggerMonologSpi' => __DIR__ . '/includes/debug/logger/Shims.php',
 -      'MWLoggerMonologSyslogHandler' => __DIR__ . '/includes/debug/logger/Shims.php',
 +      'MWLoggerMonologHandler' => __DIR__ . '/includes/debug/logger/monolog/Shims.php',
 +      'MWLoggerMonologLegacyFormatter' => __DIR__ . '/includes/debug/logger/monolog/Shims.php',
 +      'MWLoggerMonologProcessor' => __DIR__ . '/includes/debug/logger/monolog/Shims.php',
 +      'MWLoggerMonologSpi' => __DIR__ . '/includes/debug/logger/monolog/Shims.php',
 +      'MWLoggerMonologSyslogHandler' => __DIR__ . '/includes/debug/logger/monolog/Shims.php',
        'MWLoggerNullSpi' => __DIR__ . '/includes/debug/logger/Shims.php',
        'MWLoggerSpi' => __DIR__ . '/includes/debug/logger/Shims.php',
        'MWMemcached' => __DIR__ . '/includes/objectcache/MemcachedClient.php',
        'PoolCounter_Stub' => __DIR__ . '/includes/poolcounter/PoolCounter.php',
        'PoolWorkArticleView' => __DIR__ . '/includes/poolcounter/PoolWorkArticleView.php',
        'PopulateBacklinkNamespace' => __DIR__ . '/maintenance/populateBacklinkNamespace.php',
 -      'PopulateBloomFilter' => __DIR__ . '/maintenance/populateBloomCache.php',
        'PopulateCategory' => __DIR__ . '/maintenance/populateCategory.php',
        'PopulateFilearchiveSha1' => __DIR__ . '/maintenance/populateFilearchiveSha1.php',
        'PopulateImageSha1' => __DIR__ . '/maintenance/populateImageSha1.php',
        'SpecialContributions' => __DIR__ . '/includes/specials/SpecialContributions.php',
        'SpecialCreateAccount' => __DIR__ . '/includes/specials/SpecialCreateAccount.php',
        'SpecialDiff' => __DIR__ . '/includes/specials/SpecialDiff.php',
 +      'SpecialEditTags' => __DIR__ . '/includes/specials/SpecialEditTags.php',
        'SpecialEditWatchlist' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php',
        'SpecialEmailUser' => __DIR__ . '/includes/specials/SpecialEmailuser.php',
        'SpecialExpandTemplates' => __DIR__ . '/includes/specials/SpecialExpandTemplates.php',
        'SpecialNewFiles' => __DIR__ . '/includes/specials/SpecialNewimages.php',
        'SpecialNewpages' => __DIR__ . '/includes/specials/SpecialNewpages.php',
        'SpecialPage' => __DIR__ . '/includes/specialpage/SpecialPage.php',
 +      'SpecialPageAction' => __DIR__ . '/includes/actions/SpecialPageAction.php',
        'SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php',
        'SpecialPageLanguage' => __DIR__ . '/includes/specials/SpecialPageLanguage.php',
        'SpecialPagesWithProp' => __DIR__ . '/includes/specials/SpecialPagesWithProp.php',
        'TableCleanupTest' => __DIR__ . '/maintenance/cleanupTable.inc',
        'TableDiffFormatter' => __DIR__ . '/includes/diff/TableDiffFormatter.php',
        'TablePager' => __DIR__ . '/includes/pager/TablePager.php',
 +      'TagLogFormatter' => __DIR__ . '/includes/logging/TagLogFormatter.php',
        'TempFSFile' => __DIR__ . '/includes/filebackend/TempFSFile.php',
        'TempFileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php',
        'TemplateParser' => __DIR__ . '/includes/TemplateParser.php',
diff --combined includes/api/ApiBase.php
@@@ -98,17 -98,12 +98,17 @@@ abstract class ApiBase extends ContextS
         */
        const GET_VALUES_FOR_HELP = 1;
  
 +      /** @var array Maps extension paths to info arrays */
 +      private static $extensionInfo = null;
 +
        /** @var ApiMain */
        private $mMainModule;
        /** @var string */
        private $mModuleName, $mModulePrefix;
        private $mSlaveDB = null;
        private $mParamCache = array();
 +      /** @var array|null|bool */
 +      private $mModuleSource = false;
  
        /**
         * @param ApiMain $mainModule
        }
  
        /**
-        * 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();
        }
  
        /**
                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 );
+       }
        /**@}*/
  
        /************************************************************************//**
                                                        $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(
         * @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 );
        }
  
        /**
                        'code' => 'nosuchrcid',
                        'info' => "There is no change with rcid \"\$1\""
                ),
 +              'nosuchlogid' => array(
 +                      'code' => 'nosuchlogid',
 +                      'info' => "There is no log entry with ID \"\$1\""
 +              ),
                'protect-invalidaction' => array(
                        'code' => 'protect-invalidaction',
                        'info' => "Invalid protection type \"\$1\""
                return $flags;
        }
  
 +      /**
 +       * Returns information about the source of this module, if known
 +       *
 +       * Returned array is an array with the following keys:
 +       * - path: Install path
 +       * - name: Extension name, or "MediaWiki" for core
 +       * - namemsg: (optional) i18n message key for a display name
 +       * - license-name: (optional) Name of license
 +       *
 +       * @return array|null
 +       */
 +      protected function getModuleSourceInfo() {
 +              global $IP;
 +
 +              if ( $this->mModuleSource !== false ) {
 +                      return $this->mModuleSource;
 +              }
 +
 +              // First, try to find where the module comes from...
 +              $rClass = new ReflectionClass( $this );
 +              $path = $rClass->getFileName();
 +              if ( !$path ) {
 +                      // No path known?
 +                      $this->mModuleSource = null;
 +                      return null;
 +              }
 +              $path = realpath( $path ) ?: $path;
 +
 +              // Build map of extension directories to extension info
 +              if ( self::$extensionInfo === null ) {
 +                      self::$extensionInfo = array(
 +                              realpath( __DIR__ ) ?: __DIR__ => array(
 +                                      'path' => $IP,
 +                                      'name' => 'MediaWiki',
 +                                      'license-name' => 'GPL-2.0+',
 +                              ),
 +                              realpath( "$IP/extensions" ) ?: "$IP/extensions" => null,
 +                      );
 +                      $keep = array(
 +                              'path' => null,
 +                              'name' => null,
 +                              'namemsg' => null,
 +                              'license-name' => null,
 +                      );
 +                      foreach ( $this->getConfig()->get( 'ExtensionCredits' ) as $group ) {
 +                              foreach ( $group as $ext ) {
 +                                      if ( !isset( $ext['path'] ) || !isset( $ext['name'] ) ) {
 +                                              // This shouldn't happen, but does anyway.
 +                                              continue;
 +                                      }
 +
 +                                      $extpath = $ext['path'];
 +                                      if ( !is_dir( $extpath ) ) {
 +                                              $extpath = dirname( $extpath );
 +                                      }
 +                                      self::$extensionInfo[realpath( $extpath ) ?: $extpath] =
 +                                              array_intersect_key( $ext, $keep );
 +                              }
 +                      }
 +                      foreach ( ExtensionRegistry::getInstance()->getAllThings() as $ext ) {
 +                              $extpath = $ext['path'];
 +                              if ( !is_dir( $extpath ) ) {
 +                                      $extpath = dirname( $extpath );
 +                              }
 +                              self::$extensionInfo[realpath( $extpath ) ?: $extpath] =
 +                                      array_intersect_key( $ext, $keep );
 +                      }
 +              }
 +
 +              // Now traverse parent directories until we find a match or run out of
 +              // parents.
 +              do {
 +                      if ( array_key_exists( $path, self::$extensionInfo ) ) {
 +                              // Found it!
 +                              $this->mModuleSource = self::$extensionInfo[$path];
 +                              return $this->mModuleSource;
 +                      }
 +
 +                      $oldpath = $path;
 +                      $path = dirname( $path );
 +              } while ( $path !== $oldpath );
 +
 +              // No idea what extension this might be.
 +              $this->mModuleSource = null;
 +              return null;
 +      }
 +
        /**
         * Called from ApiHelp before the pieces are joined together and returned.
         *
                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();
+       }
        /**@}*/
  }
  
@@@ -82,7 -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
                        $requestArray['wpWatchthis'] = '';
                }
  
 +              // Apply change tags
 +              if ( count( $params['tags'] ) ) {
 +                      if ( $user->isAllowed( 'applychangetags' ) ) {
 +                              $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
 +                      } else {
 +                              $this->dieUsage( 'You don\'t have permission to set change tags.', 'taggingnotallowed' );
 +                      }
 +              }
 +
                // Pass through anything else we might have been given, to support extensions
                // This is kind of a hack but it's the best we can do to make extensions work
                $requestArray += $this->getRequest()->getValues();
                        case EditPage::AS_TEXTBOX_EMPTY:
                                $this->dieUsageMsg( 'emptynewsection' );
  
 +                      case EditPage::AS_CHANGE_TAG_ERROR:
 +                              $this->dieStatus( $status );
 +
                        case EditPage::AS_SUCCESS_NEW_ARTICLE:
                                $r['new'] = '';
                                // fall-through
                        ),
                        'text' => null,
                        'summary' => null,
 +                      'tags' => array(
 +                              ApiBase::PARAM_TYPE => ChangeTags::listExplicitlyDefinedTags(),
 +                              ApiBase::PARAM_ISMULTI => true,
 +                      ),
                        'minor' => false,
                        'notminor' => false,
                        'bot' => false,
diff --combined includes/api/ApiHelp.php
@@@ -60,7 -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();
                                );
                        }
  
 -                      $flags = $module->getHelpFlags();
 -                      if ( $flags ) {
 -                              $help['flags'] .= Html::openElement( 'div',
 -                                      array( 'class' => 'apihelp-block apihelp-flags' ) );
 -                              $msg = $context->msg( 'api-help-flags' );
 -                              if ( !$msg->isDisabled() ) {
 -                                      $help['flags'] .= self::wrap(
 -                                              $msg->numParams( count( $flags ) ), 'apihelp-block-head', 'div'
 -                                      );
 +                      $help['flags'] .= Html::openElement( 'div',
 +                              array( 'class' => 'apihelp-block apihelp-flags' ) );
 +                      $msg = $context->msg( 'api-help-flags' );
 +                      if ( !$msg->isDisabled() ) {
 +                              $help['flags'] .= self::wrap(
 +                                      $msg->numParams( count( $flags ) ), 'apihelp-block-head', 'div'
 +                              );
 +                      }
 +                      $help['flags'] .= Html::openElement( 'ul' );
 +                      foreach ( $module->getHelpFlags() as $flag ) {
 +                              $help['flags'] .= Html::rawElement( 'li', null,
 +                                      self::wrap( $context->msg( "api-help-flag-$flag" ), "apihelp-flag-$flag" )
 +                              );
 +                      }
 +                      $sourceInfo = $module->getModuleSourceInfo();
 +                      if ( $sourceInfo ) {
 +                              if ( isset( $sourceInfo['namemsg'] ) ) {
 +                                      $extname = $context->msg( $sourceInfo['namemsg'] )->text();
 +                              } else {
 +                                      $extname = $sourceInfo['name'];
                                }
 -                              $help['flags'] .= Html::openElement( 'ul' );
 -                              foreach ( $flags as $flag ) {
 -                                      $help['flags'] .= Html::rawElement( 'li', null,
 -                                              self::wrap( $context->msg( "api-help-flag-$flag" ), "apihelp-flag-$flag" )
 -                                      );
 +                              $help['flags'] .= Html::rawElement( 'li', null,
 +                                      self::wrap(
 +                                              $context->msg( 'api-help-source', $extname, $sourceInfo['name'] ),
 +                                              'apihelp-source'
 +                                      )
 +                              );
 +
 +                              $link = SpecialPage::getTitleFor( 'Version', 'License/' . $sourceInfo['name'] );
 +                              if ( isset( $sourceInfo['license-name'] ) ) {
 +                                      $msg = $context->msg( 'api-help-license', $link, $sourceInfo['license-name'] );
 +                              } elseif ( SpecialVersion::getExtLicenseFileName( dirname( $sourceInfo['path'] ) ) ) {
 +                                      $msg = $context->msg( 'api-help-license-noname', $link );
 +                              } else {
 +                                      $msg = $context->msg( 'api-help-license-unknown' );
                                }
 -                              $help['flags'] .= Html::closeElement( 'ul' );
 -                              $help['flags'] .= Html::closeElement( 'div' );
 +                              $help['flags'] .= Html::rawElement( 'li', null,
 +                                      self::wrap( $msg, 'apihelp-license' )
 +                              );
 +                      } else {
 +                              $help['flags'] .= Html::rawElement( 'li', null,
 +                                      self::wrap( $context->msg( 'api-help-source-unknown' ), 'apihelp-source' )
 +                              );
 +                              $help['flags'] .= Html::rawElement( 'li', null,
 +                                      self::wrap( $context->msg( 'api-help-license-unknown' ), 'apihelp-license' )
 +                              );
                        }
 +                      $help['flags'] .= Html::closeElement( 'ul' );
 +                      $help['flags'] .= Html::closeElement( 'div' );
  
                        foreach ( $module->getFinalDescription() as $msg ) {
                                $msg->setContext( $context );
diff --combined includes/api/ApiMain.php
@@@ -89,7 -89,6 +89,7 @@@ class ApiMain extends ApiBase 
                'imagerotate' => 'ApiImageRotate',
                'revisiondelete' => 'ApiRevisionDelete',
                'managetags' => 'ApiManageTags',
 +              'tag' => 'ApiTag',
        );
  
        /**
         */
        private $mPrinter;
  
-       private $mModuleMgr, $mResult;
+       private $mModuleMgr, $mResult, $mErrorFormatter, $mContinuationManager;
        private $mAction;
        private $mEnableWrite;
        private $mInternalMode, $mSquidMaxage, $mModule;
  
                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()
                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()
         *
                // Bug 63145: Rollback any open database transactions
                if ( !( $e instanceof UsageException ) ) {
                        // UsageExceptions are intentional, so don't rollback if that's the case
 -                      MWExceptionHandler::rollbackMasterChangesAndLog( $e );
 +                      try {
 +                              MWExceptionHandler::rollbackMasterChangesAndLog( $e );
 +                      } catch ( DBError $e2 ) {
 +                              // Rollback threw an exception too. Log it, but don't interrupt
 +                              // our regularly scheduled exception handling.
 +                              MWExceptionHandler::logException( $e2 );
 +                      }
                }
  
                // Allow extra cleanup and logging
                        // 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' ) ) {
                                '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
                        $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
                }
  
-               $this->getResult()->cleanUpUTF8();
                $printer = $this->mPrinter;
                $printer->initPrinter( false );
                $printer->execute();
                $printer->closePrinter();
@@@ -105,7 -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'] ) {
                                        }
                                        $res[$key][] = $a;
                                }
-                               $this->getResult()->setIndexedTagName( $res[$key], 'msg' );
+                               ApiResult::setIndexedTagName( $res[$key], 'msg' );
                                break;
                }
        }
                }
                $ret['prefix'] = $module->getModulePrefix();
  
 +              $sourceInfo = $module->getModuleSourceInfo();
 +              if ( $sourceInfo ) {
 +                      $ret['source'] = $sourceInfo['name'];
 +                      if ( isset( $sourceInfo['namemsg'] ) ) {
 +                              $ret['sourcename'] = $this->context->msg( $sourceInfo['namemsg'] )->text();
 +                      } else {
 +                              $ret['sourcename'] = $ret['source'];
 +                      }
 +
 +                      $link = SpecialPage::getTitleFor( 'Version', 'License/' . $sourceInfo['name'] )->getFullUrl();
 +                      if ( isset( $sourceInfo['license-name'] ) ) {
 +                              $ret['licensetag'] = $sourceInfo['license-name'];
 +                              $ret['licenselink'] = (string)$link;
 +                      } elseif ( SpecialVersion::getExtLicenseFileName( dirname( $sourceInfo['path'] ) ) ) {
 +                              $ret['licenselink'] = (string)$link;
 +                      }
 +              }
 +
                $this->formatHelpMessages( $ret, 'description', $module->getFinalDescription() );
  
                foreach ( $module->getHelpFlags() as $flag ) {
                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();
                                        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();
                                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] ) ) {
                                        );
                                        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}" )
                                                        ->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;
        }
@@@ -107,10 -107,7 +107,10 @@@ class ApiParse extends ApiBase 
                                $popts = $this->makeParserOptions( $pageObj, $params );
  
                                // If for some reason the "oldid" is actually the current revision, it may be cached
 -                              if ( $rev->isCurrent() ) {
 +                              // Deliberately comparing $pageObj->getLatest() with $rev->getId(), rather than
 +                              // checking $rev->isCurrent(), because $pageObj is what actually ends up being used,
 +                              // and if its ->getLatest() is outdated, $rev->isCurrent() won't tell us that.
 +                              if ( $rev->getId() == $pageObj->getLatest() ) {
                                        // May get from/save to parser cache
                                        $p_result = $this->getParsedContent( $pageObj, $popts,
                                                $pageid, isset( $prop['wikitext'] ) );
                        } else { // Not $oldid, but $pageid or $page
                                if ( $params['redirects'] ) {
                                        $reqParams = array(
-                                               'action' => 'query',
                                                'redirects' => '',
                                        );
                                        if ( !is_null( $pageid ) ) {
                                        }
                                        $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 ) ) {
                                // 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 );
  
                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'] ) ) {
                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() );
  
                        if ( isset( $prop['headhtml'] ) ) {
                                $result_array['headhtml'] = array();
-                               ApiResult::setContent(
+                               ApiResult::setContentValue(
                                        $result_array['headhtml'],
+                                       'headhtml',
                                        $context->getOutput()->headElement( $context->getSkin() )
                                );
                        }
                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;
                        }
                }
  
                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'] ) ) {
                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'] ) {
                                $xml = $dom->__toString();
                        }
                        $result_array['parsetree'] = array();
-                       ApiResult::setContent( $result_array['parsetree'], $xml );
+                       ApiResult::setContentValue( $result_array['parsetree'], 'parsetree', $xml );
                }
  
                $result_mapping = array(
                                // native language name
                                $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
                        }
-                       ApiResult::setContent( $entry, $bits[1] );
+                       ApiResult::setContentValue( $entry, 'title', $bits[1] );
                        $result[] = $entry;
                }
  
                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] ) {
                        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'] = '';
                                }
                                        $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
                                }
  
-                               ApiResult::setContent( $entry, $title->getFullText() );
+                               ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
                                $result[] = $entry;
                        }
                }
                foreach ( $headItems as $tag => $content ) {
                        $entry = array();
                        $entry['tag'] = $tag;
-                       ApiResult::setContent( $entry, $content );
+                       ApiResult::setContentValue( $entry, 'content', $content );
                        $result[] = $entry;
                }
  
                foreach ( $properties as $name => $value ) {
                        $entry = array();
                        $entry['name'] = $name;
-                       ApiResult::setContent( $entry, $value );
+                       ApiResult::setContentValue( $entry, 'value', $value );
                        $result[] = $entry;
                }
  
                foreach ( $css as $file => $link ) {
                        $entry = array();
                        $entry['file'] = $file;
-                       ApiResult::setContent( $entry, $link );
+                       ApiResult::setContentValue( $entry, 'link', $link );
                        $result[] = $entry;
                }
  
                        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;
                }
        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 );
                        }
                }
        }
@@@ -29,8 -29,6 +29,8 @@@
   */
  class ApiProtect extends ApiBase {
        public function execute() {
 +              global $wgContLang;
 +
                $params = $this->extractRequestParams();
  
                $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
@@@ -80,7 -78,7 +80,7 @@@
                        }
  
                        if ( wfIsInfinity( $expiry[$i] ) ) {
 -                              $expiryarray[$p[0]] = $db->getInfinity();
 +                              $expiryarray[$p[0]] = 'infinity';
                        } else {
                                $exp = strtotime( $expiry[$i] );
                                if ( $exp < 0 || !$exp ) {
                        }
                        $resultProtections[] = array(
                                $p[0] => $protections[$p[0]],
 -                              'expiry' => ( $expiryarray[$p[0]] == $db->getInfinity()
 -                                      ? 'infinite'
 -                                      : wfTimestamp( TS_ISO_8601, $expiryarray[$p[0]] )
 -                              )
 +                              'expiry' => $wgContLang->formatExpiry( $expiryarray[$p[0]], TS_ISO_8601, 'infinite' ),
                        );
                }
  
                }
                $res['protections'] = $resultProtections;
                $result = $this->getResult();
-               $result->setIndexedTagName( $res['protections'], 'protection' );
+               ApiResult::setIndexedTagName( $res['protections'], 'protection' );
                $result->addValue( null, $this->getModuleName(), $res );
        }
  
@@@ -123,7 -123,6 +123,7 @@@ abstract class ApiQueryBase extends Api
         */
        public function selectNamedDB( $name, $db, $groups ) {
                $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups );
 +              return $this->mDb;
        }
  
        /**
         */
        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(),
                if ( !$fit ) {
                        return false;
                }
-               $result->setIndexedTagName_internal( array( 'query', 'pages', $pageId,
+               $result->addIndexedTagName( array( 'query', 'pages', $pageId,
                        $this->getModuleName() ), $elemname );
  
                return true;
         * @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 -710,7 +711,7 @@@ abstract class ApiQueryGeneratorBase ex
         */
        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 );
                }
@@@ -239,7 -239,7 +239,7 @@@ class ApiQueryLogEvents extends ApiQuer
                                break;
                        }
                }
-               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' );
+               $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'item' );
        }
  
        /**
                                $vals2['flags'] = isset( $params[$flagsKey] ) ? $params[$flagsKey] : '';
  
                                // Indefinite blocks have no expiry time
 -                              if ( SpecialBlock::parseExpiryInput( $params[$durationKey] ) !== wfGetDB( DB_SLAVE )->getInfinity() ) {
 +                              if ( SpecialBlock::parseExpiryInput( $params[$durationKey] ) !== 'infinity' ) {
                                        $vals2['expiry'] = wfTimestamp( TS_ISO_8601,
                                                strtotime( $params[$durationKey], wfTimestamp( TS_UNIX, $ts ) ) );
                                }
                                                unset( $params[$idsKey] );
                                        }
                                        if ( isset( $params[$ofieldKey] ) ) {
 -                                              $params[] = $params[$ofieldKey];
 +                                              $params[] = 'ofield=' . $params[$ofieldKey];
                                                unset( $params[$ofieldKey] );
                                        }
                                        if ( isset( $params[$nfieldKey] ) ) {
 -                                              $params[] = $params[$nfieldKey];
 +                                              $params[] = 'nfield=' . $params[$nfieldKey];
                                                unset( $params[$nfieldKey] );
                                        }
                                }
                                $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 );
                }
  
                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();
@@@ -158,7 -158,7 +158,7 @@@ class ApiQuerySiteinfo extends ApiQuery
                }
                if ( $allowException ) {
                        $data['externalimages'] = (array)$allowFrom;
-                       $this->getResult()->setIndexedTagName( $data['externalimages'], 'prefix' );
+                       ApiResult::setIndexedTagName( $data['externalimages'], 'prefix' );
                }
  
                if ( !$config->get( 'DisableLangConversion' ) ) {
                        $fallbacks[] = array( 'code' => $code );
                }
                $data['fallback'] = $fallbacks;
-               $this->getResult()->setIndexedTagName( $data['fallback'], 'lang' );
+               ApiResult::setIndexedTagName( $data['fallback'], 'lang' );
  
                if ( $wgContLang->hasVariants() ) {
                        $variants = array();
                                );
                        }
                        $data['variants'] = $variants;
-                       $this->getResult()->setIndexedTagName( $data['variants'], 'lang' );
+                       ApiResult::setIndexedTagName( $data['variants'], 'lang' );
                }
  
                if ( $wgContLang->isRTL() ) {
                $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] );
                }
                                '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 ) ) {
                        }
                }
  
-               $this->getResult()->setIndexedTagName( $data, 'ns' );
+               ApiResult::setIndexedTagName( $data, 'ns' );
  
                return $this->getResult()->addValue( 'query', $property, $data );
        }
                        $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 );
        }
                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 );
        }
                        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 );
        }
                        $data[] = $val;
                }
  
-               $this->getResult()->setIndexedTagName( $data, 'iw' );
+               ApiResult::setIndexedTagName( $data, 'iw' );
  
                return $this->getResult()->addValue( 'query', $property, $data );
        }
                }
  
                $result = $this->getResult();
-               $result->setIndexedTagName( $data, 'db' );
+               ApiResult::setIndexedTagName( $data, 'db' );
  
                return $this->getResult()->addValue( 'query', $property, $data );
        }
                                        $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 );
        }
                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 );
        }
                                'version' => $info['version'],
                        );
                }
-               $this->getResult()->setIndexedTagName( $data, 'library' );
+               ApiResult::setIndexedTagName( $data, 'library' );
  
                return $this->getResult()->addValue( 'query', $property, $data );
  
                                        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'];
                                        }
                        }
                }
  
-               $this->getResult()->setIndexedTagName( $data, 'ext' );
+               ApiResult::setIndexedTagName( $data, 'ext' );
  
                return $this->getResult()->addValue( 'query', $property, $data );
        }
  
        protected function appendRightsInfo( $property ) {
                $config = $this->getConfig();
 -              $title = Title::newFromText( $config->get( 'RightsPage' ) );
 -              $url = $title ? wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ) : $config->get( 'RightsUrl' );
 +              $rightsPage = $config->get( 'RightsPage' );
 +              if ( is_string( $rightsPage ) ) {
 +                      $title = Title::newFromText( $rightsPage );
 +                      $url = wfExpandUrl( $title, PROTO_CURRENT );
 +              } else {
 +                      $title = false;
 +                      $url = $config->get( 'RightsUrl' );
 +              }
                $text = $config->get( 'RightsText' );
                if ( !$text && $title ) {
                        $text = $title->getPrefixedText();
                        '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 );
        }
  
                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 );
        }
                                $displayName = $msg->text();
                        }
                        $skin = array( 'code' => $name );
-                       ApiResult::setContent( $skin, $displayName );
+                       ApiResult::setContentValue( $skin, 'name', $displayName );
                        if ( !isset( $allowed[$name] ) ) {
                                $skin['unusable'] = '';
                        }
                        }
                        $data[] = $skin;
                }
-               $this->getResult()->setIndexedTagName( $data, 'skin' );
+               ApiResult::setIndexedTagName( $data, 'skin' );
  
                return $this->getResult()->addValue( 'query', $property, $data );
        }
                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 );
        }
                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 );
        }
        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 );
        }
                                '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 );
        }
@@@ -52,8 -52,6 +52,8 @@@ class ApiQueryUserInfo extends ApiQuery
        }
  
        protected function getCurrentUserInfo() {
 +              global $wgContLang;
 +
                $user = $this->getUser();
                $result = $this->getResult();
                $vals = array();
@@@ -72,9 -70,9 +72,9 @@@
                                $vals['blockedbyid'] = $block->getBy();
                                $vals['blockreason'] = $user->blockedFor();
                                $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->mTimestamp );
 -                              $vals['blockexpiry'] = $block->getExpiry() === 'infinity'
 -                                      ? 'infinite'
 -                                      : wfTimestamp( TS_ISO_8601, $block->getExpiry() );
 +                              $vals['blockexpiry'] = $wgContLang->formatExpiry(
 +                                      $block->getExpiry(), TS_ISO_8601, 'infinite'
 +                              );
                        }
                }
  
  
                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'] ) ) {
                        $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;
                }
  
@@@ -85,7 -85,6 +85,7 @@@
        "apihelp-edit-param-sectiontitle": "The title for a new section.",
        "apihelp-edit-param-text": "Page content.",
        "apihelp-edit-param-summary": "Edit summary. Also section title when $1section=new and $1sectiontitle is not set.",
 +      "apihelp-edit-param-tags": "Change tags to apply to the revision.",
        "apihelp-edit-param-minor": "Minor edit.",
        "apihelp-edit-param-notminor": "Non-minor edit.",
        "apihelp-edit-param-bot": "Mark this edit as bot.",
        "apihelp-setnotificationtimestamp-example-pagetimestamp": "Set the notification timestamp for <kbd>Main page</kbd> so all edits since 1 January 2012 are unviewed.",
        "apihelp-setnotificationtimestamp-example-allpages": "Reset the notification status for pages in the <kbd>{{ns:user}}</kbd> namespace.",
  
 +      "apihelp-tag-description": "Add or remove change tags from individual revisions or log entries.",
 +      "apihelp-tag-param-rcid": "One or more recent changes IDs from which to add or remove the tag.",
 +      "apihelp-tag-param-revid": "One or more revision IDs from which to add or remove the tag.",
 +      "apihelp-tag-param-logid": "One or more log entry IDs from which to add or remove the tag.",
 +      "apihelp-tag-param-add": "Tags to add. Only manually defined tags can be added.",
 +      "apihelp-tag-param-remove": "Tags to remove. Only tags that are either manually defined or completely undefined can be removed.",
 +      "apihelp-tag-param-reason": "Reason for the change.",
 +      "apihelp-tag-example-rev": "Add the <kbd>vandalism</kbd> tag from revision ID 123 without specifying a reason",
 +      "apihelp-tag-example-log": "Remove the <kbd>spam</kbd> tag from log entry ID 123 with the reason <kbd>Wrongly applied</kbd>",
 +
        "apihelp-tokens-description": "Get tokens for data-modifying actions.\n\nThis module is deprecated in favor of [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
        "apihelp-tokens-param-type": "Types of token to request.",
        "apihelp-tokens-example-edit": "Retrieve an edit token (the default).",
        "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.",
        "api-help-flag-writerights": "This module requires write rights.",
        "api-help-flag-mustbeposted": "This module only accepts POST requests.",
        "api-help-flag-generator": "This module can be used as a generator.",
 +      "api-help-source": "Source: $1",
 +      "api-help-source-unknown": "Source: <span class=\"apihelp-unknown\">unknown</span>",
 +      "api-help-license": "License: [[$1|$2]]",
 +      "api-help-license-noname": "License: [[$1|See link]]",
 +      "api-help-license-unknown": "License: <span class=\"apihelp-unknown\">unknown</span>",
        "api-help-help-urls": "",
        "api-help-parameters": "{{PLURAL:$1|Parameter|Parameters}}:",
        "api-help-param-deprecated": "Deprecated.",
@@@ -81,7 -81,6 +81,7 @@@
        "apihelp-edit-param-sectiontitle": "{{doc-apihelp-param|edit|sectiontitle}}",
        "apihelp-edit-param-text": "{{doc-apihelp-param|edit|text}}",
        "apihelp-edit-param-summary": "{{doc-apihelp-param|edit|summary}}",
 +      "apihelp-edit-param-tags": "{{doc-apihelp-param|edit|tags}}",
        "apihelp-edit-param-minor": "{{doc-apihelp-param|edit|minor}}\n{{Identical|Minor edit}}",
        "apihelp-edit-param-notminor": "{{doc-apihelp-param|edit|notminor}}",
        "apihelp-edit-param-bot": "{{doc-apihelp-param|edit|bot}}",
        "apihelp-setnotificationtimestamp-example-page": "{{doc-apihelp-example|setnotificationtimestamp}}",
        "apihelp-setnotificationtimestamp-example-pagetimestamp": "{{doc-apihelp-example|setnotificationtimestamp}}",
        "apihelp-setnotificationtimestamp-example-allpages": "{{doc-apihelp-example|setnotificationtimestamp}}",
 +      "apihelp-tag-description": "{{doc-apihelp-description|tag}}",
 +      "apihelp-tag-param-rcid": "{{doc-apihelp-param|tag|rcid}}",
 +      "apihelp-tag-param-revid": "{{doc-apihelp-param|tag|revid}}",
 +      "apihelp-tag-param-logid": "{{doc-apihelp-param|tag|logid}}",
 +      "apihelp-tag-param-add": "{{doc-apihelp-param|tag|add}}",
 +      "apihelp-tag-param-remove": "{{doc-apihelp-param|tag|remove}}",
 +      "apihelp-tag-param-reason": "{{doc-apihelp-param|tag|reason}}",
 +      "apihelp-tag-example-rev": "{{doc-apihelp-example|tag}}",
 +      "apihelp-tag-example-log": "{{doc-apihelp-example|tag}}",
        "apihelp-tokens-description": "{{doc-apihelp-description|tokens}}",
        "apihelp-tokens-param-type": "{{doc-apihelp-param|tokens|type}}",
        "apihelp-tokens-example-edit": "{{doc-apihelp-example|tokens}}",
        "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}}}}",
        "api-help-flag-writerights": "Flag displayed for an API module that requires write rights",
        "api-help-flag-mustbeposted": "Flag displayed for an API module that only accepts POST requests",
        "api-help-flag-generator": "Flag displayed for an API module that can be used as a generator",
 +      "api-help-source": "Displayed in the flags box to indicate the source of an API module.\n\nParameters:\n* $1 - Possibly-localised extension name, or \"MediaWiki\" if it's a core module\n* $2 - Non-localised extension name.\n\nSee also:\n* {{msg-mw|api-help-source-unknown}}",
 +      "api-help-source-unknown": "Displayed in the flags box to indicate that the source of an API module is not known.\n\nSee also:\n* {{msg-mw|api-help-source}}",
 +      "api-help-license": "Displayed in the flags box to indicate the license of an API module.\n\nParameters:\n* $1 - Page to link to display the full license text\n* $2 - Display text for the link\n\nSee also:\n* {{msg-mw|api-help-license-noname}}\n* {{msg-mw|api-help-license-unknown}}",
 +      "api-help-license-noname": "Displayed in the flags box to indicate the license of an API module, when the tag for the license is not known.\n\nParameters:\n* $1 - Page to link to display the full license text\n\nSee also:\n* {{msg-mw|api-help-license}}\n* {{msg-mw|api-help-license-unknown}}",
 +      "api-help-license-unknown": "Displayed in the flags box to indicate that the license of the API module is not known.\n\nSee also:\n* {{msg-mw|api-help-license}}\n* {{msg-mw|api-help-license-noname}}",
        "api-help-help-urls": "{{optional}} Label for the API help urls section\n\nParameters:\n* $1 - Number of urls to be displayed",
        "api-help-parameters": "Label for the API help parameters section\n\nParameters:\n* $1 - Number of parameters to be displayed\n{{Identical|Parameter}}",
        "api-help-param-deprecated": "Displayed in the API help for any deprecated parameter\n{{Identical|Deprecated}}",