Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15401289
D20136.id48077.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
9 KB
Referenced Files
None
Subscribers
None
D20136.id48077.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -529,6 +529,7 @@
'phlog' => 'error/phlog.php',
'pht' => 'internationalization/pht.php',
'phutil_build_http_querystring' => 'utils/utils.php',
+ 'phutil_build_http_querystring_from_pairs' => 'utils/utils.php',
'phutil_censor_credentials' => 'utils/utils.php',
'phutil_console_confirm' => 'console/format.php',
'phutil_console_format' => 'console/format.php',
@@ -559,6 +560,7 @@
'phutil_get_signal_name' => 'future/exec/execx.php',
'phutil_get_system_locale' => 'utils/utf8.php',
'phutil_hashes_are_identical' => 'utils/utils.php',
+ 'phutil_http_parameter_pair' => 'utils/utils.php',
'phutil_implode_html' => 'markup/render.php',
'phutil_ini_decode' => 'utils/utils.php',
'phutil_is_hiphop_runtime' => 'utils/utils.php',
diff --git a/src/parser/PhutilURI.php b/src/parser/PhutilURI.php
--- a/src/parser/PhutilURI.php
+++ b/src/parser/PhutilURI.php
@@ -123,8 +123,13 @@
$this->path = idx($parts, 'path', '');
$query = idx($parts, 'query');
if ($query) {
- $this->query = id(new PhutilQueryStringParser())->parseQueryString(
- $query);
+ $pairs = id(new PhutilQueryStringParser())
+ ->parseQueryStringToPairList($query);
+
+ foreach ($pairs as $pair) {
+ list($key, $value) = $pair;
+ $this->appendQueryParam($key, $value);
+ }
}
$this->fragment = idx($parts, 'fragment', '');
@@ -174,7 +179,7 @@
}
if ($this->query) {
- $query = '?'.phutil_build_http_querystring($this->query);
+ $query = '?'.phutil_build_http_querystring_from_pairs($this->query);
} else {
$query = null;
}
@@ -196,23 +201,91 @@
}
public function setQueryParam($key, $value) {
- if ($value === null) {
- unset($this->query[$key]);
- } else {
- $this->query[$key] = $value;
+ // To set, we replace the first matching key with the new value, then
+ // remove all other matching keys. This replaces the old value and retains
+ // the parameter order.
+
+ $is_null = ($value === null);
+
+ // Typecheck and cast the key before we compare it to existing keys. This
+ // raises an early exception if the key has a bad type.
+ list($key) = phutil_http_parameter_pair($key, '');
+
+ $found = false;
+ foreach ($this->query as $list_key => $pair) {
+ list($k, $v) = $pair;
+
+ if ($k !== $key) {
+ continue;
+ }
+
+ if ($found) {
+ unset($this->query[$list_key]);
+ continue;
+ }
+
+ $found = true;
+
+ if ($is_null) {
+ unset($this->query[$list_key]);
+ } else {
+ $this->insertQueryParam($key, $value, $list_key);
+ }
+ }
+
+ $this->query = array_values($this->query);
+
+ // If we didn't find an existing place to put it, add it to the end.
+ if (!$found) {
+ if (!$is_null) {
+ $this->appendQueryParam($key, $value);
+ }
}
+
return $this;
}
public function setQueryParams(array $params) {
- $this->query = $params;
+ $this->query = array();
+
+ foreach ($params as $k => $v) {
+ $this->appendQueryParam($k, $v);
+ }
+
return $this;
}
public function getQueryParams() {
+ $map = array();
+
+ foreach ($this->query as $pair) {
+ list($k, $v) = $pair;
+ $map[$k] = $v;
+ }
+
+ return $map;
+ }
+
+ public function getQueryParamsAsPairList() {
return $this->query;
}
+ public function appendQueryParam($key, $value) {
+ return $this->insertQueryParam($key, $value);
+ }
+
+ private function insertQueryParam($key, $value, $idx = null) {
+ list($key, $value) = phutil_http_parameter_pair($key, $value);
+
+ if ($idx === null) {
+ $this->query[] = array($key, $value);
+ } else {
+ $this->query[$idx] = array($key, $value);
+ }
+
+ return $this;
+ }
+
public function setProtocol($protocol) {
$this->protocol = $protocol;
return $this;
diff --git a/src/parser/__tests__/PhutilURITestCase.php b/src/parser/__tests__/PhutilURITestCase.php
--- a/src/parser/__tests__/PhutilURITestCase.php
+++ b/src/parser/__tests__/PhutilURITestCase.php
@@ -275,4 +275,101 @@
$this->assertTrue($caught instanceof Exception);
}
+ public function testDuplicateKeys() {
+ $uri = new PhutilURI('http://www.example.com/?x=1&x=2');
+ $this->assertEqual(
+ 'http://www.example.com/?x=1&x=2',
+ (string)$uri);
+
+ $uri->appendQueryParam('x', '3');
+ $this->assertEqual(
+ 'http://www.example.com/?x=1&x=2&x=3',
+ (string)$uri);
+
+ $uri->setQueryParam('x', '4');
+ $this->assertEqual(
+ 'http://www.example.com/?x=4',
+ (string)$uri);
+
+ $uri->setQueryParam('x', null);
+ $this->assertEqual(
+ 'http://www.example.com/',
+ (string)$uri);
+
+ $uri->appendQueryParam('a', 'a');
+ $uri->appendQueryParam('b', 'b');
+ $uri->appendQueryParam('c', 'c');
+ $uri->appendQueryParam('b', 'd');
+
+ $this->assertEqual(
+ 'http://www.example.com/?a=a&b=b&c=c&b=d',
+ (string)$uri);
+
+ $uri->setQueryParam('b', 'e');
+ $this->assertEqual(
+ 'http://www.example.com/?a=a&b=e&c=c',
+ (string)$uri,
+ pht(
+ 'Replacing a parameter should retain position and overwrite other '.
+ 'instances of the key.'));
+ }
+
+ public function testBadHTTPParameters() {
+ $uri = new PhutilURI('http://www.example.com/');
+
+ $caught = null;
+ try {
+ $uri->setQueryParam(array(), 'x');
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertTrue(
+ (bool)$caught,
+ pht('Nonscalar HTTP keys should throw.'));
+
+ $caught = null;
+ try {
+ $uri->setQueryParam('x', array());
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertTrue(
+ (bool)$caught,
+ pht('Nonscalar HTTP values should throw.'));
+ }
+
+ public function testHTTPParameterTypes() {
+ // Whether you pass an integer or string, "0" should always be the same
+ // query parameter.
+
+ $uri = new PhutilURI('http://www.example.com/');
+
+ $uri->appendQueryParam(0, 'a');
+ $uri->appendQueryParam('0', 'b');
+ $this->assertEqual(
+ 'http://www.example.com/?0=a&0=b',
+ (string)$uri);
+
+ $uri->setQueryParam(0, 'c');
+ $this->assertEqual(
+ 'http://www.example.com/?0=c',
+ (string)$uri);
+
+ $uri->setQueryParam(0, 'a');
+ $uri->appendQueryParam('0', 'b');
+ $this->assertEqual(
+ 'http://www.example.com/?0=a&0=b',
+ (string)$uri);
+
+ $uri->setQueryParam('0', 'c');
+ $this->assertEqual(
+ 'http://www.example.com/?0=c',
+ (string)$uri);
+ }
+
+
+
+
}
diff --git a/src/utils/utils.php b/src/utils/utils.php
--- a/src/utils/utils.php
+++ b/src/utils/utils.php
@@ -1561,19 +1561,37 @@
* @return string HTTP query string.
*/
function phutil_build_http_querystring(array $parameters) {
+ $pairs = array();
+ foreach ($parameters as $key => $value) {
+ $pairs[] = array($key, $value);
+ }
+
+ return phutil_build_http_querystring_from_pairs($pairs);
+}
+
+/**
+ * Build a query string from a list of parameter pairs.
+ *
+ * @param list<pair<string, string>> List of pairs.
+ * @return string HTTP query string.
+ */
+function phutil_build_http_querystring_from_pairs(array $pairs) {
// We want to encode in RFC3986 mode, but "http_build_query()" did not get
// a flag for that mode until PHP 5.4.0. This is equivalent to calling
// "http_build_query()" with the "PHP_QUERY_RFC3986" flag.
$query = array();
- foreach ($parameters as $key => $value) {
- if (is_array($value) || is_object($value)) {
+ foreach ($pairs as $pair_key => $pair) {
+ if (!is_array($pair) || (count($pair) !== 2)) {
throw new Exception(
pht(
- 'HTTP query parameter (with key "%s") is not a scalar. Parameters '.
- 'must all be scalars.',
- $key));
+ 'HTTP parameter pair (with key "%s") is not valid: each pair must '.
+ 'be an array with exactly two elements.',
+ $pair_key));
}
+
+ list($key, $value) = $pair;
+ list($key, $value) = phutil_http_parameter_pair($key, $value);
$query[] = rawurlencode($key).'='.rawurlencode($value);
}
$query = implode($query, '&');
@@ -1581,6 +1599,41 @@
return $query;
}
+/**
+ * Typecheck and cast an HTTP key-value parameter pair.
+ *
+ * Scalar values are converted to strings. Nonscalar values raise exceptions.
+ *
+ * @param scalar HTTP parameter key.
+ * @param scalar HTTP parameter value.
+ * @return pair<string, string> Key and value as strings.
+ */
+function phutil_http_parameter_pair($key, $value) {
+ try {
+ assert_stringlike($key);
+ } catch (InvalidArgumentException $ex) {
+ throw new PhutilProxyException(
+ pht('HTTP query parameter key must be a scalar.'),
+ $ex);
+ }
+
+ $key = (string)$key;
+
+ try {
+ assert_stringlike($value);
+ } catch (InvalidArgumentException $ex) {
+ throw new PhutilProxyException(
+ pht(
+ 'HTTP query parameter value (for key "%s") must be a scalar.',
+ $key),
+ $ex);
+ }
+
+ $value = (string)$value;
+
+ return array($key, $value);
+}
+
function phutil_decode_mime_header($header) {
if (function_exists('iconv_mime_decode')) {
return iconv_mime_decode($header, 0, 'UTF-8');
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Mar 18, 4:49 PM (5 d, 15 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7466349
Default Alt Text
D20136.id48077.diff (9 KB)
Attached To
Mode
D20136: Fix destructive representation of "?x=1&x=2" in PhutilURI so it can raise errors immediately
Attached
Detach File
Event Timeline
Log In to Comment