From 6a242023ca401b3c7ee9cdc68a2dffa6524fab3b Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bartosz=20Dziewo=C5=84ski?= Date: Tue, 27 Aug 2019 22:14:01 +0200 Subject: [PATCH] mw.Uri: Add support for array parameters with explicit indexes When the new 'arrayParams' option is set, query strings like `&foo[0]=a&foo[1]=b` will be parsed as a single parameter `foo` containing an array, rather than two separate parameters. The new option also affects the behavior of array parameters like `&foo[]=a&foo[]=b`, which will be parsed as a parameter named `foo` rather than `foo[]`, and disables array handling for parameters that don't contain an array index at the end. Unlike in PHP, this does not handle associative or multi-dimensional arrays, but that may be improved in the future. Bug: T231382 Change-Id: I48d4bb3fdf0ea7f5eb133c59bf63651ba356fc42 --- resources/src/mediawiki.Uri/Uri.js | 50 ++++++++++++++++--- .../resources/mediawiki/mediawiki.Uri.test.js | 44 ++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/resources/src/mediawiki.Uri/Uri.js b/resources/src/mediawiki.Uri/Uri.js index 4343ecc8f8..a91e57a4a2 100644 --- a/resources/src/mediawiki.Uri/Uri.js +++ b/resources/src/mediawiki.Uri/Uri.js @@ -176,6 +176,10 @@ * @param {boolean} [options.strictMode=false] Trigger strict mode parsing of the url. * @param {boolean} [options.overrideKeys=false] Whether to let duplicate query parameters * override each other (`true`) or automagically convert them to an array (`false`). + * @param {boolean} [options.arrayParams=false] Whether to parse array query parameters (e.g. + * `&foo[0]=a&foo[1]=b` or `&foo[]=a&foo[]=b`) or leave them alone. Currently this does not + * handle associative or multi-dimensional arrays, but that may be improved in the future. + * Implies `overrideKeys: true` (query parameters without `[...]` are not parsed as arrays). * @throws {Error} when the query string or fragment contains an unknown % sequence */ function Uri( uri, options ) { @@ -186,9 +190,12 @@ options = typeof options === 'object' ? options : { strictMode: !!options }; options = $.extend( { strictMode: false, - overrideKeys: false + overrideKeys: false, + arrayParams: false }, options ); + this.arrayParams = options.arrayParams; + if ( uri !== undefined && uri !== null && uri !== '' ) { if ( typeof uri === 'string' ) { this.parse( uri, options ); @@ -301,13 +308,34 @@ // using replace to iterate over a string if ( uri.query ) { uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( match, k, eq, v ) { + var arrayKeyMatch, i; if ( k ) { k = Uri.decode( k ); v = ( eq === '' || eq === undefined ) ? null : Uri.decode( v ); + arrayKeyMatch = k.match( /^([^[]+)\[(\d*)\]$/ ); + + // If arrayParams and this parameter name contains an array index... + if ( options.arrayParams && arrayKeyMatch ) { + // Remove the index from parameter name + k = arrayKeyMatch[ 1 ]; + + // Turn the parameter value into an array (throw away anything else) + if ( !Array.isArray( q[ k ] ) ) { + q[ k ] = []; + } + + i = arrayKeyMatch[ 2 ]; + if ( i === '' ) { + // If no explicit index, append at the end + i = q[ k ].length; + } + + q[ k ][ i ] = v; // If overrideKeys, always (re)set top level value. // If not overrideKeys but this key wasn't set before, then we set it as well. - if ( options.overrideKeys || !hasOwn.call( q, k ) ) { + // arrayParams implies overrideKeys (no array handling for non-array params). + } else if ( options.arrayParams || options.overrideKeys || !hasOwn.call( q, k ) ) { q[ k ] = v; // Use arrays if overrideKeys is false and key was already seen before @@ -369,18 +397,24 @@ * @return {string} */ getQueryString: function () { - var args = []; + var args = [], + arrayParams = this.arrayParams; // eslint-disable-next-line no-jquery/no-each-util $.each( this.query, function ( key, val ) { var k = Uri.encode( key ), - vals = Array.isArray( val ) ? val : [ val ]; - vals.forEach( function ( v ) { + isArrayParam = Array.isArray( val ), + vals = isArrayParam ? val : [ val ]; + vals.forEach( function ( v, i ) { + var ki = k; + if ( arrayParams && isArrayParam ) { + ki += Uri.encode( '[' + i + ']' ); + } if ( v === null ) { - args.push( k ); + args.push( ki ); } else if ( k === 'title' ) { - args.push( k + '=' + mw.util.wikiUrlencode( v ) ); + args.push( ki + '=' + mw.util.wikiUrlencode( v ) ); } else { - args.push( k + '=' + Uri.encode( v ) ); + args.push( ki + '=' + Uri.encode( v ) ); } } ); } ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js index 5eb5e05d39..013fb0d065 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js @@ -267,6 +267,50 @@ } ); + QUnit.test( 'arrayParams', function ( assert ) { + var uri1, uri2, uri3, expectedQ, expectedS, + uriMissing, expectedMissingQ, expectedMissingS, + uriWeird, expectedWeirdQ, expectedWeirdS; + + uri1 = new mw.Uri( 'http://example.com/?foo[]=a&foo[]=b&foo[]=c', { arrayParams: true } ); + uri2 = new mw.Uri( 'http://example.com/?foo[0]=a&foo[1]=b&foo[2]=c', { arrayParams: true } ); + uri3 = new mw.Uri( 'http://example.com/?foo[1]=b&foo[0]=a&foo[]=c', { arrayParams: true } ); + expectedQ = { foo: [ 'a', 'b', 'c' ] }; + expectedS = 'foo%5B0%5D=a&foo%5B1%5D=b&foo%5B2%5D=c'; + + assert.deepEqual( uri1.query, expectedQ, + 'array query parameters are parsed (implicit indexes)' ); + assert.deepEqual( uri1.getQueryString(), expectedS, + 'array query parameters are encoded (always with explicit indexes)' ); + assert.deepEqual( uri2.query, expectedQ, + 'array query parameters are parsed (explicit indexes)' ); + assert.deepEqual( uri2.getQueryString(), expectedS, + 'array query parameters are encoded (always with explicit indexes)' ); + assert.deepEqual( uri3.query, expectedQ, + 'array query parameters are parsed (mixed indexes, out of order)' ); + assert.deepEqual( uri3.getQueryString(), expectedS, + 'array query parameters are encoded (always with explicit indexes)' ); + + uriMissing = new mw.Uri( 'http://example.com/?foo[0]=a&foo[2]=c', { arrayParams: true } ); + // eslint-disable-next-line no-sparse-arrays + expectedMissingQ = { foo: [ 'a', , 'c' ] }; + expectedMissingS = 'foo%5B0%5D=a&foo%5B2%5D=c'; + + assert.deepEqual( uriMissing.query, expectedMissingQ, + 'array query parameters are parsed (missing array item)' ); + assert.deepEqual( uriMissing.getQueryString(), expectedMissingS, + 'array query parameters are encoded (missing array item)' ); + + uriWeird = new mw.Uri( 'http://example.com/?foo[0]=a&foo[1][1]=b&foo[x]=c', { arrayParams: true } ); + expectedWeirdQ = { foo: [ 'a' ], 'foo[1][1]': 'b', 'foo[x]': 'c' }; + expectedWeirdS = 'foo%5B0%5D=a&foo%5B1%5D%5B1%5D=b&foo%5Bx%5D=c'; + + assert.deepEqual( uriWeird.query, expectedWeirdQ, + 'array query parameters are parsed (multi-dimensional or associative arrays are ignored)' ); + assert.deepEqual( uriWeird.getQueryString(), expectedWeirdS, + 'array query parameters are encoded (multi-dimensional or associative arrays are ignored)' ); + } ); + QUnit.test( '.clone()', function ( assert ) { var original, clone; -- 2.20.1