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 @@ -528,6 +528,7 @@ 'nonempty' => 'utils/utils.php', 'phlog' => 'error/phlog.php', 'pht' => 'internationalization/pht.php', + 'phutil_build_http_querystring' => 'utils/utils.php', 'phutil_censor_credentials' => 'utils/utils.php', 'phutil_console_confirm' => 'console/format.php', 'phutil_console_format' => 'console/format.php', diff --git a/src/future/http/BaseHTTPFuture.php b/src/future/http/BaseHTTPFuture.php --- a/src/future/http/BaseHTTPFuture.php +++ b/src/future/http/BaseHTTPFuture.php @@ -273,7 +273,7 @@ return strlen($data); } - return strlen(http_build_query($data, '', '&')); + return strlen(phutil_build_http_querystring($data)); } diff --git a/src/future/http/HTTPFuture.php b/src/future/http/HTTPFuture.php --- a/src/future/http/HTTPFuture.php +++ b/src/future/http/HTTPFuture.php @@ -244,7 +244,7 @@ if ($this->getMethod() == 'GET') { if (is_array($data)) { - $data = http_build_query($data, '', '&'); + $data = phutil_build_http_querystring($data); if (strpos($uri, '?') !== false) { $uri .= '&'.$data; } else { @@ -254,7 +254,7 @@ } } else { if (is_array($data)) { - $data = http_build_query($data, '', '&')."\r\n"; + $data = phutil_build_http_querystring($data)."\r\n"; $add_headers[] = array( 'Content-Type', 'application/x-www-form-urlencoded', diff --git a/src/future/http/HTTPSFuture.php b/src/future/http/HTTPSFuture.php --- a/src/future/http/HTTPSFuture.php +++ b/src/future/http/HTTPSFuture.php @@ -523,7 +523,7 @@ // If we don't have any files, just encode the data as a query string, // make sure it's not including any files, and we're good to go. if (is_array($data)) { - $data = http_build_query($data, '', '&'); + $data = phutil_build_http_querystring($data); } $this->checkForDangerousCURLMagic($data, $is_query_string = true); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php @@ -11,7 +11,7 @@ return sprintf( "Content: (%s)\nArgv: (%s)", $content, - http_build_query($argv)); + phutil_build_http_querystring($argv)); } } diff --git a/src/parser/PhutilURI.php b/src/parser/PhutilURI.php --- a/src/parser/PhutilURI.php +++ b/src/parser/PhutilURI.php @@ -174,7 +174,7 @@ } if ($this->query) { - $query = '?'.http_build_query($this->query, '', '&'); + $query = '?'.phutil_build_http_querystring($this->query); } else { $query = null; } diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -882,4 +882,43 @@ return array_select_keys($map, $keys); } + public function testQueryStringEncoding() { + $expect = array(); + + // As a starting point, we expect every character to encode as an "%XX" + // escaped version. + foreach (range(0, 255) as $byte) { + $c = chr($byte); + $expect[$c] = sprintf('%%%02X', $byte); + } + + // We expect these characters to not be escaped. + $ranges = array( + range('a', 'z'), + range('A', 'Z'), + range('0', '9'), + array('-', '.', '_', '~'), + ); + + foreach ($ranges as $range) { + foreach ($range as $preserve_char) { + $expect[$preserve_char] = $preserve_char; + } + } + + foreach (range(0, 255) as $byte) { + $c = chr($byte); + + $expect_c = $expect[$c]; + $expect_str = "{$expect_c}={$expect_c}"; + + $actual_str = phutil_build_http_querystring(array($c => $c)); + + $this->assertEqual( + $expect_str, + $actual_str, + pht('HTTP querystring for byte "%s".', sprintf('0x%02x', $byte))); + } + } + } diff --git a/src/utils/utils.php b/src/utils/utils.php --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1537,3 +1537,24 @@ return ($bits === 0); } + + +/** + * Build a query string from a dictionary. + * + * @param map Dictionary of parameters. + * @return string HTTP query string. + */ +function phutil_build_http_querystring(array $parameters) { + // 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) { + $query[] = rawurlencode($key).'='.rawurlencode($value); + } + $query = implode($query, '&'); + + return $query; +}