* …
=== Action API changes in 1.32 ===
-* …
+* Added templated parameters.
+ * A module can define a templated parameter like "{fruit}-quantity", where
+ the actual parameters recognized correspond to the values of a multi-valued
+ parameter. Then clients can make requests like
+ "fruits=apples|bananas&apples-quantity=1&bananas-quantity=5".
+ * action=paraminfo will return templated parameter definitions separately
+ from normal parameters. All parameter definitions now include an "index"
+ key to allow clients to maintain parameter ordering when merging normal and
+ templated parameters.
=== Action API internal changes in 1.32 ===
* Added 'ApiParseMakeOutputPage' hook.
+* Parameter names may no longer contain '{' or '}', as these are now used for
+ templated parameters.
=== Languages updated in 1.32 ===
MediaWiki supports over 350 languages. Many localisations are updated regularly.
*/
const PARAM_MAX_CHARS = 24;
+ /**
+ * (array) Indicate that this is a templated parameter, and specify replacements. Keys are the
+ * placeholders in the parameter name and values are the names of (unprefixed) parameters from
+ * which the replacement values are taken.
+ *
+ * For example, a parameter "foo-{ns}-{title}" could be defined with
+ * PARAM_TEMPLATE_VARS => [ 'ns' => 'namespaces', 'title' => 'titles' ]. Then a query for
+ * namespaces=0|1&titles=X|Y would support parameters foo-0-X, foo-0-Y, foo-1-X, and foo-1-Y.
+ *
+ * All placeholders must be present in the parameter's name. Each target parameter must have
+ * PARAM_ISMULTI true. If a target is itself a templated parameter, its PARAM_TEMPLATE_VARS must
+ * be a subset of the referring parameter's, mapping the same placeholders to the same targets.
+ * A parameter cannot target itself.
+ *
+ * @since 1.32
+ */
+ const PARAM_TEMPLATE_VARS = 25;
+
/**@}*/
const ALL_DEFAULT_STRING = '*';
public function extractRequestParams( $parseLimit = true ) {
// Cache parameters, for performance and to avoid T26564.
if ( !isset( $this->mParamCache[$parseLimit] ) ) {
- $params = $this->getFinalParams();
+ $params = $this->getFinalParams() ?: [];
$results = [];
-
- if ( $params ) { // getFinalParams() can return false
- foreach ( $params as $paramName => $paramSettings ) {
+ $warned = [];
+
+ // Process all non-templates and save templates for secondary
+ // processing.
+ $toProcess = [];
+ foreach ( $params as $paramName => $paramSettings ) {
+ if ( isset( $paramSettings[self::PARAM_TEMPLATE_VARS] ) ) {
+ $toProcess[] = [ $paramName, $paramSettings[self::PARAM_TEMPLATE_VARS], $paramSettings ];
+ } else {
$results[$paramName] = $this->getParameterFromSettings(
- $paramName, $paramSettings, $parseLimit );
+ $paramName, $paramSettings, $parseLimit
+ );
+ }
+ }
+
+ // Now process all the templates by successively replacing the
+ // placeholders with all client-supplied values.
+ // This bit duplicates JavaScript logic in
+ // ApiSandbox.PageLayout.prototype.updateTemplatedParams().
+ // If you update this, see if that needs updating too.
+ while ( $toProcess ) {
+ list( $name, $targets, $settings ) = array_shift( $toProcess );
+
+ foreach ( $targets as $placeholder => $target ) {
+ if ( !array_key_exists( $target, $results ) ) {
+ // The target wasn't processed yet, try the next one.
+ // If all hit this case, the parameter has no expansions.
+ continue;
+ }
+ if ( !is_array( $results[$target] ) || !$results[$target] ) {
+ // The target was processed but has no (valid) values.
+ // That means it has no expansions.
+ break;
+ }
+
+ // Expand this target in the name and all other targets,
+ // then requeue if there are more targets left or put in
+ // $results if all are done.
+ unset( $targets[$placeholder] );
+ $placeholder = '{' . $placeholder . '}';
+ foreach ( $results[$target] as $value ) {
+ if ( !preg_match( '/^[^{}]*$/', $value ) ) {
+ // Skip values that make invalid parameter names.
+ $encTargetName = $this->encodeParamName( $target );
+ if ( !isset( $warned[$encTargetName][$value] ) ) {
+ $warned[$encTargetName][$value] = true;
+ $this->addWarning( [
+ 'apiwarn-ignoring-invalid-templated-value',
+ wfEscapeWikiText( $encTargetName ),
+ wfEscapeWikiText( $value ),
+ ] );
+ }
+ continue;
+ }
+
+ $newName = str_replace( $placeholder, $value, $name );
+ if ( !$targets ) {
+ $results[$newName] = $this->getParameterFromSettings( $newName, $settings, $parseLimit );
+ } else {
+ $newTargets = [];
+ foreach ( $targets as $k => $v ) {
+ $newTargets[$k] = str_replace( $placeholder, $value, $v );
+ }
+ $toProcess[] = [ $newName, $newTargets, $settings ];
+ }
+ }
+ break;
}
}
+
$this->mParamCache[$parseLimit] = $results;
}
* @return mixed Parameter value
*/
protected function getParameter( $paramName, $parseLimit = true ) {
- $paramSettings = $this->getFinalParams()[$paramName];
-
- return $this->getParameterFromSettings( $paramName, $paramSettings, $parseLimit );
+ return $this->extractRequestParams( $parseLimit )[$paramName];
}
/**
}
}
+ // Templated?
+ if ( !empty( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
+ $vars = [];
+ $msg = 'api-help-param-templated-var-first';
+ foreach ( $settings[ApiBase::PARAM_TEMPLATE_VARS] as $k => $v ) {
+ $vars[] = $context->msg( $msg, $k, $module->encodeParamName( $v ) );
+ $msg = 'api-help-param-templated-var';
+ }
+ $info[] = $context->msg( 'api-help-param-templated' )
+ ->numParams( count( $vars ) )
+ ->params( Message::listParam( $vars ) )
+ ->parse();
+ }
+
// Type documentation
if ( !isset( $settings[ApiBase::PARAM_TYPE] ) ) {
$dflt = isset( $settings[ApiBase::PARAM_DFLT] )
$help[$k] = $v;
}
$help['datatypes'] = '';
+ $help['templatedparams'] = '';
$help['credits'] = '';
// Fill 'permissions'
$help['permissions'] .= Html::closeElement( 'dl' );
$help['permissions'] .= Html::closeElement( 'div' );
- // Fill 'datatypes' and 'credits', if applicable
+ // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
if ( empty( $options['nolead'] ) ) {
$level = $options['headerlevel'];
$tocnumber = &$options['tocnumber'];
];
}
+ $header = $this->msg( 'api-help-templatedparams-header' )->parse();
+
+ $id = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_PRIMARY );
+ $idFallback = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_FALLBACK );
+ $headline = Linker::makeHeadline( min( 6, $level ),
+ ' class="apihelp-header">',
+ $id,
+ $header,
+ '',
+ $idFallback
+ );
+ // Ensure we have a sane anchor
+ if ( $id !== 'main/templatedparams' && $idFallback !== 'main/templatedparams' ) {
+ $headline = '<div id="main/templatedparams"></div>' . $headline;
+ }
+ $help['templatedparams'] .= $headline;
+ $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
+ if ( !isset( $tocData['main/templatedparams'] ) ) {
+ $tocnumber[$level]++;
+ $tocData['main/templatedparams'] = [
+ 'toclevel' => count( $tocnumber ),
+ 'level' => $level,
+ 'anchor' => 'main/templatedparams',
+ 'line' => $header,
+ 'number' => implode( '.', $tocnumber ),
+ 'index' => false,
+ ];
+ }
+
$header = $this->msg( 'api-credits-header' )->parse();
$id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
$idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
}
$ret['parameters'] = [];
+ $ret['templatedparameters'] = [];
$params = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
$paramDesc = $module->getFinalParamDescription();
+ $index = 0;
foreach ( $params as $name => $settings ) {
if ( !is_array( $settings ) ) {
$settings = [ ApiBase::PARAM_DFLT => $settings ];
}
$item = [
- 'name' => $name
+ 'index' => ++$index,
+ 'name' => $name,
];
+
+ if ( !empty( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
+ $item['templatevars'] = $settings[ApiBase::PARAM_TEMPLATE_VARS];
+ ApiResult::setIndexedTagName( $item['templatevars'], 'var' );
+ }
+
if ( isset( $paramDesc[$name] ) ) {
$this->formatHelpMessages( $item, 'description', $paramDesc[$name], true );
}
ApiResult::setIndexedTagName( $item['info'], 'i' );
}
- $ret['parameters'][] = $item;
+ $key = empty( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ? 'parameters' : 'templatedparameters';
+ $ret[$key][] = $item;
}
ApiResult::setIndexedTagName( $ret['parameters'], 'param' );
+ ApiResult::setIndexedTagName( $ret['templatedparameters'], 'param' );
$dynamicParams = $module->dynamicParameterDocumentation();
if ( $dynamicParams !== null ) {
"api-help-parameters": "{{PLURAL:$1|Parameter|Parameters}}:",
"api-help-param-deprecated": "Deprecated.",
"api-help-param-required": "This parameter is required.",
+ "api-help-param-templated": "This is a [[Special:ApiHelp/main#main/templatedparams|templated parameter]]. When making the request, $2.",
+ "api-help-param-templated-var-first": "<var>{$1}</var> in the parameter's name should be replaced with values of <var>$2</var>",
+ "api-help-param-templated-var": "<var>{$1}</var> with values of <var>$2</var>",
"api-help-datatypes-header": "Data types",
"api-help-datatypes": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nSome parameter types in API requests need further explanation:\n;boolean\n:Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.\n;timestamp\n:Timestamps may be specified in several formats. ISO 8601 date and time is recommended. All times are in UTC, any included timezone is ignored.\n:* ISO 8601 date and time, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (punctuation and <kbd>Z</kbd> are optional)\n:* ISO 8601 date and time with (ignored) fractional seconds, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (dashes, colons, and <kbd>Z</kbd> are optional)\n:* MediaWiki format, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Generic numeric format, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (optional timezone of <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, or <kbd>-<var>##</var></kbd> is ignored)\n:* EXIF format, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*RFC 2822 format (timezone may be omitted), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850 format (timezone may be omitted), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime format, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Seconds since 1970-01-01T00:00:00Z as a 1 to 13 digit integer (excluding <kbd>0</kbd>)\n:* The string <kbd>now</kbd>\n;alternative multiple-value separator\n:Parameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
+ "api-help-templatedparams-header": "Templated parameters",
+ "api-help-templatedparams": "Templated parameters support cases where an API module needs a value for each value of some other parameter. For example, if there were an API module to request fruit, it might have a parameter <var>fruits</var> to specify which fruits are being requested and a templated parameter <var>{fruit}-quantity</var> to specify how many of each fruit to request. An API client that wants 1 apple, 5 bananas, and 20 strawberries could then make a request like <kbd>fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20</kbd>.",
"api-help-param-type-limit": "Type: integer or <kbd>max</kbd>",
"api-help-param-type-integer": "Type: {{PLURAL:$1|1=integer|2=list of integers}}",
"api-help-param-type-boolean": "Type: boolean ([[Special:ApiHelp/main#main/datatypes|details]])",
"apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.",
"apiwarn-errorprinterfailed": "Error printer failed. Will retry without params.",
"apiwarn-errorprinterfailed-ex": "Error printer failed (will retry without params): $1",
+ "apiwarn-ignoring-invalid-templated-value": "Ignoring value <kbd>$2</kbd> in <var>$1</var> when processing templated parameters.",
"apiwarn-invalidcategory": "\"$1\" is not a category.",
"apiwarn-invalidtitle": "\"$1\" is not a valid title.",
"apiwarn-invalidxmlstylesheetext": "Stylesheet should have <code>.xsl</code> extension.",
"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}}",
"api-help-param-required": "Displayed in the API help for any required parameter",
+ "api-help-param-templated": "Displayed in the API help for any templated parameter.\n\nParameters:\n* $1 - Count of template variables in the parameter name.\n* $2 - A list, composed using {{msg-mw|comma-separator}} and {{msg-mw|and}}, of the template variables in the parameter name. The first is formatted using {{msg-mw|api-help-param-templated-var-first|notext=1}} and the rest use {{msg-mw|api-help-param-templated-var|notext=1}}.\n\nSee also:\n* {{msg-mw|api-help-param-templated-var-first}}\n* {{msg-mw|api-help-param-templated-var}}",
+ "api-help-param-templated-var-first": "Used with {{msg-mw|api-help-param-templated|notext=1}} to display templated parameter replacement variables. See that message for context.\n\nParameters:\n* $1 - Variable.\n* $2 - Parameter from which values are taken.\n\nSee also:\n* {{msg-mw|api-help-param-templated}}\n* {{msg-mw|api-help-param-templated-var}}",
+ "api-help-param-templated-var": "Used with {{msg-mw|api-help-param-templated|notext=1}} to display templated parameter replacement variables. See that message for context.\n\nParameters:\n* $1 - Variable.\n* $2 - Parameter from which values are taken.\n\nSee also:\n* {{msg-mw|api-help-param-templated}}\n* {{msg-mw|api-help-param-templated-var-first}}",
"api-help-datatypes-header": "Header for the data type section in the API help output",
"api-help-datatypes": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} Documentation of certain API data types\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
+ "api-help-templatedparams-header": "Header for the \"templated parameters\" section in the API help output.",
+ "api-help-templatedparams": "{{technical}} {{doc-important|Unlike in other API messages, feel free to localize the words \"fruit\", \"fruits\", \"quantity\", \"apples\", \"bananas\", and \"strawberries\" in this message even when inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags. Do not change the punctuation, only the words.}} Documentation for the \"templated parameters\" feature.",
"api-help-param-type-limit": "{{technical}} {{doc-important|Do not translate text inside <kbd> tags}} Used to indicate that a parameter is a \"limit\" type. Parameters:\n* $1 - Always 1.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"api-help-param-type-integer": "{{technical}} Used to indicate that a parameter is an integer or list of integers. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"api-help-param-type-boolean": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a boolean. Parameters:\n* $1 - Always 1.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n\n\"r\" is short for \"revision\". You may translate it.",
"apiwarn-errorprinterfailed": "{{doc-apierror}}",
"apiwarn-errorprinterfailed-ex": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception message, which may already end in punctuation. Probably in English.",
+ "apiwarn-ignoring-invalid-templated-value": "{{doc-apierror}}\n\nParameters:\n* $1 - Target parameter having a bad value.\n* $2 - The bad value being ignored.",
"apiwarn-invalidcategory": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied category name.",
"apiwarn-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied title.",
"apiwarn-invalidxmlstylesheetext": "{{doc-apierror}}",
"apisandbox-dynamic-parameters-add-label": "Add parameter:",
"apisandbox-dynamic-parameters-add-placeholder": "Parameter name",
"apisandbox-dynamic-error-exists": "A parameter named \"$1\" already exists.",
+ "apisandbox-templated-parameter-reason": "This [[Special:ApiHelp/main#main/templatedparams|templated parameter]] is offered based on the {{PLURAL:$1|value|values}} of $2.",
"apisandbox-deprecated-parameters": "Deprecated parameters",
"apisandbox-fetch-token": "Auto-fill the token",
"apisandbox-add-multi": "Add",
"apisandbox-dynamic-parameters-add-label": "JavaScript label for the widget to add a new arbitrary parameter.",
"apisandbox-dynamic-parameters-add-placeholder": "JavaScript text field placeholder for the widget to add a new arbitrary parameter.",
"apisandbox-dynamic-error-exists": "Displayed as an error message from JavaScript when trying to add a new arbitrary parameter with a name that already exists. Parameters:\n* $1 - Parameter name that failed.",
+ "apisandbox-templated-parameter-reason": "Displayed (from JavaScript) on each instance of a templated parameter.\n\nParameters:\n* $1 - Number of fields in $2.\n* $2 - List of targeted fields, combined using {{msg-mw|comma-separator}} and {{msg-mw|and}}.",
"apisandbox-deprecated-parameters": "JavaScript button label and fieldset legend for separating deprecated parameters in the UI.",
"apisandbox-fetch-token": "Label for the button that fetches a CSRF token.",
"apisandbox-add-multi": "Label for the button to add another value to a field that accepts multiple values\n{{Identical|Add}}",
'apisandbox-dynamic-parameters-add-label',
'apisandbox-dynamic-parameters-add-placeholder',
'apisandbox-dynamic-error-exists',
+ 'apisandbox-templated-parameter-reason',
'apisandbox-deprecated-parameters',
'apisandbox-no-parameters',
'api-help-param-limit',
'apisandbox-multivalue-all-values',
'api-format-prettyprint-status',
'blanknamespace',
+ 'comma-separator',
+ 'word-separator',
+ 'and'
],
],
'mediawiki.special.block' => [
}
}
+ widget.connect( this, {
+ change: [ this.emit, 'change' ]
+ } );
+
this.$cover.on( 'click', this.onOverlayClick.bind( this ) );
this.$element
this.widget.setDisabled( this.isDisabled() );
this.checkbox.setSelected( !this.isDisabled() );
this.$cover.toggle( this.isDisabled() );
+ this.emit( 'change' );
return this;
};
},
tagWidget: {
+ parseApiValue: function ( v ) {
+ if ( v === undefined || v === '' || v === '\x1f' ) {
+ return [];
+ } else {
+ v = String( v );
+ if ( v[ 0 ] !== '\x1f' ) {
+ return v.split( '|' );
+ } else {
+ return v.substr( 1 ).split( '\x1f' );
+ }
+ }
+ },
+ getApiValueForTemplates: function () {
+ return this.isDisabled() ? this.parseApiValue( this.paramInfo[ 'default' ] ) : this.getValue();
+ },
getApiValue: function () {
var items = this.getValue();
if ( items.join( '' ).indexOf( '|' ) === -1 ) {
}
},
setApiValue: function ( v ) {
- if ( v === undefined || v === '' || v === '\x1f' ) {
- this.setValue( [] );
- } else {
- v = String( v );
- if ( v.indexOf( '\x1f' ) !== 0 ) {
- this.setValue( v.split( '|' ) );
- } else {
- this.setValue( v.substr( 1 ).split( '\x1f' ) );
- }
+ if ( v === undefined ) {
+ v = this.paramInfo[ 'default' ];
}
+ this.setValue( this.parseApiValue( v ) );
},
apiCheckValid: function () {
var ok = true,
finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
}
+ if ( widget.getApiValueForTemplates ) {
+ finalWidget.getApiValueForTemplates = widget.getApiValueForTemplates.bind( widget );
+ }
finalWidget.setDisabled( true );
}
this.apiIsValid = true;
this.loadFromQueryParams = null;
this.widgets = {};
+ this.itemsFieldset = null;
+ this.deprecatedItemsFieldset = null;
+ this.templatedItemsCache = {};
this.tokenWidget = null;
this.indentLevel = config.indentLevel ? config.indentLevel : 0;
ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
);
};
+ function widgetLabelOnClick() {
+ var f = this.getField();
+ if ( $.isFunction( f.setDisabled ) ) {
+ f.setDisabled( false );
+ }
+ if ( $.isFunction( f.focus ) ) {
+ f.focus();
+ }
+ }
+
+ /**
+ * Create a widget and the FieldLayouts it needs
+ * @private
+ * @param {Object} ppi API paraminfo data for the parameter
+ * @param {string} name API parameter name
+ * @return {Object}
+ * @return {OO.ui.Widget} return.widget
+ * @return {OO.ui.FieldLayout} return.widgetField
+ * @return {OO.ui.FieldLayout} return.helpField
+ */
+ ApiSandbox.PageLayout.prototype.makeWidgetFieldLayouts = function ( ppi, name ) {
+ var j, l, widget, descriptionContainer, tmp, flag, count, button, widgetField, helpField, layoutConfig;
+
+ widget = Util.createWidgetForParameter( ppi );
+ if ( ppi.tokentype ) {
+ this.tokenWidget = widget;
+ }
+ if ( this.paramInfo.templatedparameters.length ) {
+ widget.on( 'change', this.updateTemplatedParameters, [ null ], this );
+ }
+
+ descriptionContainer = $( '<div>' );
+
+ tmp = Util.parseHTML( ppi.description );
+ tmp.filter( 'dl' ).makeCollapsible( {
+ collapsed: true
+ } ).children( '.mw-collapsible-toggle' ).each( function () {
+ var $this = $( this );
+ $this.parent().prev( 'p' ).append( $this );
+ } );
+ descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
+
+ if ( ppi.info && ppi.info.length ) {
+ for ( j = 0; j < ppi.info.length; j++ ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseHTML( ppi.info[ j ] ) )
+ );
+ }
+ }
+ flag = true;
+ count = Infinity;
+ switch ( ppi.type ) {
+ case 'namespace':
+ flag = false;
+ count = mw.config.get( 'wgFormattedNamespaces' ).length;
+ break;
+
+ case 'limit':
+ if ( ppi.highmax !== undefined ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append(
+ Util.parseMsg(
+ 'api-help-param-limit2', ppi.max, ppi.highmax
+ ),
+ ' ',
+ Util.parseMsg( 'apisandbox-param-limit' )
+ )
+ );
+ } else {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append(
+ Util.parseMsg( 'api-help-param-limit', ppi.max ),
+ ' ',
+ Util.parseMsg( 'apisandbox-param-limit' )
+ )
+ );
+ }
+ break;
+
+ case 'integer':
+ tmp = '';
+ if ( ppi.min !== undefined ) {
+ tmp += 'min';
+ }
+ if ( ppi.max !== undefined ) {
+ tmp += 'max';
+ }
+ if ( tmp !== '' ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseMsg(
+ 'api-help-param-integer-' + tmp,
+ Util.apiBool( ppi.multi ) ? 2 : 1,
+ ppi.min, ppi.max
+ ) )
+ );
+ }
+ break;
+
+ default:
+ if ( Array.isArray( ppi.type ) ) {
+ flag = false;
+ count = ppi.type.length;
+ }
+ break;
+ }
+ if ( Util.apiBool( ppi.multi ) ) {
+ tmp = [];
+ if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
+ !(
+ widget instanceof OptionalWidget &&
+ widget.widget instanceof OO.ui.TagMultiselectWidget
+ )
+ ) {
+ tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
+ }
+ if ( count > ppi.lowlimit ) {
+ tmp.push(
+ mw.message( 'api-help-param-multi-max', ppi.lowlimit, ppi.highlimit ).parse()
+ );
+ }
+ if ( tmp.length ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseHTML( tmp.join( ' ' ) ) )
+ );
+ }
+ }
+ if ( 'maxbytes' in ppi ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseMsg( 'api-help-param-maxbytes', ppi.maxbytes ) )
+ );
+ }
+ if ( 'maxchars' in ppi ) {
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseMsg( 'api-help-param-maxchars', ppi.maxchars ) )
+ );
+ }
+ if ( ppi.usedTemplateVars && ppi.usedTemplateVars.length ) {
+ tmp = $();
+ for ( j = 0, l = ppi.usedTemplateVars.length; j < l; j++ ) {
+ tmp = tmp.add( $( '<var>' ).text( ppi.usedTemplateVars[ j ] ) );
+ if ( j === l - 2 ) {
+ tmp = tmp.add( mw.message( 'and' ).parseDom() );
+ tmp = tmp.add( mw.message( 'word-separator' ).parseDom() );
+ } else if ( j !== l - 1 ) {
+ tmp = tmp.add( mw.message( 'comma-separator' ).parseDom() );
+ }
+ }
+ descriptionContainer.append( $( '<div>' )
+ .addClass( 'info' )
+ .append( Util.parseMsg(
+ 'apisandbox-templated-parameter-reason',
+ ppi.usedTemplateVars.length,
+ tmp
+ ) )
+ );
+ }
+
+ helpField = new OO.ui.FieldLayout(
+ new OO.ui.Widget( {
+ $content: '\xa0',
+ classes: [ 'mw-apisandbox-spacer' ]
+ } ), {
+ align: 'inline',
+ classes: [ 'mw-apisandbox-help-field' ],
+ label: descriptionContainer
+ }
+ );
+
+ layoutConfig = {
+ align: 'left',
+ classes: [ 'mw-apisandbox-widget-field' ],
+ label: name
+ };
+
+ if ( ppi.tokentype ) {
+ button = new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-fetch-token' ).text()
+ } );
+ button.on( 'click', widget.fetchToken, [], widget );
+
+ widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
+ } else {
+ widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
+ }
+
+ // We need our own click handler on the widget label to
+ // turn off the disablement.
+ widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
+
+ // Don't grey out the label when the field is disabled,
+ // it makes it too hard to read and our "disabled"
+ // isn't really disabled.
+ widgetField.onFieldDisable( false );
+ widgetField.onFieldDisable = $.noop;
+
+ widgetField.apiParamIndex = ppi.index;
+
+ return {
+ widget: widget,
+ widgetField: widgetField,
+ helpField: helpField
+ };
+ };
+
+ /**
+ * Update templated parameters in the page
+ * @private
+ * @param {Object} [params] Query parameters for initializing the widgets
+ */
+ ApiSandbox.PageLayout.prototype.updateTemplatedParameters = function ( params ) {
+ var p, toProcess, doProcess, tmp, toRemove,
+ that = this,
+ pi = this.paramInfo,
+ prefix = that.prefix + pi.prefix;
+
+ if ( !pi || !pi.templatedparameters.length ) {
+ return;
+ }
+
+ if ( !$.isPlainObject( params ) ) {
+ params = null;
+ }
+
+ toRemove = {};
+ $.each( this.templatedItemsCache, function ( k, el ) {
+ if ( el.widget.isElementAttached() ) {
+ toRemove[ k ] = el;
+ }
+ } );
+
+ // This bit duplicates the PHP logic in ApiBase::extractRequestParams().
+ // If you update this, see if that needs updating too.
+ toProcess = pi.templatedparameters.map( function ( p ) {
+ return {
+ name: prefix + p.name,
+ info: p,
+ vars: $.extend( {}, p.templatevars ),
+ usedVars: []
+ };
+ } );
+ doProcess = function ( placeholder, target ) {
+ var values, container, index, usedVars, done;
+
+ target = prefix + target;
+
+ if ( !that.widgets[ target ] ) {
+ // The target wasn't processed yet, try the next one.
+ // If all hit this case, the parameter has no expansions.
+ return true;
+ }
+
+ if ( !that.widgets[ target ].getApiValueForTemplates ) {
+ // Not a multi-valued widget, so it can't have expansions.
+ return false;
+ }
+
+ values = that.widgets[ target ].getApiValueForTemplates();
+ if ( !Array.isArray( values ) || !values.length ) {
+ // The target was processed but has no (valid) values.
+ // That means it has no expansions.
+ return false;
+ }
+
+ // Expand this target in the name and all other targets,
+ // then requeue if there are more targets left or create the widget
+ // and add it to the form if all are done.
+ delete p.vars[ placeholder ];
+ usedVars = p.usedVars.concat( [ target ] );
+ placeholder = '{' + placeholder + '}';
+ done = $.isEmptyObject( p.vars );
+ if ( done ) {
+ container = Util.apiBool( p.info.deprecated ) ? that.deprecatedItemsFieldset : that.itemsFieldset;
+ index = container.getItems().findIndex( function ( el ) {
+ return el.apiParamIndex !== undefined && el.apiParamIndex > p.info.index;
+ } );
+ if ( index < 0 ) {
+ index = undefined;
+ }
+ }
+ values.forEach( function ( value ) {
+ var name, newVars;
+
+ if ( !/^[^{}]*$/.exec( value ) ) {
+ // Skip values that make invalid parameter names
+ return;
+ }
+
+ name = p.name.replace( placeholder, value );
+ if ( done ) {
+ if ( that.templatedItemsCache[ name ] ) {
+ tmp = that.templatedItemsCache[ name ];
+ } else {
+ tmp = that.makeWidgetFieldLayouts(
+ $.extend( {}, p.info, { usedTemplateVars: usedVars } ), name
+ );
+ that.templatedItemsCache[ name ] = tmp;
+ }
+ delete toRemove[ name ];
+ if ( !tmp.widget.isElementAttached() ) {
+ that.widgets[ name ] = tmp.widget;
+ container.addItems( [ tmp.widgetField, tmp.helpField ], index );
+ if ( index !== undefined ) {
+ index += 2;
+ }
+ }
+ if ( params ) {
+ tmp.widget.setApiValue( params.hasOwnProperty( name ) ? params[ name ] : undefined );
+ }
+ } else {
+ newVars = {};
+ $.each( p.vars, function ( k, v ) {
+ newVars[ k ] = v.replace( placeholder, value );
+ } );
+ toProcess.push( {
+ name: name,
+ info: p.info,
+ vars: newVars,
+ usedVars: usedVars
+ } );
+ }
+ } );
+ return false;
+ };
+ while ( toProcess.length ) {
+ p = toProcess.shift();
+ $.each( p.vars, doProcess );
+ }
+
+ toRemove = $.map( toRemove, function ( el, name ) {
+ delete that.widgets[ name ];
+ return [ el.widgetField, el.helpField ];
+ } );
+ if ( toRemove.length ) {
+ this.itemsFieldset.removeItems( toRemove );
+ this.deprecatedItemsFieldset.removeItems( toRemove );
+ }
+ };
+
/**
* Fetch module information for this page's module, then create UI
*/
Util.fetchModuleInfo( this.apiModule )
.done( function ( pi ) {
- var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count,
+ var prefix, i, j, tmp,
items = [],
deprecatedItems = [],
buttons = [],
filterFmModules = function ( v ) {
return v.substr( -2 ) !== 'fm' ||
!availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
- },
- widgetLabelOnClick = function () {
- var f = this.getField();
- if ( $.isFunction( f.setDisabled ) ) {
- f.setDisabled( false );
- }
- if ( $.isFunction( f.focus ) ) {
- f.focus();
- }
};
// This is something of a hack. We always want the 'format' and
if ( pi.parameters.length ) {
prefix = that.prefix + pi.prefix;
for ( i = 0; i < pi.parameters.length; i++ ) {
- widget = Util.createWidgetForParameter( pi.parameters[ i ] );
- that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
- if ( pi.parameters[ i ].tokentype ) {
- that.tokenWidget = widget;
- }
-
- descriptionContainer = $( '<div>' );
-
- tmp = Util.parseHTML( pi.parameters[ i ].description );
- tmp.filter( 'dl' ).makeCollapsible( {
- collapsed: true
- } ).children( '.mw-collapsible-toggle' ).each( function () {
- var $this = $( this );
- $this.parent().prev( 'p' ).append( $this );
- } );
- descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
-
- if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
- for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseHTML( pi.parameters[ i ].info[ j ] ) )
- );
- }
- }
- flag = true;
- count = 1e100;
- switch ( pi.parameters[ i ].type ) {
- case 'namespace':
- flag = false;
- count = mw.config.get( 'wgFormattedNamespaces' ).length;
- break;
-
- case 'limit':
- if ( pi.parameters[ i ].highmax !== undefined ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append(
- Util.parseMsg(
- 'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
- ),
- ' ',
- Util.parseMsg( 'apisandbox-param-limit' )
- )
- );
- } else {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append(
- Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
- ' ',
- Util.parseMsg( 'apisandbox-param-limit' )
- )
- );
- }
- break;
-
- case 'integer':
- tmp = '';
- if ( pi.parameters[ i ].min !== undefined ) {
- tmp += 'min';
- }
- if ( pi.parameters[ i ].max !== undefined ) {
- tmp += 'max';
- }
- if ( tmp !== '' ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseMsg(
- 'api-help-param-integer-' + tmp,
- Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
- pi.parameters[ i ].min, pi.parameters[ i ].max
- ) )
- );
- }
- break;
-
- default:
- if ( Array.isArray( pi.parameters[ i ].type ) ) {
- flag = false;
- count = pi.parameters[ i ].type.length;
- }
- break;
- }
- if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
- tmp = [];
- if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
- !(
- widget instanceof OptionalWidget &&
- widget.widget instanceof OO.ui.TagMultiselectWidget
- )
- ) {
- tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
- }
- if ( count > pi.parameters[ i ].lowlimit ) {
- tmp.push(
- mw.message( 'api-help-param-multi-max',
- pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
- ).parse()
- );
- }
- if ( tmp.length ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseHTML( tmp.join( ' ' ) ) )
- );
- }
- }
- if ( 'maxbytes' in pi.parameters[ i ] ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes ) )
- );
- }
- if ( 'maxchars' in pi.parameters[ i ] ) {
- descriptionContainer.append( $( '<div>' )
- .addClass( 'info' )
- .append( Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars ) )
- );
- }
- helpField = new OO.ui.FieldLayout(
- new OO.ui.Widget( {
- $content: '\xa0',
- classes: [ 'mw-apisandbox-spacer' ]
- } ), {
- align: 'inline',
- classes: [ 'mw-apisandbox-help-field' ],
- label: descriptionContainer
- }
- );
-
- layoutConfig = {
- align: 'left',
- classes: [ 'mw-apisandbox-widget-field' ],
- label: prefix + pi.parameters[ i ].name
- };
-
- if ( pi.parameters[ i ].tokentype ) {
- button = new OO.ui.ButtonWidget( {
- label: mw.message( 'apisandbox-fetch-token' ).text()
- } );
- button.on( 'click', widget.fetchToken, [], widget );
-
- widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
- } else {
- widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
- }
-
- // We need our own click handler on the widget label to
- // turn off the disablement.
- widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
-
- // Don't grey out the label when the field is disabled,
- // it makes it too hard to read and our "disabled"
- // isn't really disabled.
- widgetField.onFieldDisable( false );
- widgetField.onFieldDisable = $.noop;
-
+ tmp = that.makeWidgetFieldLayouts( pi.parameters[ i ], prefix + pi.parameters[ i ].name );
+ that.widgets[ prefix + pi.parameters[ i ].name ] = tmp.widget;
if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
- deprecatedItems.push( widgetField, helpField );
+ deprecatedItems.push( tmp.widgetField, tmp.helpField );
} else {
- items.push( widgetField, helpField );
+ items.push( tmp.widgetField, tmp.helpField );
}
}
}
that.$element.empty();
- new OO.ui.FieldsetLayout( {
+ that.itemsFieldset = new OO.ui.FieldsetLayout( {
label: that.displayText
- } ).addItems( items )
- .$element.appendTo( that.$element );
+ } );
+ that.itemsFieldset.addItems( items );
+ that.itemsFieldset.$element.appendTo( that.$element );
if ( Util.apiBool( pi.dynamicparameters ) ) {
dynamicFieldset = new OO.ui.FieldsetLayout();
.appendTo( that.$element );
}
- if ( deprecatedItems.length ) {
- tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
- $( '<fieldset>' )
- .append(
- $( '<legend>' ).append(
- new OO.ui.ToggleButtonWidget( {
- label: mw.message( 'apisandbox-deprecated-parameters' ).text()
- } ).on( 'change', tmp.toggle, [], tmp ).$element
- ),
- tmp.$element
- )
- .appendTo( that.$element );
- }
+ that.deprecatedItemsFieldset = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
+ tmp = $( '<fieldset>' )
+ .toggle( !that.deprecatedItemsFieldset.isEmpty() )
+ .append(
+ $( '<legend>' ).append(
+ new OO.ui.ToggleButtonWidget( {
+ label: mw.message( 'apisandbox-deprecated-parameters' ).text()
+ } ).on( 'change', that.deprecatedItemsFieldset.toggle, [], that.deprecatedItemsFieldset ).$element
+ ),
+ that.deprecatedItemsFieldset.$element
+ )
+ .appendTo( that.$element );
+ that.deprecatedItemsFieldset.on( 'add', function () {
+ this.toggle( !that.deprecatedItemsFieldset.isEmpty() );
+ }, [], tmp );
+ that.deprecatedItemsFieldset.on( 'remove', function () {
+ this.toggle( !that.deprecatedItemsFieldset.isEmpty() );
+ }, [], tmp );
// Load stored params, if any, then update the booklet if we
// have subpages (or else just update our valid-indicator).
that.loadFromQueryParams = null;
if ( $.isPlainObject( tmp ) ) {
that.loadQueryParams( tmp );
+ } else {
+ that.updateTemplatedParameters();
}
if ( that.getSubpages().length > 0 ) {
ApiSandbox.updateUI( tmp );
var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
widget.setApiValue( v );
} );
+ this.updateTemplatedParameters( params );
}
};
}
}
+ /**
+ * @covers ApiBase::extractRequestParams
+ */
+ public function testExtractRequestParams() {
+ $request = new FauxRequest( [
+ 'xxexists' => 'exists!',
+ 'xxmulti' => 'a|b|c|d|{bad}',
+ 'xxempty' => '',
+ 'xxtemplate-a' => 'A!',
+ 'xxtemplate-b' => 'B1|B2|B3',
+ 'xxtemplate-c' => '',
+ 'xxrecursivetemplate-b-B1' => 'X',
+ 'xxrecursivetemplate-b-B3' => 'Y',
+ 'xxrecursivetemplate-b-B4' => '?',
+ 'xxemptytemplate-' => 'nope',
+ 'foo' => 'a|b|c',
+ 'xxfoo' => 'a|b|c',
+ 'errorformat' => 'raw',
+ ] );
+ $context = new DerivativeContext( RequestContext::getMain() );
+ $context->setRequest( $request );
+ $main = new ApiMain( $context );
+
+ $mock = $this->getMockBuilder( ApiBase::class )
+ ->setConstructorArgs( [ $main, 'test', 'xx' ] )
+ ->setMethods( [ 'getAllowedParams' ] )
+ ->getMockForAbstractClass();
+ $mock->method( 'getAllowedParams' )->willReturn( [
+ 'notexists' => null,
+ 'exists' => null,
+ 'multi' => [
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'empty' => [
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'template-{m}' => [
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_TEMPLATE_VARS => [ 'm' => 'multi' ],
+ ],
+ 'recursivetemplate-{m}-{t}' => [
+ ApiBase::PARAM_TEMPLATE_VARS => [ 't' => 'template-{m}', 'm' => 'multi' ],
+ ],
+ 'emptytemplate-{m}' => [
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_TEMPLATE_VARS => [ 'm' => 'empty' ],
+ ],
+ 'badtemplate-{e}' => [
+ ApiBase::PARAM_TEMPLATE_VARS => [ 'e' => 'exists' ],
+ ],
+ 'badtemplate2-{e}' => [
+ ApiBase::PARAM_TEMPLATE_VARS => [ 'e' => 'badtemplate2-{e}' ],
+ ],
+ 'badtemplate3-{x}' => [
+ ApiBase::PARAM_TEMPLATE_VARS => [ 'x' => 'foo' ],
+ ],
+ ] );
+
+ $this->assertEquals( [
+ 'notexists' => null,
+ 'exists' => 'exists!',
+ 'multi' => [ 'a', 'b', 'c', 'd', '{bad}' ],
+ 'empty' => [],
+ 'template-a' => [ 'A!' ],
+ 'template-b' => [ 'B1', 'B2', 'B3' ],
+ 'template-c' => [],
+ 'template-d' => null,
+ 'recursivetemplate-a-A!' => null,
+ 'recursivetemplate-b-B1' => 'X',
+ 'recursivetemplate-b-B2' => null,
+ 'recursivetemplate-b-B3' => 'Y',
+ ], $mock->extractRequestParams() );
+
+ $used = TestingAccessWrapper::newFromObject( $main )->getParamsUsed();
+ sort( $used );
+ $this->assertEquals( [
+ 'xxempty',
+ 'xxexists',
+ 'xxmulti',
+ 'xxnotexists',
+ 'xxrecursivetemplate-a-A!',
+ 'xxrecursivetemplate-b-B1',
+ 'xxrecursivetemplate-b-B2',
+ 'xxrecursivetemplate-b-B3',
+ 'xxtemplate-a',
+ 'xxtemplate-b',
+ 'xxtemplate-c',
+ 'xxtemplate-d',
+ ], $used );
+
+ $warnings = $mock->getResult()->getResultData( 'warnings', [ 'Strip' => 'all' ] );
+ $this->assertCount( 1, $warnings );
+ $this->assertSame( 'ignoring-invalid-templated-value', $warnings[0]['code'] );
+ }
+
}
ApiBase::PARAM_ISMULTI_LIMIT2 => [ 'integer' ],
ApiBase::PARAM_MAX_BYTES => [ 'integer' ],
ApiBase::PARAM_MAX_CHARS => [ 'integer' ],
+ ApiBase::PARAM_TEMPLATE_VARS => [ 'array' ],
];
// param => [ other param that must be present => required value or null ]
"$param: PARAM_MAX_BYTES cannot be less than PARAM_MAX_CHARS"
);
}
+
+ if ( isset( $config[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
+ $this->assertNotSame( [], $config[ApiBase::PARAM_TEMPLATE_VARS],
+ "$param: PARAM_TEMPLATE_VARS cannot be empty" );
+ foreach ( $config[ApiBase::PARAM_TEMPLATE_VARS] as $key => $target ) {
+ $this->assertRegExp( '/^[^{}]+$/', $key,
+ "$param: PARAM_TEMPLATE_VARS key may not contain '{' or '}'" );
+
+ $this->assertContains( '{' . $key . '}', $param,
+ "$param: Name must contain PARAM_TEMPLATE_VARS key {" . $key . "}" );
+ $this->assertArrayHasKey( $target, $params,
+ "$param: PARAM_TEMPLATE_VARS target parameter '$target' does not exist" );
+ $config2 = $params[$target];
+ $this->assertTrue( !empty( $config2[ApiBase::PARAM_ISMULTI] ),
+ "$param: PARAM_TEMPLATE_VARS target parameter '$target' must have PARAM_ISMULTI = true" );
+
+ if ( isset( $config2[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
+ $this->assertNotSame( $param, $target,
+ "$param: PARAM_TEMPLATE_VARS cannot target itself" );
+
+ $this->assertArraySubset(
+ $config2[ApiBase::PARAM_TEMPLATE_VARS],
+ $config[ApiBase::PARAM_TEMPLATE_VARS],
+ true,
+ "$param: PARAM_TEMPLATE_VARS target parameter '$target': "
+ . "the target's PARAM_TEMPLATE_VARS must be a subset of the original."
+ );
+ }
+ }
+
+ $keys = implode( '|',
+ array_map( 'preg_quote', array_keys( $config[ApiBase::PARAM_TEMPLATE_VARS] ) )
+ );
+ $this->assertRegExp( '/^(?>[^{}]+|\{(?:' . $keys . ')\})+$/', $param,
+ "$param: Name may not contain '{' or '}' other than as defined by PARAM_TEMPLATE_VARS" );
+ } else {
+ $this->assertRegExp( '/^[^{}]+$/', $param,
+ "$param: Name may not contain '{' or '}' without PARAM_TEMPLATE_VARS" );
+ }
}
}
}