From 8929182a67ef9144f4cb9afedffe27aafca94469 Mon Sep 17 00:00:00 2001 From: Marc Ordinas i Llopis Date: Thu, 4 Jun 2015 03:32:23 -0700 Subject: [PATCH] Expose RL modules and js config vars in action=expandtemplates 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 | 30 +++++++- includes/api/ApiParse.php | 73 ++++++-------------- includes/api/ApiResult.php | 70 +++++++++++++++++-- includes/api/i18n/en.json | 13 +++- includes/api/i18n/qqq.json | 11 ++- tests/phpunit/includes/api/ApiResultTest.php | 70 +++++++++++++++++++ 6 files changed, 205 insertions(+), 62 deletions(-) diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index be1a695f8b..966af5306d 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -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( diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 36be7777b6..1833434e32 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -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] ) ) { diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 7c573a8f75..f0c7430140 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -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; + } + } + /**@}*/ /************************************************************************//** diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index f3faf4897e..2a2e855a06 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -121,7 +121,16 @@ "apihelp-expandtemplates-param-title": "Title of page.", "apihelp-expandtemplates-param-text": "Wikitext to convert.", "apihelp-expandtemplates-param-revid": "Revision ID, for {{REVISIONID}} 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 jsconfigvars or encodedjsconfigvars must be requested jointly with modules.", + "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 {{Project:Sandbox}}.", @@ -286,7 +295,7 @@ "apihelp-parse-paramvalue-prop-displaytitle": "Adds the title of the parsed wikitext.", "apihelp-parse-paramvalue-prop-headitems": "Gives items to put in the <head> of the page.", "apihelp-parse-paramvalue-prop-headhtml": "Gives parsed <head> 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 jsconfigvars or encodedjsconfigvars must be requested jointly with modules.", "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.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index c6ac8fd6b3..2cf7aa6945 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -116,7 +116,16 @@ "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 <nowiki>{{REVISIONID}}</nowiki>}}", - "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}}", diff --git a/tests/phpunit/includes/api/ApiResultTest.php b/tests/phpunit/includes/api/ApiResultTest.php index f0d8455278..1ed571c2ff 100644 --- a/tests/phpunit/includes/api/ApiResultTest.php +++ b/tests/phpunit/includes/api/ApiResultTest.php @@ -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 */ -- 2.20.1