Support the same parameter key more than once in Uri query parameters.

R=floitsch@google.com, sgjesse@google.com

Review URL: https://codereview.chromium.org/1520943002 .
This commit is contained in:
Lasse R.H. Nielsen 2016-01-13 13:07:18 +01:00
parent cd21da1d98
commit f70bef4a2c
3 changed files with 157 additions and 28 deletions

View file

@ -12,6 +12,8 @@
* Added `current` getter to `StackTrace` class.
* Added `Uri.data` getter for `data:` URIs, and `UriData` class for the
return type.
* Addad `Uri.queryParametersAll` to handle multiple query parameters with
the same name.
* Added `growable` parameter to `List.filled` constructor.
* Added microsecond support to `DateTime`: `DateTime.microsecond`,
`DateTime.microsecondsSinceEpoch`, and

View file

@ -76,6 +76,7 @@ class Uri {
* Cache the computed return value of [queryParameters].
*/
Map<String, String> _queryParameters;
Map<String, List<String>> _queryParameterLists;
/// Internal non-verifying constructor. Only call with validated arguments.
Uri._internal(this.scheme,
@ -116,39 +117,46 @@ class Uri {
* default port.
*
* If any of `userInfo`, `host` or `port` are provided,
* the URI will have an autority according to [hasAuthority].
* the URI has an autority according to [hasAuthority].
*
* The path component is set through either [path] or
* [pathSegments]. When [path] is used, it should be a valid URI path,
* [pathSegments].
* When [path] is used, it should be a valid URI path,
* but invalid characters, except the general delimiters ':/@[]?#',
* will be escaped if necessary.
* When [pathSegments] is used, each of the provided segments
* is first percent-encoded and then joined using the forward slash
* separator. The percent-encoding of the path segments encodes all
* separator.
*
* The percent-encoding of the path segments encodes all
* characters except for the unreserved characters and the following
* list of characters: `!$&'()*+,;=:@`. If the other components
* calls for an absolute path a leading slash `/` is prepended if
* necessitate an absolute path, a leading slash `/` is prepended if
* not already there.
*
* The query component is set through either [query] or
* [queryParameters]. When [query] is used the provided string should
* be a valid URI query, but invalid characters other than general delimiters,
* The query component is set through either [query] or [queryParameters].
* When [query] is used, the provided string should be a valid URI query,
* but invalid characters, other than general delimiters,
* will be escaped if necessary.
* When [queryParameters] is used the query is built from the
* provided map. Each key and value in the map is percent-encoded
* and joined using equal and ampersand characters. The
* percent-encoding of the keys and values encodes all characters
* except for the unreserved characters.
* and joined using equal and ampersand characters.
* A value in the map must be either a string, or an [Iterable] of strings,
* where the latter corresponds to multiple values for the same key.
*
* The percent-encoding of the keys and values encodes all characters
* except for the unreserved characters, and replaces spaces with `+`.
* If `query` is the empty string, it is equivalent to omitting it.
* To have an actual empty query part,
* use an empty list for `queryParameters`.
* If both `query` and `queryParameters` are omitted or `null`, the
* URI will have no query part.
*
* If both `query` and `queryParameters` are omitted or `null`,
* the URI has no query part.
*
* The fragment component is set through [fragment].
* It should be a valid URI fragment, but invalid characters other than
* general delimiters, will be escaped if necessary.
* If `fragment` is omitted or `null`, the URI will have no fragment part.
* general delimiters, are escaped if necessary.
* If `fragment` is omitted or `null`, the URI has no fragment part.
*/
factory Uri({String scheme : "",
String userInfo : "",
@ -157,7 +165,7 @@ class Uri {
String path,
Iterable<String> pathSegments,
String query,
Map<String, String> queryParameters,
Map<String, dynamic> queryParameters,
String fragment}) {
scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme));
userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo));
@ -207,7 +215,7 @@ class Uri {
*
* The `userInfo`, `host` and `port` components are set from the
* [authority] argument. If `authority` is `null` or empty,
* the created `Uri` will have no authority, and will not be directly usable
* the created `Uri` has no authority, and isn't directly usable
* as an HTTP URL, which must have a non-empty host.
*
* The `path` component is set from the [unencodedPath]
@ -1104,22 +1112,52 @@ class Uri {
* Returns the URI query split into a map according to the rules
* specified for FORM post in the [HTML 4.01 specification section
* 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4").
* Each key and value in the returned map has been decoded. If there is no
* query the empty map is returned.
* Each key and value in the returned map has been decoded.
* If there is no query the empty map is returned.
*
* Keys in the query string that have no value are mapped to the
* empty string.
* If a key occurs more than once in the query string, it is mapped to
* an arbitrary choice of possible value.
* The [queryParametersAll] getter can provide a map
* that maps keys to all of their values.
*
* The returned map is unmodifiable and will throw [UnsupportedError] on any
* calls that would mutate it.
* The returned map is unmodifiable.
*/
Map<String, String> get queryParameters {
if (_queryParameters == null) {
_queryParameters = new UnmodifiableMapView(splitQueryString(query));
_queryParameters =
new UnmodifiableMapView<String, String>(splitQueryString(query));
}
return _queryParameters;
}
/**
* Returns the URI query split into a map according to the rules
* specified for FORM post in the [HTML 4.01 specification section
* 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4").
* Each key and value in the returned map has been decoded. If there is no
* query the empty map is returned.
*
* Keys are mapped to lists of their values. If a key occurs only once,
* its value is a singleton list. If a key occurs with no value, the
* empty string is used as the value for that occurrence.
*
* The returned map and the lists it contains are unmodifiable.
*/
Map<String, List<String>> get queryParametersAll {
if (_queryParameterLists == null) {
Map queryParameterLists = _splitQueryStringAll(query);
for (var key in queryParameterLists.keys) {
queryParameterLists[key] =
new List<String>.unmodifiable(queryParameterLists[key]);
}
_queryParameterLists =
new Map<String, List<String>>.unmodifiable(queryParameterLists);
}
return _queryParameterLists;
}
/**
* Returns a URI where the path has been normalized.
*
@ -1344,17 +1382,27 @@ class Uri {
if (query != null) return _normalize(query, start, end, _queryCharTable);
var result = new StringBuffer();
var first = true;
queryParameters.forEach((key, value) {
if (!first) {
result.write("&");
}
first = false;
var separator = "";
void writeParameter(String key, String value) {
result.write(separator);
separator = "&";
result.write(Uri.encodeQueryComponent(key));
if (value != null && !value.isEmpty) {
if (value != null && value.isNotEmpty) {
result.write("=");
result.write(Uri.encodeQueryComponent(value));
}
}
queryParameters.forEach((key, value) {
if (value == null || value is String) {
writeParameter(key, value);
} else {
Iterable values = value;
for (String value in values) {
writeParameter(key, value);
}
}
});
return result.toString();
}
@ -2156,6 +2204,46 @@ class Uri {
});
}
static List _createList() => [];
static Map _splitQueryStringAll(
String query, {Encoding encoding: UTF8}) {
Map result = {};
int i = 0;
int start = 0;
int equalsIndex = -1;
void parsePair(int start, int equalsIndex, int end) {
String key;
String value;
if (start == end) return;
if (equalsIndex < 0) {
key = _uriDecode(query, start, end, encoding, true);
value = "";
} else {
key = _uriDecode(query, start, equalsIndex, encoding, true);
value = _uriDecode(query, equalsIndex + 1, end, encoding, true);
}
result.putIfAbsent(key, _createList).add(value);
}
const int _equals = 0x3d;
const int _ampersand = 0x26;
while (i < query.length) {
int char = query.codeUnitAt(i);
if (char == _equals) {
if (equalsIndex < 0) equalsIndex = i;
} else if (char == _ampersand) {
parsePair(start, equalsIndex, i);
start = i + 1;
equalsIndex = -1;
}
i++;
}
parsePair(start, equalsIndex, i);
return result;
}
/**
* Parse the [host] as an IP version 4 (IPv4) address, returning the address
* as a list of 4 bytes in network byte order (big endian).

View file

@ -0,0 +1,39 @@
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import "package:expect/expect.dart";
import 'dart:convert';
main() {
testAll(["a", "b", "c"]);
testAll([""]);
testAll(["a"]);
testAll(["",""]);
testAll(["baz"]);
testParse("z&y&w&z", {"z": ["", ""], "y": [""], "w": [""]});
testParse("x=42&y=42&x=37&y=37", {"x": ["42", "37"], "y": ["42", "37"]});
testParse("x&x&x&x&x", {"x": ["", "", "", "", ""]});
testParse("x=&&y", {"x": [""], "y": [""]});
}
testAll(List values) {
var uri = new Uri(scheme: "foo", path: "bar",
queryParameters: {"baz": values});
var list = uri.queryParametersAll["baz"];
Expect.listEquals(values, list);
}
testParse(query, results) {
var uri = new Uri(scheme: "foo", path: "bar", query: query);
var params = uri.queryParametersAll;
for (var k in results.keys) {
Expect.listEquals(results[k], params[k]);
}
uri = new Uri(scheme: "foo", path: "bar", queryParameters: results);
params = uri.queryParametersAll;
for (var k in results.keys) {
Expect.listEquals(results[k], params[k]);
}
}