Expose RL modules and js config vars in action=expandtemplates
authorMarc Ordinas i Llopis <marcoil@wikimedia.org>
Thu, 4 Jun 2015 10:32:23 +0000 (03:32 -0700)
committerMarc Ordinas i Llopis <marcoil@wikimedia.org>
Fri, 5 Jun 2015 16:04:47 +0000 (09:04 -0700)
Adds the 'modules', 'jsconfigvars', and 'encodedjsconfigvars' props
to action=expandtemplates, that output the modules and Javascript
configuration variables added to ResourceLoader by extensions and
parser functions, in the same way action=parse does.

This is needed by Parsoid to correctly include all modules used by
parser functions.

Based on I5c3ccb25385e57633639bb0c7e6f562eb58b05a2 by @Jackmcbarn.

Bug: T69540
Change-Id: Iaf58c66c987a318c0dd1ee2b81774106c40e7561

includes/api/ApiExpandTemplates.php
includes/api/ApiParse.php
includes/api/ApiResult.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
tests/phpunit/includes/api/ApiResultTest.php

index be1a695..966af53 100644 (file)
@@ -111,8 +111,9 @@ class ApiExpandTemplates extends ApiBase {
                                // the old way
                                ApiResult::setContentValue( $retval, 'wikitext', $wikitext );
                        } else {
+                               $p_output = $wgParser->getOutput();
                                if ( isset( $prop['categories'] ) ) {
-                                       $categories = $wgParser->getOutput()->getCategories();
+                                       $categories = $p_output->getCategories();
                                        if ( $categories ) {
                                                $categories_result = array();
                                                foreach ( $categories as $category => $sortkey ) {
@@ -126,7 +127,7 @@ class ApiExpandTemplates extends ApiBase {
                                        }
                                }
                                if ( isset( $prop['properties'] ) ) {
-                                       $properties = $wgParser->getOutput()->getProperties();
+                                       $properties = $p_output->getProperties();
                                        if ( $properties ) {
                                                ApiResult::setArrayType( $properties, 'BCkvp', 'name' );
                                                ApiResult::setIndexedTagName( $properties, 'property' );
@@ -142,6 +143,27 @@ class ApiExpandTemplates extends ApiBase {
                                if ( isset( $prop['wikitext'] ) ) {
                                        $retval['wikitext'] = $wikitext;
                                }
+                               if ( isset( $prop['modules'] ) ) {
+                                       $retval['modules'] = array_values( array_unique( $p_output->getModules() ) );
+                                       $retval['modulescripts'] = array_values( array_unique( $p_output->getModuleScripts() ) );
+                                       $retval['modulestyles'] = array_values( array_unique( $p_output->getModuleStyles() ) );
+                               }
+                               if ( isset( $prop['jsconfigvars'] ) ) {
+                                       $retval['jsconfigvars'] =
+                                               ApiResult::addMetadataToResultVars( $p_output->getJsConfigVars() );
+                               }
+                               if ( isset( $prop['encodedjsconfigvars'] ) ) {
+                                       $retval['encodedjsconfigvars'] = FormatJson::encode(
+                                               $p_output->getJsConfigVars(), false, FormatJson::ALL_OK
+                                       );
+                                       $retval[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
+                               }
+                               if ( isset( $prop['modules'] ) &&
+                                       !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
+                                       $this->setWarning( "Property 'modules' was set but not 'jsconfigvars' " .
+                                               "or 'encodedjsconfigvars'. Configuration variables are necessary " .
+                                               "for proper module usage.");
+                               }
                        }
                }
                ApiResult::setSubelementsList( $retval, array( 'wikitext', 'parsetree' ) );
@@ -167,9 +189,13 @@ class ApiExpandTemplates extends ApiBase {
                                        'properties',
                                        'volatile',
                                        'ttl',
+                                       'modules',
+                                       'jsconfigvars',
+                                       'encodedjsconfigvars',
                                        'parsetree',
                                ),
                                ApiBase::PARAM_ISMULTI => true,
+                               ApiBase::PARAM_HELP_MSG_PER_VALUE => array(),
                        ),
                        'includecomments' => false,
                        'generatexml' => array(
index 36be777..1833434 100644 (file)
@@ -71,7 +71,9 @@ class ApiParse extends ApiBase {
                if ( isset( $params['section'] ) ) {
                        $this->section = $params['section'];
                        if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
-                               $this->dieUsage( "The section parameter must be a valid section id or 'new'", "invalidsection" );
+                               $this->dieUsage(
+                                       "The section parameter must be a valid section id or 'new'", "invalidsection"
+                               );
                        }
                } else {
                        $this->section = false;
@@ -89,7 +91,10 @@ class ApiParse extends ApiBase {
 
                if ( !is_null( $oldid ) || !is_null( $pageid ) || !is_null( $page ) ) {
                        if ( $this->section === 'new' ) {
-                                       $this->dieUsage( 'section=new cannot be combined with oldid, pageid or page parameters. Please use text', 'params' );
+                                       $this->dieUsage(
+                                               'section=new cannot be combined with oldid, pageid or page parameters. ' .
+                                               'Please use text', 'params'
+                                       );
                        }
                        if ( !is_null( $oldid ) ) {
                                // Don't use the parser cache
@@ -356,7 +361,8 @@ class ApiParse extends ApiBase {
                }
 
                if ( isset( $prop['jsconfigvars'] ) ) {
-                       $result_array['jsconfigvars'] = $this->formatJsConfigVars( $p_result->getJsConfigVars() );
+                       $result_array['jsconfigvars'] =
+                               ApiResult::addMetadataToResultVars( $p_result->getJsConfigVars() );
                }
 
                if ( isset( $prop['encodedjsconfigvars'] ) ) {
@@ -366,6 +372,13 @@ class ApiParse extends ApiBase {
                        $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
                }
 
+               if ( isset( $prop['modules'] ) &&
+                       !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
+                       $this->setWarning( "Property 'modules' was set but not 'jsconfigvars' " .
+                               "or 'encodedjsconfigvars'. Configuration variables are necessary " .
+                               "for proper module usage.");
+               }
+
                if ( isset( $prop['indicators'] ) ) {
                        $result_array['indicators'] = (array)$p_result->getIndicators();
                        ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
@@ -520,12 +533,13 @@ class ApiParse extends ApiBase {
                $sectionTitle = !is_null( $params['sectiontitle'] ) ? $params['sectiontitle'] : '';
 
                if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
-                       if( $sectionTitle !== '' ) {
+                       if ( $sectionTitle !== '' ) {
                                $summary = $params['sectiontitle'];
                        }
                        if ( $summary !== '' ) {
-                               $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
-                                       ->inContentLanguage()->text();
+                               $summary = wfMessage( 'newsectionsummary' )
+                                       ->rawParams( $wgParser->stripSectionName( $summary ) )
+                                               ->inContentLanguage()->text();
                        }
                }
                return Linker::formatComment( $summary, $title, $this->section === 'new' );
@@ -681,53 +695,6 @@ class ApiParse extends ApiBase {
                return $result;
        }
 
-       private function formatJsConfigVars( $vars, $forceHash = true ) {
-               // Process subarrays and determine if this is a JS [] or {}
-               $hash = $forceHash;
-               $maxKey = -1;
-               $bools = array();
-               foreach ( $vars as $k => $v ) {
-                       if ( is_array( $v ) || is_object( $v ) ) {
-                               $vars[$k] = $this->formatJsConfigVars( (array)$v, false );
-                       } elseif ( is_bool( $v ) ) {
-                               // Better here to use real bools even in BC formats
-                               $bools[] = $k;
-                       }
-                       if ( is_string( $k ) ) {
-                               $hash = true;
-                       } elseif ( $k > $maxKey ) {
-                               $maxKey = $k;
-                       }
-               }
-               if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
-                       $hash = true;
-               }
-
-               // Get the list of keys we actually care about. Unfortunately, we can't support
-               // certain keys that conflict with ApiResult metadata.
-               $keys = array_diff( array_keys( $vars ), array(
-                       ApiResult::META_TYPE, ApiResult::META_PRESERVE_KEYS, ApiResult::META_KVP_KEY_NAME,
-                       ApiResult::META_INDEXED_TAG_NAME, ApiResult::META_BC_BOOLS
-               ) );
-
-               // Set metadata appropriately
-               if ( $hash ) {
-                       return array(
-                               ApiResult::META_TYPE => 'kvp',
-                               ApiResult::META_KVP_KEY_NAME => 'key',
-                               ApiResult::META_PRESERVE_KEYS => $keys,
-                               ApiResult::META_BC_BOOLS => $bools,
-                               ApiResult::META_INDEXED_TAG_NAME => 'var',
-                       ) + $vars;
-               } else {
-                       return array(
-                               ApiResult::META_TYPE => 'array',
-                               ApiResult::META_BC_BOOLS => $bools,
-                               ApiResult::META_INDEXED_TAG_NAME => 'value',
-                       ) + $vars;
-               }
-       }
-
        private function setIndexedTagNames( &$array, $mapping ) {
                foreach ( $mapping as $key => $name ) {
                        if ( isset( $array[$key] ) ) {
index 7c573a8..f0c7430 100644 (file)
@@ -302,10 +302,14 @@ class ApiResult implements ApiSerializable {
                                $arr[$name] += $value;
                        } else {
                                $keys = join( ', ', array_keys( $conflicts ) );
-                               throw new RuntimeException( "Conflicting keys ($keys) when attempting to merge element $name" );
+                               throw new RuntimeException(
+                                       "Conflicting keys ($keys) when attempting to merge element $name"
+                               );
                        }
                } else {
-                       throw new RuntimeException( "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]}"
+                       );
                }
        }
 
@@ -703,7 +707,9 @@ class ApiResult implements ApiSerializable {
         * @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 ) ) {
+               if ( !in_array( $type, array(
+                               'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
+                               ), true ) ) {
                        throw new InvalidArgumentException( 'Bad type' );
                }
                $arr[self::META_TYPE] = $type;
@@ -1035,7 +1041,8 @@ class ApiResult implements ApiSerializable {
        /**
         * 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.
+        * @note Once the deprecated public self::size is removed, we can rename
+        *       this back to a less awkward name.
         * @param mixed $value
         * @return int
         */
@@ -1096,6 +1103,61 @@ class ApiResult implements ApiSerializable {
                return $ret;
        }
 
+       /**
+        * Add the correct metadata to an array of vars we want to export through
+        * the API.
+        *
+        * @param array $vars
+        * @param boolean $forceHash
+        * @return array
+        */
+       public static function addMetadataToResultVars( $vars, $forceHash = true ) {
+               // Process subarrays and determine if this is a JS [] or {}
+               $hash = $forceHash;
+               $maxKey = -1;
+               $bools = array();
+               foreach ( $vars as $k => $v ) {
+                       if ( is_array( $v ) || is_object( $v ) ) {
+                               $vars[$k] = ApiResult::addMetadataToResultVars( (array)$v, is_object( $v ) );
+                       } elseif ( is_bool( $v ) ) {
+                               // Better here to use real bools even in BC formats
+                               $bools[] = $k;
+                       }
+                       if ( is_string( $k ) ) {
+                               $hash = true;
+                       } elseif ( $k > $maxKey ) {
+                               $maxKey = $k;
+                       }
+               }
+               if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
+                       $hash = true;
+               }
+
+               // Set metadata appropriately
+               if ( $hash ) {
+                       // Get the list of keys we actually care about. Unfortunately, we can't support
+                       // certain keys that conflict with ApiResult metadata.
+                       $keys = array_diff( array_keys( $vars ), array(
+                               ApiResult::META_TYPE, ApiResult::META_PRESERVE_KEYS, ApiResult::META_KVP_KEY_NAME,
+                               ApiResult::META_INDEXED_TAG_NAME, ApiResult::META_BC_BOOLS
+                       ) );
+
+                       return array(
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                               ApiResult::META_PRESERVE_KEYS => $keys,
+                               ApiResult::META_BC_BOOLS => $bools,
+                               ApiResult::META_INDEXED_TAG_NAME => 'var',
+                       ) + $vars;
+               } else {
+                       return array(
+                               ApiResult::META_TYPE => 'array',
+                               ApiResult::META_BC_BOOLS => $bools,
+                               ApiResult::META_INDEXED_TAG_NAME => 'value',
+                       ) + $vars;
+               }
+       }
+
        /**@}*/
 
        /************************************************************************//**
index f3faf48..2a2e855 100644 (file)
        "apihelp-expandtemplates-param-title": "Title of page.",
        "apihelp-expandtemplates-param-text": "Wikitext to convert.",
        "apihelp-expandtemplates-param-revid": "Revision ID, for <nowiki>{{REVISIONID}}</nowiki> and similar variables.",
-       "apihelp-expandtemplates-param-prop": "Which pieces of information to get:\n;wikitext:The expanded wikitext.\n;categories:Any categories present in the input that are not represented in the wikitext output.\n;properties:Page properties defined by expanded magic words in the wikitext.\n;volatile:Whether the output is volatile and should not be reused elsewhere within the page.\n;ttl:The maximum time after which caches of the result should be invalidated.\n;parsetree:The XML parse tree of the input.\nNote that if no values are selected, the result will contain the wikitext, but the output will be in a deprecated format.",
+       "apihelp-expandtemplates-param-prop": "Which pieces of information to get.\n\nNote that if no values are selected, the result will contain the wikitext, but the output will be in a deprecated format.",
+       "apihelp-expandtemplates-paramvalue-prop-wikitext": "The expanded wikitext.",
+       "apihelp-expandtemplates-paramvalue-prop-categories": "Any categories present in the input that are not represented in the wikitext output.",
+       "apihelp-expandtemplates-paramvalue-prop-properties": "Page properties defined by expanded magic words in the wikitext.",
+       "apihelp-expandtemplates-paramvalue-prop-volatile": "Whether the output is volatile and should not be reused elsewhere within the page.",
+       "apihelp-expandtemplates-paramvalue-prop-ttl": "The maximum time after which caches of the result should be invalidated.",
+       "apihelp-expandtemplates-paramvalue-prop-modules": "Any ResourceLoader modules that parser functions have requested be added to the output. Either <kbd>jsconfigvars</kbd> or <kbd>encodedjsconfigvars</kbd> must be requested jointly with <kbd>modules</kbd>.",
+       "apihelp-expandtemplates-paramvalue-prop-jsconfigvars": "Gives the JavaScript configuration variables specific to the page.",
+       "apihelp-expandtemplates-paramvalue-prop-encodedjsconfigvars": "Gives the JavaScript configuration variables specific to the page as a JSON string.",
+       "apihelp-expandtemplates-paramvalue-prop-parsetree": "The XML parse tree of the input.",
        "apihelp-expandtemplates-param-includecomments": "Whether to include HTML comments in the output.",
        "apihelp-expandtemplates-param-generatexml": "Generate XML parse tree (replaced by $1prop=parsetree).",
        "apihelp-expandtemplates-example-simple": "Expand the wikitext <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.",
        "apihelp-parse-paramvalue-prop-displaytitle": "Adds the title of the parsed wikitext.",
        "apihelp-parse-paramvalue-prop-headitems": "Gives items to put in the <code>&lt;head&gt;</code> of the page.",
        "apihelp-parse-paramvalue-prop-headhtml": "Gives parsed <code>&lt;head&gt;</code> of the page.",
-       "apihelp-parse-paramvalue-prop-modules": "Gives the ResourceLoader modules used on the page.",
+       "apihelp-parse-paramvalue-prop-modules": "Gives the ResourceLoader modules used on the page. Either <kbd>jsconfigvars</kbd> or <kbd>encodedjsconfigvars</kbd> must be requested jointly with <kbd>modules</kbd>.",
        "apihelp-parse-paramvalue-prop-jsconfigvars": "Gives the JavaScript configuration variables specific to the page.",
        "apihelp-parse-paramvalue-prop-encodedjsconfigvars": "Gives the JavaScript configuration variables specific to the page as a JSON string.",
        "apihelp-parse-paramvalue-prop-indicators": "Gives the HTML of page status indicators used on the page.",
index c6ac8fd..2cf7aa6 100644 (file)
        "apihelp-expandtemplates-param-title": "{{doc-apihelp-param|expandtemplates|title}}",
        "apihelp-expandtemplates-param-text": "{{doc-apihelp-param|expandtemplates|text}}",
        "apihelp-expandtemplates-param-revid": "{{doc-apihelp-param|expandtemplates|revid}}\n{{doc-important|Do not translate <code><<nowiki />nowiki>{{<nowiki />REVISIONID}}<<nowiki />/nowiki></code>}}",
-       "apihelp-expandtemplates-param-prop": "{{doc-apihelp-param|expandtemplates|prop}}",
+       "apihelp-expandtemplates-param-prop": "{{doc-apihelp-param|expandtemplates|prop|paramvalues=1}}",
+       "apihelp-expandtemplates-paramvalue-prop-wikitext": "{{doc-apihelp-paramvalue|expandtemplates|prop|wikitext}}",
+       "apihelp-expandtemplates-paramvalue-prop-categories": "{{doc-apihelp-paramvalue|expandtemplates|prop|categories}}",
+       "apihelp-expandtemplates-paramvalue-prop-properties": "{{doc-apihelp-paramvalue|expandtemplates|prop|properties}}",
+       "apihelp-expandtemplates-paramvalue-prop-volatile": "{{doc-apihelp-paramvalue|expandtemplates|prop|volatile}}",
+       "apihelp-expandtemplates-paramvalue-prop-ttl": "{{doc-apihelp-paramvalue|expandtemplates|prop|ttl}}",
+       "apihelp-expandtemplates-paramvalue-prop-modules": "{{doc-apihelp-paramvalue|expandtemplates|prop|modules}}",
+       "apihelp-expandtemplates-paramvalue-prop-jsconfigvars": "{{doc-apihelp-paramvalue|expandtemplates|prop|jsconfigvars}}",
+       "apihelp-expandtemplates-paramvalue-prop-encodedjsconfigvars": "{{doc-apihelp-paramvalue|expandtemplates|prop|encodedjsconfigvars}}",
+       "apihelp-expandtemplates-paramvalue-prop-parsetree": "{{doc-apihelp-paramvalue|expandtemplates|prop|parsetree}}",
        "apihelp-expandtemplates-param-includecomments": "{{doc-apihelp-param|expandtemplates|includecomments}}",
        "apihelp-expandtemplates-param-generatexml": "{{doc-apihelp-param|expandtemplates|generatexml}}",
        "apihelp-expandtemplates-example-simple": "{{doc-apihelp-example|expandtemplates}}",
index f0d8455..1ed571c 100644 (file)
@@ -1124,6 +1124,76 @@ class ApiResultTest extends MediaWikiTestCase {
                $data[ApiResult::META_CONTENT] = 'bar';
        }
 
+       /**
+        * @covers ApiResult
+        */
+       public function testAddMetadataToResultVars() {
+               $arr = array(
+                       'a' => "foo",
+                       'b' => false,
+                       'c' => 10,
+                       'sequential_numeric_keys' => array( 'a', 'b', 'c' ),
+                       'non_sequential_numeric_keys' => array( 'a', 'b', 4 => 'c' ),
+                       'string_keys' => array(
+                               'one' => 1,
+                               'two' => 2
+                       ),
+                       'object_sequential_keys' => (object)array( 'a', 'b', 'c' ),
+                       '_type' => "should be overwritten in result",
+               );
+               $this->assertSame( array(
+                       ApiResult::META_TYPE => 'kvp',
+                       ApiResult::META_KVP_KEY_NAME => 'key',
+                       ApiResult::META_PRESERVE_KEYS => array(
+                               'a', 'b', 'c',
+                               'sequential_numeric_keys', 'non_sequential_numeric_keys',
+                               'string_keys', 'object_sequential_keys'
+                       ),
+                       ApiResult::META_BC_BOOLS => array( 'b' ),
+                       ApiResult::META_INDEXED_TAG_NAME => 'var',
+                       'a' => "foo",
+                       'b' => false,
+                       'c' => 10,
+                       'sequential_numeric_keys' => array(
+                               ApiResult::META_TYPE => 'array',
+                               ApiResult::META_BC_BOOLS => array(),
+                               ApiResult::META_INDEXED_TAG_NAME => 'value',
+                               0 => 'a',
+                               1 => 'b',
+                               2 => 'c',
+                       ),
+                       'non_sequential_numeric_keys' => array(
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                               ApiResult::META_PRESERVE_KEYS => array( 0, 1, 4 ),
+                               ApiResult::META_BC_BOOLS => array(),
+                               ApiResult::META_INDEXED_TAG_NAME => 'var',
+                               0 => 'a',
+                               1 => 'b',
+                               4 => 'c',
+                       ),
+                       'string_keys' => array(
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                               ApiResult::META_PRESERVE_KEYS => array( 'one', 'two' ),
+                               ApiResult::META_BC_BOOLS => array(),
+                               ApiResult::META_INDEXED_TAG_NAME => 'var',
+                               'one' => 1,
+                               'two' => 2,
+                       ),
+                       'object_sequential_keys' => array(
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                               ApiResult::META_PRESERVE_KEYS => array( 0, 1, 2 ),
+                               ApiResult::META_BC_BOOLS => array(),
+                               ApiResult::META_INDEXED_TAG_NAME => 'var',
+                               0 => 'a',
+                               1 => 'b',
+                               2 => 'c',
+                       ),
+               ), ApiResult::addMetadataToResultVars( $arr ) );
+       }
+
        /**
         * @covers ApiResult
         */