diff --git a/ext/lexbor/lexbor/url/url.c b/ext/lexbor/lexbor/url/url.c
index 1fd044bbf950..9c8b764c4c1e 100644
--- a/ext/lexbor/lexbor/url/url.c
+++ b/ext/lexbor/lexbor/url/url.c
@@ -613,10 +613,6 @@ lxb_url_percent_decode(const lxb_char_t *data, const lxb_char_t *end,
lexbor_str_t *str, lexbor_mraw_t *mraw,
lxb_url_host_opt_t *opt);
-static lxb_status_t
-lxb_url_percent_decode_plus(const lxb_char_t *data, const lxb_char_t *end,
- lexbor_str_t *str, lexbor_mraw_t *mraw);
-
static const lxb_char_t *
lxb_url_path_part_by_index(const lxb_url_t *url, size_t index,
size_t *out_length);
@@ -4071,7 +4067,7 @@ lxb_url_percent_decode(const lxb_char_t *data, const lxb_char_t *end,
return LXB_STATUS_OK;
}
-static lxb_status_t
+lxb_status_t
lxb_url_percent_decode_plus(const lxb_char_t *data, const lxb_char_t *end,
lexbor_str_t *str, lexbor_mraw_t *mraw)
{
diff --git a/ext/lexbor/lexbor/url/url.h b/ext/lexbor/lexbor/url/url.h
index 6cc6f1081c8a..82470fe1559b 100644
--- a/ext/lexbor/lexbor/url/url.h
+++ b/ext/lexbor/lexbor/url/url.h
@@ -854,6 +854,10 @@ lxb_url_get(lxb_url_parser_t *parser)
return parser->url;
}
+lxb_status_t
+lxb_url_percent_decode_plus(const lxb_char_t *data, const lxb_char_t *end,
+ lexbor_str_t *str, lexbor_mraw_t *mraw);
+
#ifdef __cplusplus
} /* extern "C" */
diff --git a/ext/uri/config.m4 b/ext/uri/config.m4
index a518cf84b3bd..bce573078dab 100644
--- a/ext/uri/config.m4
+++ b/ext/uri/config.m4
@@ -12,6 +12,7 @@ PHP_INSTALL_HEADERS([ext/uri], m4_normalize([
php_uri.h
php_uri_common.h
php_uri_decl.h
+ php_uri_query.h
uri_parser_rfc3986.h
uri_parser_whatwg.h
uri_parser_php_parse_url.h
@@ -39,7 +40,7 @@ else
PHP_EVAL_INCLINE([$LIBURIPARSER_CFLAGS])
fi
-PHP_NEW_EXTENSION(uri, [php_uri.c php_uri_common.c uri_parser_rfc3986.c uri_parser_whatwg.c uri_parser_php_parse_url.c $URIPARSER_SOURCES], [no],,[$URI_CFLAGS])
+PHP_NEW_EXTENSION(uri, [php_uri.c php_uri_common.c php_uri_query.c uri_parser_rfc3986.c uri_parser_whatwg.c uri_parser_php_parse_url.c $URIPARSER_SOURCES], [no],,[$URI_CFLAGS])
PHP_ADD_EXTENSION_DEP(uri, lexbor)
if test "$PHP_EXTERNAL_URIPARSER" = "no"; then
diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c
index 74a559fd591c..283750d95d05 100644
--- a/ext/uri/php_uri.c
+++ b/ext/uri/php_uri.c
@@ -24,6 +24,7 @@
#include "ext/standard/info.h"
#include "php_uri.h"
+#include "php_uri_query.h"
#include "uri_parser_whatwg.h"
#include "uri_parser_rfc3986.h"
#include "uri_parser_php_parse_url.h"
@@ -39,6 +40,8 @@ zend_class_entry *php_uri_ce_comparison_mode;
zend_class_entry *php_uri_ce_exception;
zend_class_entry *php_uri_ce_error;
zend_class_entry *php_uri_ce_invalid_uri_exception;
+zend_class_entry *php_uri_ce_query_params;
+zend_class_entry *php_uri_ce_query_param_options;
zend_class_entry *php_uri_ce_whatwg_url_host_type;
zend_class_entry *php_uri_ce_whatwg_invalid_url_exception;
zend_class_entry *php_uri_ce_whatwg_url_validation_error_type;
@@ -46,6 +49,8 @@ zend_class_entry *php_uri_ce_whatwg_url_validation_error;
static zend_object_handlers object_handlers_rfc3986_uri;
static zend_object_handlers object_handlers_whatwg_uri;
+static zend_object_handlers object_handlers_query_params;
+static zend_object_handlers object_handlers_query_param_options;
typedef zend_result (*php_uri_component_validator_string)(const zend_string *component);
typedef zend_result (*php_uri_component_validator_long)(zend_long component);
@@ -1312,6 +1317,19 @@ PHPAPI zend_result php_uri_parser_register(const php_uri_parser *uri_parser)
return result;
}
+static PHP_MINFO_FUNCTION(uri)
+{
+ php_info_print_table_start();
+ php_info_print_table_row(2, "URI support", "active");
+#ifdef URI_STATIC_BUILD
+ php_info_print_table_row(2, "uriparser bundled version", URI_VER_ANSI);
+#else
+ php_info_print_table_row(2, "uriparser compiled version", URI_VER_ANSI);
+ php_info_print_table_row(2, "uriparser loaded version", uriBaseRuntimeVersionA());
+#endif
+ php_info_print_table_end();
+}
+
static PHP_MINIT_FUNCTION(uri)
{
php_uri_ce_rfc3986_uri_builder = register_class_Uri_Rfc3986_UriBuilder();
@@ -1339,6 +1357,28 @@ static PHP_MINIT_FUNCTION(uri)
php_uri_ce_exception = register_class_Uri_UriException(zend_ce_exception);
php_uri_ce_error = register_class_Uri_UriError(zend_ce_error);
php_uri_ce_invalid_uri_exception = register_class_Uri_InvalidUriException(php_uri_ce_exception);
+
+ php_uri_ce_query_params = register_class_Uri_QueryParams(zend_ce_aggregate, zend_ce_countable);
+ php_uri_ce_query_params->create_object = php_uri_object_query_params_create_object;
+ php_uri_ce_query_params->get_iterator = php_uri_object_query_params_get_iterator;
+ php_uri_ce_query_params->default_object_handlers = &object_handlers_query_params;
+ memcpy(&object_handlers_query_params, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
+ object_handlers_query_params.offset = offsetof(php_uri_query_params_object, std);
+ object_handlers_query_params.free_obj = php_uri_query_params_object_handler_free;
+ object_handlers_query_params.clone_obj = php_uri_query_params_object_handler_clone;
+
+ php_uri_ce_query_param_options = register_class_Uri_QueryParamOptions();
+ php_uri_ce_query_param_options->create_object = php_uri_object_create_query_param_options;
+ memcpy(&object_handlers_query_param_options, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
+ object_handlers_query_param_options.offset = offsetof(php_uri_query_param_options_object, std);
+ object_handlers_query_param_options.free_obj = php_uri_query_param_options_object_handler_free;
+ object_handlers_query_param_options.clone_obj = php_uri_query_param_options_object_handler_clone;
+ object_handlers_query_param_options.has_property = php_uri_query_param_options_object_handler_has_property;
+ object_handlers_query_param_options.read_property = php_uri_query_param_options_object_handler_read_property;
+ object_handlers_query_param_options.write_property = php_uri_query_param_options_object_handler_write_property;
+ object_handlers_query_param_options.unset_property = php_uri_query_param_options_object_handler_unset_property;
+ object_handlers_query_param_options.get_property_ptr_ptr = php_uri_query_param_options_object_handler_get_property_ptr_ptr;
+
php_uri_ce_whatwg_invalid_url_exception = register_class_Uri_WhatWg_InvalidUrlException(php_uri_ce_invalid_uri_exception);
php_uri_ce_whatwg_url_host_type = register_class_Uri_WhatWg_UrlHostType();
php_uri_ce_whatwg_url_validation_error = register_class_Uri_WhatWg_UrlValidationError();
@@ -1361,19 +1401,6 @@ static PHP_MINIT_FUNCTION(uri)
return SUCCESS;
}
-static PHP_MINFO_FUNCTION(uri)
-{
- php_info_print_table_start();
- php_info_print_table_row(2, "URI support", "active");
-#ifdef URI_STATIC_BUILD
- php_info_print_table_row(2, "uriparser bundled version", URI_VER_ANSI);
-#else
- php_info_print_table_row(2, "uriparser compiled version", URI_VER_ANSI);
- php_info_print_table_row(2, "uriparser loaded version", uriBaseRuntimeVersionA());
-#endif
- php_info_print_table_end();
-}
-
static PHP_MSHUTDOWN_FUNCTION(uri)
{
zend_hash_destroy(&uri_parsers);
diff --git a/ext/uri/php_uri.stub.php b/ext/uri/php_uri.stub.php
index d00ef45cb86e..be4c1674cfc0 100644
--- a/ext/uri/php_uri.stub.php
+++ b/ext/uri/php_uri.stub.php
@@ -26,6 +26,85 @@ enum UriComparisonMode
case IncludeFragment;
case ExcludeFragment;
}
+
+ final readonly class QueryParamOptions
+ {
+ /** @virtual */
+ public int $parsingMaxQueryStringLength;
+ /** @virtual */
+ public int $parsingMaxParamCount;
+ /** @virtual */
+ public string $trueValue;
+ /** @virtual */
+ public string $falseValue;
+ /** @virtual */
+ public bool $useNullAsEmptyString;
+
+ public function __construct(
+ int $parsingMaxQueryStringLength = 10000,
+ int $parsingMaxParamCount = 1000,
+ string $trueValue = "0",
+ string $falseValue = "0",
+ bool $useNullAsEmptyString = false
+ ) {}
+
+ public function __debugInfo(): array {}
+ }
+
+ final readonly class QueryParams implements \IteratorAggregate, \Countable
+ {
+ private QueryParamOptions $options;
+
+ public static function parseRfc1866(string $queryString, ?QueryParamOptions $options = null): \Uri\QueryParams {}
+
+ public static function parseRfc3986(string $queryString, ?QueryParamOptions $options = null): ?\Uri\QueryParams {}
+
+ public static function parseWhatWg(string $queryString, ?QueryParamOptions $options = null): \Uri\QueryParams {}
+
+ public static function fromArray(array $queryParams, ?QueryParamOptions $options = null): \Uri\QueryParams {}
+
+ public function __construct(?QueryParamOptions $options = null) {}
+
+ public function append(string $name, mixed $value): static {}
+
+ public function delete(string $name): static {}
+
+ public function deleteValue(string $name, ?string $value): static {}
+
+ public function has(string $name): bool {}
+
+ public function hasValue(string $name, mixed $value): bool {}
+
+ public function getFirst(string $name): ?string {}
+
+ public function getLast(string $name): ?string {}
+
+ public function getAll(string $name): array {}
+
+ public function list(): array {}
+
+ public function getIterator(): \Traversable {}
+
+ public function count(): int {}
+
+ public function set(string $name, mixed $value): static {}
+
+ public function sort(): static {}
+
+ public function toArray(): array {}
+
+ public function toRfc1866String(): string {}
+
+ public function toRfc3986String(): string {}
+
+ public function toWhatWgString(): string {}
+
+ public function __serialize(): array {}
+
+ public function __unserialize(array $data): void {}
+
+ public function __debugInfo(): array {}
+ }
}
namespace Uri\Rfc3986 {
diff --git a/ext/uri/php_uri_arginfo.h b/ext/uri/php_uri_arginfo.h
index e63e495526f5..2fc47c47f575 100644
--- a/ext/uri/php_uri_arginfo.h
+++ b/ext/uri/php_uri_arginfo.h
@@ -1,10 +1,104 @@
/* This is a generated file, edit php_uri.stub.php instead.
- * Stub hash: 1a396b7a5b170b968d84a2c64a5bcf7d962653db
+ * Stub hash: 5c99026c91273c8094ebbfb6809141649fc13c60
* Has decl header: yes */
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_UriBuilder_reset, 0, 0, IS_STATIC, 0)
+ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Uri_QueryParamOptions___construct, 0, 0, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, parsingMaxQueryStringLength, IS_LONG, 0, "10000")
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, parsingMaxParamCount, IS_LONG, 0, "1000")
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, trueValue, IS_STRING, 0, "\"0\"")
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, falseValue, IS_STRING, 0, "\"0\"")
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, useNullAsEmptyString, _IS_BOOL, 0, "false")
ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParamOptions___debugInfo, 0, 0, IS_ARRAY, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Uri_QueryParams_parseRfc1866, 0, 1, Uri\\QueryParams, 0)
+ ZEND_ARG_TYPE_INFO(0, queryString, IS_STRING, 0)
+ ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, options, Uri\\QueryParamOptions, 1, "null")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Uri_QueryParams_parseRfc3986, 0, 1, Uri\\QueryParams, 1)
+ ZEND_ARG_TYPE_INFO(0, queryString, IS_STRING, 0)
+ ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, options, Uri\\QueryParamOptions, 1, "null")
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_QueryParams_parseWhatWg arginfo_class_Uri_QueryParams_parseRfc1866
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Uri_QueryParams_fromArray, 0, 1, Uri\\QueryParams, 0)
+ ZEND_ARG_TYPE_INFO(0, queryParams, IS_ARRAY, 0)
+ ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, options, Uri\\QueryParamOptions, 1, "null")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Uri_QueryParams___construct, 0, 0, 0)
+ ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, options, Uri\\QueryParamOptions, 1, "null")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_append, 0, 2, IS_STATIC, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_delete, 0, 1, IS_STATIC, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_deleteValue, 0, 2, IS_STATIC, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 1)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_has, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_hasValue, 0, 2, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_getFirst, 0, 1, IS_STRING, 1)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_QueryParams_getLast arginfo_class_Uri_QueryParams_getFirst
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_getAll, 0, 1, IS_ARRAY, 0)
+ ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_QueryParams_list arginfo_class_Uri_QueryParamOptions___debugInfo
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Uri_QueryParams_getIterator, 0, 0, Traversable, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_count, 0, 0, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_QueryParams_set arginfo_class_Uri_QueryParams_append
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_sort, 0, 0, IS_STATIC, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_QueryParams_toArray arginfo_class_Uri_QueryParamOptions___debugInfo
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams_toRfc1866String, 0, 0, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_QueryParams_toRfc3986String arginfo_class_Uri_QueryParams_toRfc1866String
+
+#define arginfo_class_Uri_QueryParams_toWhatWgString arginfo_class_Uri_QueryParams_toRfc1866String
+
+#define arginfo_class_Uri_QueryParams___serialize arginfo_class_Uri_QueryParamOptions___debugInfo
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_QueryParams___unserialize, 0, 1, IS_VOID, 0)
+ ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_QueryParams___debugInfo arginfo_class_Uri_QueryParamOptions___debugInfo
+
+#define arginfo_class_Uri_Rfc3986_UriBuilder_reset arginfo_class_Uri_QueryParams_sort
+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_UriBuilder_setScheme, 0, 1, IS_STATIC, 0)
ZEND_ARG_TYPE_INFO(0, scheme, IS_STRING, 1)
ZEND_END_ARG_INFO()
@@ -87,10 +181,9 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Uri_Rfc3986_Uri_withPort arginfo_class_Uri_Rfc3986_UriBuilder_setPort
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_Uri_getPath, 0, 0, IS_STRING, 0)
-ZEND_END_ARG_INFO()
+#define arginfo_class_Uri_Rfc3986_Uri_getPath arginfo_class_Uri_QueryParams_toRfc1866String
-#define arginfo_class_Uri_Rfc3986_Uri_getRawPath arginfo_class_Uri_Rfc3986_Uri_getPath
+#define arginfo_class_Uri_Rfc3986_Uri_getRawPath arginfo_class_Uri_QueryParams_toRfc1866String
#define arginfo_class_Uri_Rfc3986_Uri_withPath arginfo_class_Uri_Rfc3986_UriBuilder_setPath
@@ -111,22 +204,19 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_Uri_equals, 0,
ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, comparisonMode, Uri\\\125riComparisonMode, 0, "Uri\\UriComparisonMode::ExcludeFragment")
ZEND_END_ARG_INFO()
-#define arginfo_class_Uri_Rfc3986_Uri_toString arginfo_class_Uri_Rfc3986_Uri_getPath
+#define arginfo_class_Uri_Rfc3986_Uri_toString arginfo_class_Uri_QueryParams_toRfc1866String
-#define arginfo_class_Uri_Rfc3986_Uri_toRawString arginfo_class_Uri_Rfc3986_Uri_getPath
+#define arginfo_class_Uri_Rfc3986_Uri_toRawString arginfo_class_Uri_QueryParams_toRfc1866String
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_Uri_resolve, 0, 1, IS_STATIC, 0)
ZEND_ARG_TYPE_INFO(0, uri, IS_STRING, 0)
ZEND_END_ARG_INFO()
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_Uri___serialize, 0, 0, IS_ARRAY, 0)
-ZEND_END_ARG_INFO()
+#define arginfo_class_Uri_Rfc3986_Uri___serialize arginfo_class_Uri_QueryParamOptions___debugInfo
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_Uri___unserialize, 0, 1, IS_VOID, 0)
- ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0)
-ZEND_END_ARG_INFO()
+#define arginfo_class_Uri_Rfc3986_Uri___unserialize arginfo_class_Uri_QueryParams___unserialize
-#define arginfo_class_Uri_Rfc3986_Uri___debugInfo arginfo_class_Uri_Rfc3986_Uri___serialize
+#define arginfo_class_Uri_Rfc3986_Uri___debugInfo arginfo_class_Uri_QueryParamOptions___debugInfo
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Uri_WhatWg_InvalidUrlException___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, message, IS_STRING, 0, "\"\"")
@@ -153,7 +243,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Uri_WhatWg_Url___construct, 0, 0, 1)
ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, softErrors, "null")
ZEND_END_ARG_INFO()
-#define arginfo_class_Uri_WhatWg_Url_getScheme arginfo_class_Uri_Rfc3986_Uri_getPath
+#define arginfo_class_Uri_WhatWg_Url_getScheme arginfo_class_Uri_QueryParams_toRfc1866String
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_Url_withScheme, 0, 1, IS_STATIC, 0)
ZEND_ARG_TYPE_INFO(0, scheme, IS_STRING, 0)
@@ -187,7 +277,7 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Uri_WhatWg_Url_withPort arginfo_class_Uri_Rfc3986_UriBuilder_setPort
-#define arginfo_class_Uri_WhatWg_Url_getPath arginfo_class_Uri_Rfc3986_Uri_getPath
+#define arginfo_class_Uri_WhatWg_Url_getPath arginfo_class_Uri_QueryParams_toRfc1866String
#define arginfo_class_Uri_WhatWg_Url_withPath arginfo_class_Uri_Rfc3986_UriBuilder_setPath
@@ -204,21 +294,48 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_Url_equals, 0,
ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, comparisonMode, Uri\\\125riComparisonMode, 0, "Uri\\UriComparisonMode::ExcludeFragment")
ZEND_END_ARG_INFO()
-#define arginfo_class_Uri_WhatWg_Url_toAsciiString arginfo_class_Uri_Rfc3986_Uri_getPath
+#define arginfo_class_Uri_WhatWg_Url_toAsciiString arginfo_class_Uri_QueryParams_toRfc1866String
-#define arginfo_class_Uri_WhatWg_Url_toUnicodeString arginfo_class_Uri_Rfc3986_Uri_getPath
+#define arginfo_class_Uri_WhatWg_Url_toUnicodeString arginfo_class_Uri_QueryParams_toRfc1866String
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_Url_resolve, 0, 1, IS_STATIC, 0)
ZEND_ARG_TYPE_INFO(0, uri, IS_STRING, 0)
ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, softErrors, "null")
ZEND_END_ARG_INFO()
-#define arginfo_class_Uri_WhatWg_Url___serialize arginfo_class_Uri_Rfc3986_Uri___serialize
-
-#define arginfo_class_Uri_WhatWg_Url___unserialize arginfo_class_Uri_Rfc3986_Uri___unserialize
-
-#define arginfo_class_Uri_WhatWg_Url___debugInfo arginfo_class_Uri_Rfc3986_Uri___serialize
-
+#define arginfo_class_Uri_WhatWg_Url___serialize arginfo_class_Uri_QueryParamOptions___debugInfo
+
+#define arginfo_class_Uri_WhatWg_Url___unserialize arginfo_class_Uri_QueryParams___unserialize
+
+#define arginfo_class_Uri_WhatWg_Url___debugInfo arginfo_class_Uri_QueryParamOptions___debugInfo
+
+ZEND_METHOD(Uri_QueryParamOptions, __construct);
+ZEND_METHOD(Uri_QueryParamOptions, __debugInfo);
+ZEND_METHOD(Uri_QueryParams, parseRfc1866);
+ZEND_METHOD(Uri_QueryParams, parseRfc3986);
+ZEND_METHOD(Uri_QueryParams, parseWhatWg);
+ZEND_METHOD(Uri_QueryParams, fromArray);
+ZEND_METHOD(Uri_QueryParams, __construct);
+ZEND_METHOD(Uri_QueryParams, append);
+ZEND_METHOD(Uri_QueryParams, delete);
+ZEND_METHOD(Uri_QueryParams, deleteValue);
+ZEND_METHOD(Uri_QueryParams, has);
+ZEND_METHOD(Uri_QueryParams, hasValue);
+ZEND_METHOD(Uri_QueryParams, getFirst);
+ZEND_METHOD(Uri_QueryParams, getLast);
+ZEND_METHOD(Uri_QueryParams, getAll);
+ZEND_METHOD(Uri_QueryParams, list);
+ZEND_METHOD(Uri_QueryParams, getIterator);
+ZEND_METHOD(Uri_QueryParams, count);
+ZEND_METHOD(Uri_QueryParams, set);
+ZEND_METHOD(Uri_QueryParams, sort);
+ZEND_METHOD(Uri_QueryParams, toArray);
+ZEND_METHOD(Uri_QueryParams, toRfc1866String);
+ZEND_METHOD(Uri_QueryParams, toRfc3986String);
+ZEND_METHOD(Uri_QueryParams, toWhatWgString);
+ZEND_METHOD(Uri_QueryParams, __serialize);
+ZEND_METHOD(Uri_QueryParams, __unserialize);
+ZEND_METHOD(Uri_QueryParams, __debugInfo);
ZEND_METHOD(Uri_Rfc3986_UriBuilder, reset);
ZEND_METHOD(Uri_Rfc3986_UriBuilder, setScheme);
ZEND_METHOD(Uri_Rfc3986_UriBuilder, setUserInfo);
@@ -283,6 +400,41 @@ ZEND_METHOD(Uri_WhatWg_Url, __serialize);
ZEND_METHOD(Uri_WhatWg_Url, __unserialize);
ZEND_METHOD(Uri_WhatWg_Url, __debugInfo);
+static const zend_function_entry class_Uri_QueryParamOptions_methods[] = {
+ ZEND_ME(Uri_QueryParamOptions, __construct, arginfo_class_Uri_QueryParamOptions___construct, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParamOptions, __debugInfo, arginfo_class_Uri_QueryParamOptions___debugInfo, ZEND_ACC_PUBLIC)
+ ZEND_FE_END
+};
+
+static const zend_function_entry class_Uri_QueryParams_methods[] = {
+ ZEND_ME(Uri_QueryParams, parseRfc1866, arginfo_class_Uri_QueryParams_parseRfc1866, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(Uri_QueryParams, parseRfc3986, arginfo_class_Uri_QueryParams_parseRfc3986, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(Uri_QueryParams, parseWhatWg, arginfo_class_Uri_QueryParams_parseWhatWg, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(Uri_QueryParams, fromArray, arginfo_class_Uri_QueryParams_fromArray, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(Uri_QueryParams, __construct, arginfo_class_Uri_QueryParams___construct, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, append, arginfo_class_Uri_QueryParams_append, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, delete, arginfo_class_Uri_QueryParams_delete, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, deleteValue, arginfo_class_Uri_QueryParams_deleteValue, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, has, arginfo_class_Uri_QueryParams_has, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, hasValue, arginfo_class_Uri_QueryParams_hasValue, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, getFirst, arginfo_class_Uri_QueryParams_getFirst, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, getLast, arginfo_class_Uri_QueryParams_getLast, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, getAll, arginfo_class_Uri_QueryParams_getAll, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, list, arginfo_class_Uri_QueryParams_list, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, getIterator, arginfo_class_Uri_QueryParams_getIterator, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, count, arginfo_class_Uri_QueryParams_count, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, set, arginfo_class_Uri_QueryParams_set, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, sort, arginfo_class_Uri_QueryParams_sort, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, toArray, arginfo_class_Uri_QueryParams_toArray, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, toRfc1866String, arginfo_class_Uri_QueryParams_toRfc1866String, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, toRfc3986String, arginfo_class_Uri_QueryParams_toRfc3986String, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, toWhatWgString, arginfo_class_Uri_QueryParams_toWhatWgString, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, __serialize, arginfo_class_Uri_QueryParams___serialize, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, __unserialize, arginfo_class_Uri_QueryParams___unserialize, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_QueryParams, __debugInfo, arginfo_class_Uri_QueryParams___debugInfo, ZEND_ACC_PUBLIC)
+ ZEND_FE_END
+};
+
static const zend_function_entry class_Uri_Rfc3986_UriBuilder_methods[] = {
ZEND_ME(Uri_Rfc3986_UriBuilder, reset, arginfo_class_Uri_Rfc3986_UriBuilder_reset, ZEND_ACC_PUBLIC)
ZEND_ME(Uri_Rfc3986_UriBuilder, setScheme, arginfo_class_Uri_Rfc3986_UriBuilder_setScheme, ZEND_ACC_PUBLIC)
@@ -418,6 +570,64 @@ static zend_class_entry *register_class_Uri_UriComparisonMode(void)
return class_entry;
}
+static zend_class_entry *register_class_Uri_QueryParamOptions(void)
+{
+ zend_class_entry ce, *class_entry;
+
+ INIT_NS_CLASS_ENTRY(ce, "Uri", "QueryParamOptions", class_Uri_QueryParamOptions_methods);
+ class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_READONLY_CLASS);
+
+ zval property_parsingMaxQueryStringLength_default_value;
+ ZVAL_UNDEF(&property_parsingMaxQueryStringLength_default_value);
+ zend_string *property_parsingMaxQueryStringLength_name = zend_string_init("parsingMaxQueryStringLength", sizeof("parsingMaxQueryStringLength") - 1, true);
+ zend_declare_typed_property(class_entry, property_parsingMaxQueryStringLength_name, &property_parsingMaxQueryStringLength_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+ zend_string_release_ex(property_parsingMaxQueryStringLength_name, true);
+
+ zval property_parsingMaxParamCount_default_value;
+ ZVAL_UNDEF(&property_parsingMaxParamCount_default_value);
+ zend_string *property_parsingMaxParamCount_name = zend_string_init("parsingMaxParamCount", sizeof("parsingMaxParamCount") - 1, true);
+ zend_declare_typed_property(class_entry, property_parsingMaxParamCount_name, &property_parsingMaxParamCount_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+ zend_string_release_ex(property_parsingMaxParamCount_name, true);
+
+ zval property_trueValue_default_value;
+ ZVAL_UNDEF(&property_trueValue_default_value);
+ zend_string *property_trueValue_name = zend_string_init("trueValue", sizeof("trueValue") - 1, true);
+ zend_declare_typed_property(class_entry, property_trueValue_name, &property_trueValue_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
+ zend_string_release_ex(property_trueValue_name, true);
+
+ zval property_falseValue_default_value;
+ ZVAL_UNDEF(&property_falseValue_default_value);
+ zend_string *property_falseValue_name = zend_string_init("falseValue", sizeof("falseValue") - 1, true);
+ zend_declare_typed_property(class_entry, property_falseValue_name, &property_falseValue_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
+ zend_string_release_ex(property_falseValue_name, true);
+
+ zval property_useNullAsEmptyString_default_value;
+ ZVAL_UNDEF(&property_useNullAsEmptyString_default_value);
+ zend_string *property_useNullAsEmptyString_name = zend_string_init("useNullAsEmptyString", sizeof("useNullAsEmptyString") - 1, true);
+ zend_declare_typed_property(class_entry, property_useNullAsEmptyString_name, &property_useNullAsEmptyString_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL));
+ zend_string_release_ex(property_useNullAsEmptyString_name, true);
+
+ return class_entry;
+}
+
+static zend_class_entry *register_class_Uri_QueryParams(zend_class_entry *class_entry_IteratorAggregate, zend_class_entry *class_entry_Countable)
+{
+ zend_class_entry ce, *class_entry;
+
+ INIT_NS_CLASS_ENTRY(ce, "Uri", "QueryParams", class_Uri_QueryParams_methods);
+ class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_READONLY_CLASS);
+ zend_class_implements(class_entry, 2, class_entry_IteratorAggregate, class_entry_Countable);
+
+ zval property_options_default_value;
+ ZVAL_UNDEF(&property_options_default_value);
+ zend_string *property_options_name = zend_string_init("options", sizeof("options") - 1, true);
+ zend_string *property_options_class_Uri_QueryParamOptions = zend_string_init("Uri\\QueryParamOptions", sizeof("Uri\\QueryParamOptions")-1, 1);
+ zend_declare_typed_property(class_entry, property_options_name, &property_options_default_value, ZEND_ACC_PRIVATE|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_options_class_Uri_QueryParamOptions, 0, 0));
+ zend_string_release_ex(property_options_name, true);
+
+ return class_entry;
+}
+
static zend_class_entry *register_class_Uri_Rfc3986_UriType(void)
{
zend_class_entry *class_entry = zend_register_internal_enum("Uri\\Rfc3986\\UriType", IS_UNDEF, NULL);
diff --git a/ext/uri/php_uri_common.h b/ext/uri/php_uri_common.h
index 9106f6acd15f..390f94347af6 100644
--- a/ext/uri/php_uri_common.h
+++ b/ext/uri/php_uri_common.h
@@ -26,6 +26,8 @@ extern zend_class_entry *php_uri_ce_comparison_mode;
extern zend_class_entry *php_uri_ce_exception;
extern zend_class_entry *php_uri_ce_error;
extern zend_class_entry *php_uri_ce_invalid_uri_exception;
+extern zend_class_entry *php_uri_ce_query_params;
+extern zend_class_entry *php_uri_ce_query_param_options;
extern zend_class_entry *php_uri_ce_whatwg_invalid_url_exception;
extern zend_class_entry *php_uri_ce_whatwg_url_validation_error_type;
extern zend_class_entry *php_uri_ce_whatwg_url_validation_error;
@@ -162,6 +164,7 @@ PHPAPI zend_object *php_uri_object_handler_clone(zend_object *object);
#define PHP_URI_PARSER_WHATWG "Uri\\WhatWg\\Url"
#define PHP_URI_PARSER_PHP_PARSE_URL "parse_url"
#define PHP_URI_SERIALIZE_URI_FIELD_NAME "uri"
+#define PHP_URI_SERIALIZE_URI_QUERY_PARAM_FIELD_NAME "query"
static inline const php_uri_property_handler *php_uri_parser_property_handler_by_name(const php_uri_parser *parser, php_uri_property_name property_name)
{
@@ -191,4 +194,33 @@ void php_uri_property_write_str_helper(INTERNAL_FUNCTION_PARAMETERS, php_uri_pro
void php_uri_property_write_str_or_null_helper(INTERNAL_FUNCTION_PARAMETERS, php_uri_property_name property_name);
void php_uri_property_write_long_or_null_helper(INTERNAL_FUNCTION_PARAMETERS, php_uri_property_name property_name);
+typedef struct php_uri_query_param_list_entry {
+ zend_string *key;
+ zend_string *value;
+ uint32_t next_same_key;
+} php_uri_query_param_list_entry;
+
+typedef struct php_uri_query_param_lookup_entry {
+ uint32_t first_index;
+ uint32_t last_index;
+} php_uri_query_param_lookup_entry;
+
+typedef struct php_uri_query_params_object {
+ HashTable entry_list;
+ HashTable entry_lookup_table;
+ uint32_t tombstone_count;
+ bool is_initialized;
+ zend_object std;
+} php_uri_query_params_object;
+
+typedef struct php_uri_query_param_options_object {
+ size_t parsing_max_query_string_length;
+ uint32_t parsing_max_param_count;
+ zend_string *true_value;
+ zend_string *false_value;
+ bool use_null_as_string;
+ bool is_initialized;
+ zend_object std;
+} php_uri_query_param_options_object;
+
#endif
diff --git a/ext/uri/php_uri_decl.h b/ext/uri/php_uri_decl.h
index 784ac5b4c0e5..9abf7572dd9a 100644
--- a/ext/uri/php_uri_decl.h
+++ b/ext/uri/php_uri_decl.h
@@ -1,8 +1,8 @@
/* This is a generated file, edit php_uri.stub.php instead.
- * Stub hash: 1a396b7a5b170b968d84a2c64a5bcf7d962653db */
+ * Stub hash: 5c99026c91273c8094ebbfb6809141649fc13c60 */
-#ifndef ZEND_PHP_URI_DECL_1a396b7a5b170b968d84a2c64a5bcf7d962653db_H
-#define ZEND_PHP_URI_DECL_1a396b7a5b170b968d84a2c64a5bcf7d962653db_H
+#ifndef ZEND_PHP_URI_DECL_5c99026c91273c8094ebbfb6809141649fc13c60_H
+#define ZEND_PHP_URI_DECL_5c99026c91273c8094ebbfb6809141649fc13c60_H
typedef enum zend_enum_Uri_UriComparisonMode {
ZEND_ENUM_Uri_UriComparisonMode_IncludeFragment = 1,
@@ -63,4 +63,4 @@ typedef enum zend_enum_Uri_WhatWg_UrlHostType {
ZEND_ENUM_Uri_WhatWg_UrlHostType_Empty = 5,
} zend_enum_Uri_WhatWg_UrlHostType;
-#endif /* ZEND_PHP_URI_DECL_1a396b7a5b170b968d84a2c64a5bcf7d962653db_H */
+#endif /* ZEND_PHP_URI_DECL_5c99026c91273c8094ebbfb6809141649fc13c60_H */
diff --git a/ext/uri/php_uri_query.c b/ext/uri/php_uri_query.c
new file mode 100644
index 000000000000..120f27807fc8
--- /dev/null
+++ b/ext/uri/php_uri_query.c
@@ -0,0 +1,1560 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright © The PHP Group and Contributors. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to the Modified BSD License that is |
+ | bundled with this package in the file LICENSE, and is available |
+ | through the World Wide Web at . |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Máté Kocsis |
+ +----------------------------------------------------------------------+
+*/
+#include "zend_enum.h"
+#include "zend_smart_str.h"
+#include "../../Zend/zend_hash.h"
+#ifdef HAVE_CONFIG_H
+# include
+#endif
+
+#include "php.h"
+#include "Zend/zend_exceptions.h"
+#include "Zend/zend_interfaces.h"
+
+#include "php_uri_common.h"
+#include "php_uri_query.h"
+#include "uri_parser_rfc3986.h"
+#include "uri_parser_whatwg.h"
+
+#define MAX_TOMBSTONES 1000
+
+#define PARSING_MAX_QUERY_STRING_LENGTH_DEFAULT 10000
+#define PARSING_MAX_QUERY_PARAM_COUNT_DEFAULT 1000
+#define USE_NULL_AS_EMPTY_STRING_DEFAULT false
+
+typedef enum php_uri_query_params_spec {
+ PHP_URI_QUERY_PARAMS_SPEC_RFC1866,
+ PHP_URI_QUERY_PARAMS_SPEC_RFC3986,
+ PHP_URI_QUERY_PARAMS_SPEC_WHATWG,
+} php_uri_query_params_spec;
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1) static zend_result php_uri_query_param_options_update_properties(
+ zend_object *query_param_options_object, const zend_long parsing_max_query_string_length, const zend_long parsing_max_param_count,
+ zend_string *true_value, zend_string *false_value, const bool use_null_as_empty_string
+) {
+ php_uri_query_param_options_object *options = php_uri_query_param_options_object_from_obj(query_param_options_object);
+ if (options->is_initialized) {
+ zend_throw_error(NULL, "Cannot modify readonly object of class %s", ZSTR_VAL(query_param_options_object->ce->name));
+ return FAILURE;
+ }
+
+ options->parsing_max_query_string_length = parsing_max_query_string_length;
+ options->parsing_max_param_count = parsing_max_param_count;
+
+ if (true_value == NULL) {
+ options->true_value = zend_string_init(ZEND_STRL("1"), false);
+ } else {
+ options->true_value = true_value;
+ }
+
+ if (false_value == NULL) {
+ options->false_value = zend_string_init(ZEND_STRL("0"), false);
+ } else {
+ options->false_value = false_value;
+ }
+
+ options->use_null_as_string = use_null_as_empty_string;
+ options->is_initialized = true;
+
+ return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1,2) static zend_string *php_uri_query_params_convert_value_to_str(
+ const php_uri_query_params_object *query_params_object, const zval *zv
+) {
+ switch (Z_TYPE_P(zv)) {
+ case IS_STRING: {
+ return zend_string_copy(Z_STR_P(zv));
+ }
+ case IS_FALSE:
+ ZEND_FALLTHROUGH;
+ case IS_TRUE: {
+ return zend_string_init(Z_TYPE_P(zv) == IS_FALSE ? "0" : "1", 1, false);
+ }
+ case IS_LONG: {
+ return zend_long_to_str(Z_LVAL_P(zv));
+ }
+ case IS_DOUBLE: {
+ return zend_double_to_str(Z_LVAL_P(zv));
+ }
+ case IS_NULL: {
+ return NULL;
+ }
+ case IS_RESOURCE:
+ zend_argument_value_error(2, "must not contain a resource");
+ return NULL;
+ case IS_ARRAY: {
+ zend_argument_value_error(2, "must not contain an arra");
+ return NULL;
+ }
+ case IS_OBJECT:
+ if (Z_OBJCE_P(zv)->ce_flags & ZEND_ACC_ENUM && Z_OBJCE_P(zv)->enum_backing_type != IS_UNDEF) {
+ const zval *tmp = zend_enum_fetch_case_value(Z_OBJ_P(zv));
+ return php_uri_query_params_convert_value_to_str(query_params_object, tmp);
+ }
+
+ zend_argument_value_error(2, "must not contain an object");
+ return NULL;
+ default: ZEND_UNREACHABLE();
+ }
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1,2) static bool php_uri_query_params_has_entry(
+ const php_uri_query_params_object *query_params_object, zend_string *name
+) {
+ const php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+
+ return lookup_entry != NULL;
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1,2) static bool php_uri_query_params_has_entry_with_value(
+ const php_uri_query_params_object *query_params_object, zend_string *name, const zend_string *value
+) {
+ const php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+ if (lookup_entry == NULL) {
+ return false;
+ }
+
+ uint32_t index = lookup_entry->first_index;
+ while (index != UINT32_MAX) {
+ const php_uri_query_param_list_entry *list_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, index);
+ ZEND_ASSERT(list_entry != NULL);
+
+ if (
+ (value == NULL && list_entry->value == NULL) ||
+ (value != NULL && list_entry->value != NULL && zend_string_equal_content(value, list_entry->value))
+ ) { // TODO properly handle search for NULL values according to options
+ return true;
+ }
+
+ index = list_entry->next_same_key;
+ }
+
+ return false;
+}
+
+static void php_uri_query_params_find_value(
+ const php_uri_query_params_object *query_params_object, const uint32_t index, zval *zv
+) {
+ const php_uri_query_param_list_entry *list_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, index);
+ ZEND_ASSERT(list_entry != NULL);
+
+ if (list_entry->value == NULL) {
+ ZVAL_NULL(zv);
+ } else {
+ ZVAL_STR_COPY(zv, list_entry->value);
+ }
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2) static void php_uri_query_params_find_first_value(
+ const php_uri_query_params_object *query_params_object, zend_string *name, zval *zv
+) {
+ const php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+ if (lookup_entry == NULL) {
+ ZVAL_NULL(zv);
+ return;
+ }
+
+ php_uri_query_params_find_value(query_params_object, lookup_entry->first_index, zv);
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1,2) static void php_uri_query_params_find_last_value(
+ const php_uri_query_params_object *query_params_object, zend_string *name, zval *zv
+) {
+ const php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+ if (lookup_entry == NULL) {
+ ZVAL_NULL(zv);
+ return;
+ }
+
+ php_uri_query_params_find_value(query_params_object, lookup_entry->last_index, zv);
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1,2) static void php_uri_query_params_find_all_values(
+ const php_uri_query_params_object *query_params_object, zend_string *name, zval *zv
+) {
+ const php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+ if (lookup_entry == NULL) {
+ ZVAL_EMPTY_ARRAY(zv);
+ return;
+ }
+
+ array_init(zv);
+
+ uint32_t index = lookup_entry->first_index;
+ while (index != UINT32_MAX) {
+ const php_uri_query_param_list_entry *list_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, index);
+ ZEND_ASSERT(list_entry != NULL);
+
+ if (list_entry->key != NULL) {
+ zval value;
+ zend_hash_next_index_insert(Z_ARR_P(zv), &value);
+ }
+
+ index = list_entry->next_same_key;
+ }
+}
+
+ZEND_ATTRIBUTE_NONNULL static void php_uri_query_params_list_entries(php_uri_query_params_object *object, zval *params)
+{
+ array_init_size(params, zend_array_count(&object->entry_list));
+
+ ZEND_HASH_FOREACH_PTR(&object->entry_list, php_uri_query_param_list_entry *list_entry) {
+ if (list_entry->key != NULL) {
+ zval key;
+ ZVAL_STR_COPY(&key, list_entry->key);
+
+ zval value;
+ if (list_entry->value == NULL) {
+ ZVAL_NULL(&value);
+ } else {
+ ZVAL_STR_COPY(&value, list_entry->value);
+ }
+
+ zval zv;
+ array_init_size(&zv, 2);
+ zend_hash_index_add(Z_ARR(zv), 0, &key);
+ zend_hash_index_add(Z_ARR(zv), 1, &value);
+
+ zend_hash_next_index_insert(Z_ARR_P(params), &zv);
+ }
+ } ZEND_HASH_FOREACH_END();
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1,2) static zend_result php_uri_query_params_append_entry(
+ php_uri_query_params_object *query_params_object, zend_string *name, zend_string *value
+) {
+ php_uri_query_param_list_entry new_list_entry = {
+ .key = zend_string_copy(name),
+ .value = value == NULL ? NULL : zend_string_copy(value),
+ .next_same_key = UINT32_MAX,
+ };
+
+ const void *result = zend_hash_next_index_insert_mem(&query_params_object->entry_list, &new_list_entry, sizeof(new_list_entry));
+ if (result == NULL) {
+ return FAILURE;
+ }
+
+ const zend_long new_index = query_params_object->entry_list.nNextFreeElement - 1;
+
+ php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+
+ if (lookup_entry == NULL) {
+ /* Insert a new lookup entry */
+
+ php_uri_query_param_lookup_entry new_lookup_entry = {
+ .first_index = new_index,
+ .last_index = new_index,
+ };
+ if (zend_hash_add_mem(&query_params_object->entry_lookup_table, name, &new_lookup_entry, sizeof(new_lookup_entry)) == NULL) {
+ zend_hash_index_del(&query_params_object->entry_list, new_index);
+ return FAILURE;
+ }
+ } else {
+ /* Update the existing lookup entry + the same key list */
+
+ php_uri_query_param_list_entry *last_list_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, lookup_entry->last_index);
+ ZEND_ASSERT(last_list_entry != NULL);
+
+ last_list_entry->next_same_key = new_index;
+ lookup_entry->last_index = new_index;
+ }
+
+ return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NONNULL static void php_uri_query_params_cleanup_tombstones(
+ php_uri_query_params_object *query_params_object
+) {
+ if (query_params_object->tombstone_count <= MAX_TOMBSTONES) {
+ return;
+ }
+
+ HashTable new_entry_list;
+ zend_hash_init(&new_entry_list, query_params_object->tombstone_count, NULL, query_params_object->entry_list.pDestructor, 0);
+
+ HashTable last_seen_index;
+ zend_hash_init(&last_seen_index, 8, NULL, NULL, 0);
+
+ php_uri_query_param_list_entry *old_entry;
+ ZEND_HASH_FOREACH_PTR(&query_params_object->entry_list, old_entry) {
+ if (old_entry->key == NULL) {
+ continue;
+ }
+
+ uint32_t new_index = zend_hash_num_elements(&new_entry_list);
+
+ php_uri_query_param_list_entry new_entry = {
+ .key = old_entry->key,
+ .value = old_entry->value,
+ .next_same_key = UINT32_MAX,
+ };
+ zend_hash_next_index_insert_mem(&new_entry_list, &new_entry, sizeof(new_entry));
+
+ php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, old_entry->key);
+ ZEND_ASSERT(lookup_entry != NULL);
+
+ zval *last_index_zv = zend_hash_find(&last_seen_index, old_entry->key);
+
+ if (last_index_zv == NULL) {
+ lookup_entry->first_index = new_index;
+ } else {
+ uint32_t prev_index = (uint32_t) Z_LVAL_P(last_index_zv);
+ php_uri_query_param_list_entry *prev_entry = zend_hash_index_find_ptr(&new_entry_list, prev_index);
+ ZEND_ASSERT(prev_entry != NULL);
+ prev_entry->next_same_key = new_index;
+ }
+
+ lookup_entry->last_index = new_index;
+
+ zval new_last_zv;
+ ZVAL_LONG(&new_last_zv, (zend_long) new_index);
+ zend_hash_update(&last_seen_index, old_entry->key, &new_last_zv);
+
+ } ZEND_HASH_FOREACH_END();
+
+ zend_hash_destroy(&last_seen_index);
+ zend_hash_destroy(&query_params_object->entry_list);
+ query_params_object->entry_list = new_entry_list;
+}
+
+static void php_uri_query_params_tombstone_entry(
+ php_uri_query_params_object *query_params_object, php_uri_query_param_list_entry *list_entry
+) {
+ if (list_entry->key == NULL) {
+ return;
+ }
+
+ zend_string_release(list_entry->key);
+ list_entry->key = NULL;
+ if (list_entry->value != NULL) {
+ zend_string_release(list_entry->value);
+ list_entry->value = NULL;
+ }
+ list_entry->next_same_key = UINT32_MAX;
+ query_params_object->tombstone_count++;
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2) static void php_uri_query_params_delete_entries(
+ php_uri_query_params_object *query_params_object, zend_string *name
+) {
+ const php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+ if (lookup_entry == NULL) {
+ return;
+ }
+
+ uint32_t index = lookup_entry->first_index;
+ while (index != UINT32_MAX) {
+ php_uri_query_param_list_entry *list_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, index);
+ ZEND_ASSERT(list_entry != NULL);
+
+ const uint32_t next = list_entry->next_same_key;
+ php_uri_query_params_tombstone_entry(query_params_object, list_entry);
+ index = next;
+ }
+
+ zend_hash_del(&query_params_object->entry_lookup_table, name);
+
+ php_uri_query_params_cleanup_tombstones(query_params_object);
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1,2) static void php_uri_query_params_delete_entries_with_value(
+ php_uri_query_params_object *query_params_object, zend_string *name, const zend_string *value
+) {
+ php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+ if (lookup_entry == NULL) {
+ return;
+ }
+
+ uint32_t index = lookup_entry->first_index;
+ uint32_t new_first = UINT32_MAX;
+ uint32_t new_last = UINT32_MAX;
+ php_uri_query_param_list_entry *prev_list_entry = NULL;
+
+ while (index != UINT32_MAX) {
+ php_uri_query_param_list_entry *list_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, index);
+ ZEND_ASSERT(list_entry != NULL);
+
+ const uint32_t next = list_entry->next_same_key;
+
+ const bool should_delete = (value == NULL && list_entry->value == NULL) ||
+ (value != NULL && list_entry->value != NULL && zend_string_equal_content(value, list_entry->value));
+
+ if (should_delete) {
+ php_uri_query_params_tombstone_entry(query_params_object, list_entry);
+
+ /* Rebuild the same key list */
+ if (prev_list_entry != NULL) {
+ prev_list_entry->next_same_key = next;
+ }
+ } else {
+ if (new_first == UINT32_MAX) {
+ new_first = index;
+ }
+ new_last = index;
+ prev_list_entry = list_entry;
+ }
+
+ index = next;
+ }
+
+ /* Either delete the lookup entry if no list entries remain, or update it with the new data. */
+ if (new_first == UINT32_MAX) {
+ zend_hash_del(&query_params_object->entry_lookup_table, name);
+ } else {
+ php_uri_query_param_list_entry *last_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, new_last);
+ ZEND_ASSERT(last_entry != NULL);
+ last_entry->next_same_key = UINT32_MAX;
+
+ lookup_entry->first_index = new_first;
+ lookup_entry->last_index = new_last;
+ }
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1, 2) static zend_result php_uri_query_params_set_entry(
+ php_uri_query_params_object *query_params_object, zend_string *name, zend_string *value
+) {
+ php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&query_params_object->entry_lookup_table, name);
+ if (lookup_entry == NULL) {
+ return php_uri_query_params_append_entry(query_params_object, name, value);
+ }
+
+ /* Overwrite first occurrence */
+ php_uri_query_param_list_entry *first_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, lookup_entry->first_index);
+ ZEND_ASSERT(first_entry != NULL);
+
+ if (first_entry->value != NULL) {
+ zend_string_release(first_entry->value);
+ }
+ first_entry->value = value != NULL ? zend_string_copy(value) : NULL;
+
+ /* Tombstone the rest of the occurrences */
+ uint32_t index = first_entry->next_same_key;
+ first_entry->next_same_key = UINT32_MAX;
+
+ while (index != UINT32_MAX) {
+ php_uri_query_param_list_entry *list_entry = zend_hash_index_find_ptr(&query_params_object->entry_list, index);
+ ZEND_ASSERT(list_entry != NULL);
+
+ const uint32_t next = list_entry->next_same_key;
+ php_uri_query_params_tombstone_entry(query_params_object, list_entry);
+ index = next;
+ }
+
+ /* Update the lookup entry */
+ lookup_entry->last_index = lookup_entry->first_index;
+
+ php_uri_query_params_cleanup_tombstones(query_params_object);
+
+ return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_object_query_params_create_object(zend_class_entry *ce)
+{
+ php_uri_query_params_object *uri_query_params_object = zend_object_alloc(sizeof(*uri_query_params_object), ce);
+
+ zend_object_std_init(&uri_query_params_object->std, ce);
+ object_properties_init(&uri_query_params_object->std, ce);
+
+ uri_query_params_object->is_initialized = false;
+ uri_query_params_object->tombstone_count = 0;
+
+ return &uri_query_params_object->std;
+}
+
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_params_object_free(php_uri_query_params_object *uri_query_params_object)
+{
+ if (uri_query_params_object->is_initialized) {
+ zend_hash_destroy(&uri_query_params_object->entry_list);
+ zend_hash_destroy(&uri_query_params_object->entry_lookup_table);
+ uri_query_params_object->tombstone_count = 0;
+ }
+}
+
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_params_object_handler_free(zend_object *object)
+{
+ php_uri_query_params_object *uri_query_params_object = php_uri_query_params_object_from_obj(object);
+
+ php_uri_query_params_object_free(uri_query_params_object);
+
+ zend_object_std_dtor(&uri_query_params_object->std);
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_query_params_object_handler_clone(zend_object *object)
+{
+ const php_uri_query_params_object *uri_query_params_object = php_uri_query_params_object_from_obj(object);
+
+ php_uri_query_params_object *new_uri_query_params_object = php_uri_query_params_object_from_obj(
+ object->ce->create_object(object->ce)
+ );
+
+ zend_hash_copy(&new_uri_query_params_object->entry_list, &uri_query_params_object->entry_list, NULL);
+ zend_hash_copy(&new_uri_query_params_object->entry_lookup_table, &uri_query_params_object->entry_lookup_table, NULL);
+ new_uri_query_params_object->tombstone_count = uri_query_params_object->tombstone_count;
+ new_uri_query_params_object->is_initialized = uri_query_params_object->is_initialized;
+ zend_objects_clone_members(&new_uri_query_params_object->std, &uri_query_params_object->std);
+
+ return &new_uri_query_params_object->std;
+}
+
+typedef struct php_uri_query_params_iterator {
+ zend_object_iterator intern;
+ php_uri_query_params_object *query_params_object;
+ uint32_t current_index;
+ zval current_value;
+} php_uri_query_params_iterator;
+
+static void php_uri_query_params_iterator_dtor(zend_object_iterator *iter)
+{
+ php_uri_query_params_iterator *iterator = (php_uri_query_params_iterator *) iter;
+ zval_ptr_dtor(&iterator->current_value);
+ zval_ptr_dtor(&iterator->intern.data);
+}
+
+static zend_result php_uri_query_params_iterator_valid(zend_object_iterator *iter)
+{
+ php_uri_query_params_iterator *iterator = (php_uri_query_params_iterator *) iter;
+ php_uri_query_params_object *obj = iterator->query_params_object;
+
+ if (!obj->is_initialized) {
+ return FAILURE;
+ }
+
+ while (iterator->current_index < zend_hash_num_elements(&obj->entry_list)) {
+ php_uri_query_param_list_entry *entry = zend_hash_index_find_ptr(
+ &obj->entry_list, iterator->current_index);
+ if (entry != NULL && entry->key != NULL) {
+ return SUCCESS;
+ }
+ iterator->current_index++;
+ }
+
+ return FAILURE;
+}
+
+static zval *php_uri_query_params_iterator_current_data(zend_object_iterator *iter)
+{
+ php_uri_query_params_iterator *iterator = (php_uri_query_params_iterator *) iter;
+ php_uri_query_params_object *obj = iterator->query_params_object;
+
+ php_uri_query_param_list_entry *entry = zend_hash_index_find_ptr(
+ &obj->entry_list, iterator->current_index);
+ ZEND_ASSERT(entry != NULL && entry->key != NULL);
+
+ zval_ptr_dtor(&iterator->current_value);
+ array_init_size(&iterator->current_value, 2);
+
+ zval key_zv, value_zv;
+ ZVAL_STR_COPY(&key_zv, entry->key);
+ if (entry->value != NULL) {
+ ZVAL_STR_COPY(&value_zv, entry->value);
+ } else {
+ ZVAL_NULL(&value_zv);
+ }
+ zend_hash_next_index_insert(Z_ARRVAL(iterator->current_value), &key_zv);
+ zend_hash_next_index_insert(Z_ARRVAL(iterator->current_value), &value_zv);
+
+ return &iterator->current_value;
+}
+
+static void php_uri_query_params_iterator_current_key(
+ zend_object_iterator *iter, zval *key)
+{
+ php_uri_query_params_iterator *iterator = (php_uri_query_params_iterator *) iter;
+ ZVAL_LONG(key, (zend_long) iterator->current_index);
+}
+
+static void php_uri_query_params_iterator_move_forward(zend_object_iterator *iter)
+{
+ php_uri_query_params_iterator *iterator = (php_uri_query_params_iterator *) iter;
+ iterator->current_index++;
+}
+
+static void php_uri_query_params_iterator_rewind(zend_object_iterator *iter)
+{
+ php_uri_query_params_iterator *iterator = (php_uri_query_params_iterator *) iter;
+ iterator->current_index = 0;
+}
+
+static const zend_object_iterator_funcs php_uri_query_params_iterator_funcs = {
+ .dtor = php_uri_query_params_iterator_dtor,
+ .valid = php_uri_query_params_iterator_valid,
+ .get_current_data = php_uri_query_params_iterator_current_data,
+ .get_current_key = php_uri_query_params_iterator_current_key,
+ .move_forward = php_uri_query_params_iterator_move_forward,
+ .rewind = php_uri_query_params_iterator_rewind,
+ .invalidate_current = NULL,
+ .get_gc = NULL,
+};
+
+ZEND_ATTRIBUTE_NONNULL zend_object_iterator *php_uri_object_query_params_get_iterator(
+ zend_class_entry *ce, zval *object, const int by_ref)
+{
+ if (by_ref) {
+ zend_throw_error(NULL, "An iterator cannot be used with foreach by reference");
+ return NULL;
+ }
+
+ php_uri_query_params_iterator *iterator = emalloc(sizeof(php_uri_query_params_iterator));
+
+ zend_iterator_init(&iterator->intern);
+ iterator->intern.funcs = &php_uri_query_params_iterator_funcs;
+
+ ZVAL_OBJ_COPY(&iterator->intern.data, Z_OBJ_P(object));
+ iterator->query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(object);
+ iterator->current_index = 0;
+ ZVAL_UNDEF(&iterator->current_value);
+
+ return &iterator->intern;
+}
+
+static void php_uri_query_param_list_entry_dtor(zval *zv)
+{
+ php_uri_query_param_list_entry *list_entry = Z_PTR_P(zv);
+
+ if (list_entry->key != NULL) {
+ zend_string_release(list_entry->key);
+ }
+
+ if (list_entry->value != NULL) {
+ zend_string_release(list_entry->value);
+ }
+
+ efree(list_entry);
+}
+
+static void php_uri_query_param_lookup_entry_dtor(zval *zv)
+{
+ efree(Z_PTR_P(zv));
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1) static php_uri_query_param_options_object *php_uri_query_params_object_init(
+ php_uri_query_params_object *uri_query_params_object, zval *options
+) {
+ if (!uri_query_params_object->is_initialized) {
+ zend_hash_init(&uri_query_params_object->entry_list, 8, NULL, php_uri_query_param_list_entry_dtor, 0);
+ zend_hash_init(&uri_query_params_object->entry_lookup_table, 8, NULL, php_uri_query_param_lookup_entry_dtor, 0);
+ uri_query_params_object->is_initialized = true;
+ }
+
+ if (options == NULL) {
+ zval default_options;
+ object_init_ex(&default_options, php_uri_ce_query_param_options);
+
+ if (php_uri_query_param_options_update_properties(
+ Z_OBJ(default_options), PARSING_MAX_QUERY_STRING_LENGTH_DEFAULT,
+ PARSING_MAX_QUERY_PARAM_COUNT_DEFAULT, NULL, NULL, USE_NULL_AS_EMPTY_STRING_DEFAULT
+ )) {
+ return NULL;
+ }
+
+ /* There is no need to update the properties as the default values are already set by the create handler */
+ zend_update_property(uri_query_params_object->std.ce, &uri_query_params_object->std, ZEND_STRL("options"), &default_options);
+ if (EG(exception)) {
+ zval_ptr_dtor(&default_options);
+ return NULL;
+ }
+
+ return Z_URI_QUERY_PARAM_OPTIONS_OBJECT_P(&default_options);
+ }
+
+ zend_update_property(uri_query_params_object->std.ce, &uri_query_params_object->std, ZEND_STRL("options"), options);
+ if (EG(exception)) {
+ return NULL;
+ }
+ GC_ADDREF(&uri_query_params_object->std);
+
+ return Z_URI_QUERY_PARAM_OPTIONS_OBJECT_P(options);
+}
+
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_param_options_object_free(php_uri_query_param_options_object *uri_query_param_options_object)
+{
+ if (uri_query_param_options_object->is_initialized) {
+ if (uri_query_param_options_object->true_value != NULL) {
+ zend_string_release(uri_query_param_options_object->true_value);
+ uri_query_param_options_object->true_value = NULL;
+ }
+ if (uri_query_param_options_object->false_value != NULL) {
+ zend_string_release(uri_query_param_options_object->false_value);
+ uri_query_param_options_object->false_value = NULL;
+ }
+ }
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_object_create_query_param_options(zend_class_entry *ce)
+{
+ php_uri_query_param_options_object *uri_query_param_options_object = zend_object_alloc(sizeof(*uri_query_param_options_object), ce);
+
+ zend_object_std_init(&uri_query_param_options_object->std, ce);
+ object_properties_init(&uri_query_param_options_object->std, ce);
+
+ uri_query_param_options_object->is_initialized = false;
+
+ return &uri_query_param_options_object->std;
+}
+
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_param_options_object_handler_free(zend_object *object)
+{
+ php_uri_query_param_options_object *uri_query_param_options_object = php_uri_query_param_options_object_from_obj(object);
+
+ php_uri_query_param_options_object_free(uri_query_param_options_object);
+
+ zend_object_std_dtor(&uri_query_param_options_object->std);
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_query_param_options_object_handler_clone(zend_object *object)
+{
+ const php_uri_query_param_options_object *uri_query_param_options_object = php_uri_query_param_options_object_from_obj(object);
+
+ php_uri_query_param_options_object *new_uri_query_param_options_object = php_uri_query_param_options_object_from_obj(
+ object->ce->create_object(object->ce)
+ );
+
+ new_uri_query_param_options_object->parsing_max_query_string_length = uri_query_param_options_object->parsing_max_query_string_length;
+ new_uri_query_param_options_object->parsing_max_param_count = uri_query_param_options_object->parsing_max_param_count;
+ if (uri_query_param_options_object->true_value != NULL) {
+ new_uri_query_param_options_object->true_value = zend_string_copy(uri_query_param_options_object->true_value);
+ }
+ if (uri_query_param_options_object->false_value != NULL) {
+ new_uri_query_param_options_object->false_value = zend_string_copy(uri_query_param_options_object->false_value);
+ }
+ new_uri_query_param_options_object->use_null_as_string = uri_query_param_options_object->use_null_as_string;
+ new_uri_query_param_options_object->is_initialized = uri_query_param_options_object->is_initialized;
+ zend_objects_clone_members(&new_uri_query_param_options_object->std, &uri_query_param_options_object->std);
+
+ return &new_uri_query_param_options_object->std;
+}
+
+ZEND_ATTRIBUTE_NONNULL int php_uri_query_param_options_object_handler_has_property(zend_object *obj, zend_string *name, int check_empty, void **cache_slot)
+{
+ if (check_empty == ZEND_PROPERTY_NOT_EMPTY) {
+ const php_uri_query_param_options_object *options = php_uri_query_param_options_object_from_obj(obj);
+
+ if (zend_string_equals_literal(name, "parsingMaxQueryStringLength")) {
+ return options->parsing_max_query_string_length != 0;
+ }
+ }
+
+ return zend_string_equals_literal(name, "trueValue") || zend_string_equals_literal(name, "falseValue");
+}
+
+ZEND_ATTRIBUTE_NONNULL zval *php_uri_query_param_options_object_handler_read_property(zend_object *obj, zend_string *name, int type, void **cache_slot, zval *rv)
+{
+ const php_uri_query_param_options_object *options = php_uri_query_param_options_object_from_obj(obj);
+
+ if (zend_string_equals_literal(name, "parsingMaxQueryStringLength")) {
+ ZVAL_LONG(rv, options->parsing_max_query_string_length);
+ return rv;
+ }
+
+ if (zend_string_equals_literal(name, "parsingMaxParamCount")) {
+ ZVAL_LONG(rv, options->parsing_max_param_count);
+ return rv;
+ }
+
+ if (zend_string_equals_literal(name, "trueValue")) {
+ ZVAL_STR_COPY(rv, options->true_value);
+ return rv;
+ }
+
+ if (zend_string_equals_literal(name, "falseValue")) {
+ ZVAL_STR_COPY(rv, options->false_value);
+ return rv;
+ }
+
+ if (zend_string_equals_literal(name, "useNullAsEmptyString")) {
+ ZVAL_BOOL(rv, options->use_null_as_string);
+ return rv;
+ }
+
+ ZEND_UNREACHABLE();
+}
+
+ZEND_ATTRIBUTE_NONNULL zval *php_uri_query_param_options_object_handler_write_property(zend_object *obj, zend_string *name, zval *value, void **cache_slot)
+{
+ zend_readonly_property_modification_error_ex(ZSTR_VAL(obj->ce->name), ZSTR_VAL(name));
+ return &EG(error_zval);
+}
+
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_param_options_object_handler_unset_property(zend_object *obj, zend_string *name, void **cache_slot)
+{
+ zend_throw_error(NULL, "Cannot unset readonly property %s::$%s", ZSTR_VAL(obj->ce->name), ZSTR_VAL(name));
+}
+
+ZEND_ATTRIBUTE_NONNULL zval *php_uri_query_param_options_object_handler_get_property_ptr_ptr(zend_object *object, zend_string *member, int type, void **cache_slot)
+{
+ /* Must always go through read property because all properties are virtual, and no dynamic properties are allowed. */
+ return NULL;
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1, 3) static zend_result php_uri_query_params_parse(
+ const char *query_str_first, const size_t query_str_length, php_uri_query_params_object *query_params_object,
+ const php_uri_query_params_spec spec, const bool silent, zval *options
+) {
+ const php_uri_query_param_options_object *query_param_options_object = php_uri_query_params_object_init(query_params_object, options);
+ if (query_param_options_object == NULL) {
+ return FAILURE;
+ }
+
+ if (UNEXPECTED(query_str_length > query_param_options_object->parsing_max_query_string_length)) {
+ if (!silent) {
+ zend_throw_exception(php_uri_ce_exception, "The specified query string is too long", 0);
+ }
+ return FAILURE;
+ }
+
+ const char *p = query_str_first;
+ const char *end = query_str_first + query_str_length;
+
+ if (spec == PHP_URI_QUERY_PARAMS_SPEC_WHATWG && *p == '?') { // TODO check other skippable characters ("/t" "/n" etc.)
+ p++;
+ }
+
+ while (p < end) {
+ const char *ampersand = memchr(p, '&', (size_t)(end - p));
+ const char *pair_end = ampersand != NULL ? ampersand : end;
+ const size_t pair_len = (size_t)(pair_end - p);
+
+ if (pair_len == 0) {
+ p = pair_end + 1;
+ continue;
+ }
+
+ const char *eq = memchr(p, '=', pair_len);
+
+ const char *name_start;
+ size_t name_len;
+ const char *value_start;
+ size_t value_len;
+
+ if (eq == NULL) {
+ name_start = p;
+ name_len = pair_len;
+ value_start = NULL;
+ value_len = 0;
+ } else {
+ name_start = p;
+ name_len = (size_t)(eq - p);
+ value_start = eq + 1;
+ value_len = (size_t) (pair_end - value_start);
+ }
+
+ zend_string *name;
+ zend_string *value;
+
+ switch (spec) {
+ case PHP_URI_QUERY_PARAMS_SPEC_RFC1866:
+ name = php_uri_parser_rfc3986_query_component_percent_decode(name_start, name_len, true);
+ value = php_uri_parser_rfc3986_query_component_percent_decode(value_start, value_len, true);
+ break;
+ case PHP_URI_QUERY_PARAMS_SPEC_RFC3986:
+ name = php_uri_parser_rfc3986_query_component_percent_decode(name_start, name_len, false);
+ value = php_uri_parser_rfc3986_query_component_percent_decode(value_start, value_len, false);
+ break;
+ case PHP_URI_QUERY_PARAMS_SPEC_WHATWG:
+ name = php_uri_parser_whatwg_query_component_percent_decode(name_start, name_len);
+ value = php_uri_parser_whatwg_query_component_percent_decode(value_start, value_len);
+ break;
+ default: ZEND_UNREACHABLE();
+ }
+
+ const zend_result result = php_uri_query_params_append_entry(query_params_object, name, value);
+
+ zend_string_release(name);
+ if (value != NULL) {
+ zend_string_release(value);
+ }
+
+ if (UNEXPECTED(result == FAILURE)) {
+ if (!silent) {
+ zend_throw_exception(php_uri_ce_exception, "Failed to parse the specified query string", 0);
+ }
+
+ return FAILURE;
+ }
+
+ if (UNEXPECTED(zend_array_count(&query_params_object->entry_list) > query_param_options_object->parsing_max_param_count)) {
+ if (!silent) {
+ zend_throw_exception(php_uri_ce_exception, "The specified query string contains too many parameters", 0);
+ }
+ return FAILURE;
+ }
+
+ p = pair_end + 1;
+ }
+
+ return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NONNULL static void throw_cannot_recompose_uri_to_string(const zend_object *object)
+{
+ zend_throw_exception_ex(php_uri_ce_error, 0, "Cannot recompose %s to a string", ZSTR_VAL(object->ce->name));
+}
+
+ZEND_ATTRIBUTE_NONNULL static zend_string *php_uri_query_params_to_rfc1866_string(const php_uri_query_params_object *uri_query_params_object)
+{
+ return php_uri_parser_rfc3986_query_params_to_string(uri_query_params_object, true);
+}
+
+ZEND_ATTRIBUTE_NONNULL static zend_string *php_uri_query_params_to_rfc3986_string(const php_uri_query_params_object *uri_query_params_object)
+{
+ return php_uri_parser_rfc3986_query_params_to_string(uri_query_params_object, false);
+}
+
+ZEND_ATTRIBUTE_NONNULL static zend_string *php_uri_query_params_to_whatwg_string(const php_uri_query_params_object *uri_query_params_object)
+{
+ return php_uri_parser_rfc3986_query_params_to_string(uri_query_params_object, false);
+}
+
+ZEND_ATTRIBUTE_NONNULL static HashTable *php_uri_query_params_get_debug_properties(php_uri_query_params_object *object)
+{
+ const HashTable *std_properties = zend_std_get_properties(&object->std);
+ HashTable *result = zend_array_dup(std_properties);
+
+ zval params;
+ php_uri_query_params_list_entries(object, ¶ms);
+
+ zend_hash_str_add(result, ZEND_STRL("params"), ¶ms);
+
+ return result;
+}
+
+PHP_METHOD(Uri_QueryParams, parseRfc1866)
+{
+ zend_string *query_str;
+ zval *options;
+
+ ZEND_PARSE_PARAMETERS_START(1, 2)
+ Z_PARAM_STR(query_str)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(options, php_uri_ce_query_param_options)
+ ZEND_PARSE_PARAMETERS_END();
+
+ object_init_ex(return_value, Z_CE_P(ZEND_THIS));
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(return_value);
+
+ if (php_uri_query_params_parse(ZSTR_VAL(query_str), ZSTR_LEN(query_str), uri_query_params_object,
+ PHP_URI_QUERY_PARAMS_SPEC_RFC1866, false, options
+ ) == FAILURE) {
+ php_uri_query_params_object_free(Z_URI_QUERY_PARAMS_OBJECT_P(return_value));
+ RETURN_NULL();
+ }
+}
+
+PHP_METHOD(Uri_QueryParams, parseRfc3986)
+{
+ zend_string *query_str;
+ zval *options;
+
+ ZEND_PARSE_PARAMETERS_START(1, 2)
+ Z_PARAM_STR(query_str)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(options, php_uri_ce_query_param_options)
+ ZEND_PARSE_PARAMETERS_END();
+
+ object_init_ex(return_value, Z_CE_P(ZEND_THIS));
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(return_value);
+
+ if (php_uri_query_params_parse(ZSTR_VAL(query_str), ZSTR_LEN(query_str), uri_query_params_object,
+ PHP_URI_QUERY_PARAMS_SPEC_RFC3986, false, options
+ ) == FAILURE) {
+ php_uri_query_params_object_free(Z_URI_QUERY_PARAMS_OBJECT_P(return_value));
+ RETURN_NULL();
+ }
+}
+
+PHP_METHOD(Uri_QueryParams, parseWhatWg)
+{
+ zend_string *query_str;
+ zval *options;
+
+ ZEND_PARSE_PARAMETERS_START(1, 2)
+ Z_PARAM_STR(query_str)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(options, php_uri_ce_query_param_options)
+ ZEND_PARSE_PARAMETERS_END();
+
+ object_init_ex(return_value, Z_CE_P(ZEND_THIS));
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(return_value);
+
+ if (php_uri_query_params_parse(ZSTR_VAL(query_str), ZSTR_LEN(query_str), uri_query_params_object,
+ PHP_URI_QUERY_PARAMS_SPEC_WHATWG, false, options
+ ) == FAILURE) {
+ php_uri_query_params_object_free(Z_URI_QUERY_PARAMS_OBJECT_P(return_value));
+ RETURN_NULL();
+ }
+}
+
+PHP_METHOD(Uri_QueryParams, fromArray)
+{
+ HashTable *query_params_arr;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ARRAY_HT(query_params_arr)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_throw_exception(NULL, "Not implemented", 0);
+
+ RETVAL_COPY(ZEND_THIS);
+}
+
+PHP_METHOD(Uri_QueryParams, __construct)
+{
+ zval *options = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(options, php_uri_ce_query_param_options)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ if (php_uri_query_params_object_init(uri_query_params_object, options) == NULL) {
+ RETURN_THROWS();
+ }
+}
+
+PHP_METHOD(Uri_QueryParams, append)
+{
+ zend_string *name;
+ zval *value;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_STR(name)
+ Z_PARAM_ZVAL(value)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ zend_string *value_str = php_uri_query_params_convert_value_to_str(uri_query_params_object, value);
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ php_uri_query_params_append_entry(uri_query_params_object, name, value_str);
+
+ RETVAL_COPY(ZEND_THIS);
+}
+
+PHP_METHOD(Uri_QueryParams, delete)
+{
+ zend_string *name;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ php_uri_query_params_delete_entries(uri_query_params_object, name);
+
+ RETVAL_COPY(ZEND_THIS);
+}
+
+PHP_METHOD(Uri_QueryParams, deleteValue)
+{
+ zend_string *name, *value;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_STR(name)
+ Z_PARAM_STR_OR_NULL(value)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ php_uri_query_params_delete_entries_with_value(uri_query_params_object, name, value);
+
+ RETVAL_COPY(ZEND_THIS);
+}
+
+PHP_METHOD(Uri_QueryParams, has)
+{
+ zend_string *name;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ RETVAL_BOOL(php_uri_query_params_has_entry(uri_query_params_object, name));
+}
+
+PHP_METHOD(Uri_QueryParams, hasValue)
+{
+ zend_string *name;
+ zval *value;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_STR(name)
+ Z_PARAM_ZVAL(value)
+ ZEND_PARSE_PARAMETERS_END();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ zend_string *value_str = php_uri_query_params_convert_value_to_str(uri_query_params_object, value);
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETVAL_BOOL(php_uri_query_params_has_entry_with_value(uri_query_params_object, name, value_str));
+}
+
+PHP_METHOD(Uri_QueryParams, getFirst)
+{
+ zend_string *name;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ php_uri_query_params_find_first_value(uri_query_params_object, name, return_value);
+}
+
+PHP_METHOD(Uri_QueryParams, getLast)
+{
+ zend_string *name;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ php_uri_query_params_find_last_value(uri_query_params_object, name, return_value);
+}
+
+PHP_METHOD(Uri_QueryParams, getAll)
+{
+ zend_string *name;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(name)
+ ZEND_PARSE_PARAMETERS_END();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ php_uri_query_params_find_all_values(uri_query_params_object, name, return_value);
+}
+
+PHP_METHOD(Uri_QueryParams, list)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ php_uri_query_params_list_entries(uri_query_params_object, return_value);
+}
+
+PHP_METHOD(Uri_QueryParams, getIterator)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ zend_create_internal_iterator_zval(return_value, ZEND_THIS);
+}
+
+PHP_METHOD(Uri_QueryParams, count)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ RETVAL_LONG(zend_array_count(&uri_query_params_object->entry_list) - uri_query_params_object->tombstone_count);
+}
+
+PHP_METHOD(Uri_QueryParams, set)
+{
+ zend_string *name;
+ zval *value;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_STR(name)
+ Z_PARAM_ZVAL(value)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ zend_string *value_str = php_uri_query_params_convert_value_to_str(uri_query_params_object, value);
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ php_uri_query_params_set_entry(uri_query_params_object, name, value_str);
+
+ RETVAL_COPY(ZEND_THIS);
+}
+
+PHP_METHOD(Uri_QueryParams, sort)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ zend_throw_exception(NULL, "Not implemented", 0);
+
+ RETVAL_COPY(ZEND_THIS);
+}
+
+static void php_uri_query_params_navigate_and_insert(zval *current, const char *suffix, size_t suffix_len, zend_string *value)
+{
+ ZEND_ASSERT(Z_TYPE_P(current) == IS_ARRAY);
+
+ if (suffix_len == 0) {
+ return;
+ }
+
+ ZEND_ASSERT(suffix[0] == '[');
+
+ const char *close = memchr(suffix + 1, ']', suffix_len - 1);
+ if (UNEXPECTED(close == NULL)) {
+ return; /* TODO malformed key */
+ }
+
+ const char *seg = suffix + 1;
+ size_t seg_len = (size_t) (close - seg);
+ const char *rest = close + 1;
+ size_t rest_len = suffix_len - (size_t) (rest - suffix);
+
+ if (seg_len == 0) {
+ if (rest_len == 0) {
+ zval val;
+ ZVAL_STR_COPY(&val, value);
+ zend_hash_next_index_insert(Z_ARRVAL_P(current), &val);
+ } else {
+ zval new_arr;
+ array_init(&new_arr);
+ zval *inserted = zend_hash_next_index_insert(Z_ARRVAL_P(current), &new_arr);
+ if (EXPECTED(inserted != NULL)) {
+ php_uri_query_params_navigate_and_insert(inserted, rest, rest_len, value);
+ }
+ }
+ return;
+ }
+
+ zend_long lval;
+ zval *nested;
+
+ if (is_numeric_string(seg, seg_len, &lval, NULL, 0) == IS_LONG) {
+ /* Numeric key: foo[0] → integer index */
+ nested = zend_hash_index_find(Z_ARRVAL_P(current), (zend_ulong) lval);
+ if (rest_len == 0) {
+ zval val;
+ ZVAL_STR_COPY(&val, value);
+ zend_hash_index_update(Z_ARRVAL_P(current), (zend_ulong) lval, &val);
+ } else {
+ if (nested == NULL || Z_TYPE_P(nested) != IS_ARRAY) {
+ zval new_arr;
+ array_init(&new_arr);
+ nested = zend_hash_index_update(Z_ARRVAL_P(current), (zend_ulong) lval, &new_arr);
+ }
+ if (EXPECTED(nested != NULL)) {
+ php_uri_query_params_navigate_and_insert(nested, rest, rest_len, value);
+ }
+ }
+ } else {
+ /* String key: foo[a] */
+ zend_string *seg_str = zend_string_init(seg, seg_len, 0);
+ if (rest_len == 0) {
+ zval val;
+ ZVAL_STR_COPY(&val, value);
+ zend_hash_update(Z_ARRVAL_P(current), seg_str, &val);
+ } else {
+ nested = zend_hash_find(Z_ARRVAL_P(current), seg_str);
+ if (nested == NULL || Z_TYPE_P(nested) != IS_ARRAY) {
+ zval new_arr;
+ array_init(&new_arr);
+ nested = zend_hash_update(Z_ARRVAL_P(current), seg_str, &new_arr);
+ }
+ if (EXPECTED(nested != NULL)) {
+ php_uri_query_params_navigate_and_insert(nested, rest, rest_len, value);
+ }
+ }
+ zend_string_release(seg_str);
+ }
+}
+
+static void php_uri_query_params_insert_entry(
+ zval *result, const php_uri_query_param_list_entry *list_entry, bool is_list
+) {
+ const char *key = ZSTR_VAL(list_entry->key);
+ size_t key_len = ZSTR_LEN(list_entry->key);
+
+ const char *bracket = memchr(key, '[', key_len);
+ size_t root_len = bracket != NULL ? (size_t) (bracket - key) : key_len;
+ const char *suffix = (bracket != NULL) ? bracket : NULL;
+ size_t suffix_len = (bracket != NULL) ? (key_len - root_len) : 0;
+
+ if (suffix == NULL) {
+ /* Bare key: scalar or list */
+ if (is_list) {
+ zval *existing = zend_hash_str_find(Z_ARRVAL_P(result), key, key_len);
+ if (existing == NULL) {
+ zval list;
+ array_init(&list);
+ zval val;
+ ZVAL_STR_COPY(&val, list_entry->value);
+ zend_hash_next_index_insert(Z_ARRVAL(list), &val);
+ zend_hash_str_add(Z_ARRVAL_P(result), key, key_len, &list);
+ } else {
+ ZEND_ASSERT(Z_TYPE_P(existing) == IS_ARRAY);
+ zval val;
+ ZVAL_STR_COPY(&val, list_entry->value);
+ zend_hash_next_index_insert(Z_ARRVAL_P(existing), &val);
+ }
+ } else {
+ zval val;
+ ZVAL_STR_COPY(&val, list_entry->value);
+ zend_hash_str_update(Z_ARRVAL_P(result), key, key_len, &val);
+ }
+ return;
+ }
+
+ /* key with brackets: navigate */
+ zval *container = zend_hash_str_find(Z_ARRVAL_P(result), key, root_len);
+ if (container == NULL || Z_TYPE_P(container) != IS_ARRAY) {
+ zval new_arr;
+ array_init(&new_arr);
+ container = zend_hash_str_update(Z_ARRVAL_P(result), key, root_len, &new_arr);
+ }
+
+ if (EXPECTED(container != NULL)) {
+ php_uri_query_params_navigate_and_insert(container, suffix, suffix_len, list_entry->value);
+ }
+}
+
+PHP_METHOD(Uri_QueryParams, toArray)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ const php_uri_query_params_object *obj = php_uri_query_params_object_from_obj(Z_OBJ_P(ZEND_THIS));
+
+ array_init(return_value);
+
+ php_uri_query_param_list_entry *list_entry;
+
+ ZEND_HASH_FOREACH_PTR(&obj->entry_list, list_entry) {
+ if (list_entry->key == NULL) {
+ continue; /* tombstone */
+ }
+
+ bool is_list = false;
+ if (memchr(ZSTR_VAL(list_entry->key), '[', ZSTR_LEN(list_entry->key)) == NULL) {
+ const php_uri_query_param_lookup_entry *lookup_entry = zend_hash_find_ptr(&obj->entry_lookup_table, list_entry->key);
+ is_list = lookup_entry != NULL && lookup_entry->first_index != lookup_entry->last_index;
+ }
+
+ php_uri_query_params_insert_entry(return_value, list_entry, is_list);
+
+ } ZEND_HASH_FOREACH_END();
+}
+
+PHP_METHOD(Uri_QueryParams, toRfc1866String)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ zend_string *result = php_uri_query_params_to_rfc1866_string(uri_query_params_object);
+ if (result == NULL) {
+ throw_cannot_recompose_uri_to_string(Z_OBJ_P(ZEND_THIS));
+ RETURN_THROWS();
+ }
+
+ ZVAL_STR(return_value, result);
+}
+
+PHP_METHOD(Uri_QueryParams, toRfc3986String)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ zend_string *result = php_uri_query_params_to_rfc3986_string(uri_query_params_object);
+ if (result == NULL) {
+ throw_cannot_recompose_uri_to_string(Z_OBJ_P(ZEND_THIS));
+ RETURN_THROWS();
+ }
+
+ ZVAL_STR(return_value, result);
+}
+
+PHP_METHOD(Uri_QueryParams, toWhatWgString)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ const php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ zend_string *result = php_uri_query_params_to_whatwg_string(uri_query_params_object);
+ if (result == NULL) {
+ throw_cannot_recompose_uri_to_string(Z_OBJ_P(ZEND_THIS));
+ RETURN_THROWS();
+ }
+
+ ZVAL_STR(return_value, result);
+}
+
+PHP_METHOD(Uri_QueryParams, __serialize)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ /* Serialize state: "query" key in the first array */
+ zend_string *uri_query_param_str = php_uri_query_params_to_rfc3986_string(uri_query_params_object);
+ if (uri_query_param_str == NULL) {
+ throw_cannot_recompose_uri_to_string(Z_OBJ_P(ZEND_THIS));
+ RETURN_THROWS();
+ }
+ zval tmp;
+ ZVAL_STR(&tmp, uri_query_param_str);
+
+ array_init(return_value);
+
+ zval arr;
+ array_init(&arr);
+ zend_hash_str_add_new(Z_ARRVAL(arr), PHP_URI_SERIALIZE_URI_QUERY_PARAM_FIELD_NAME, sizeof(PHP_URI_SERIALIZE_URI_QUERY_PARAM_FIELD_NAME) - 1, &tmp);
+ zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &arr);
+
+ /* Serialize regular properties: second array */
+ ZVAL_ARR(&arr, uri_query_params_object->std.handlers->get_properties(&uri_query_params_object->std));
+ Z_TRY_ADDREF(arr);
+ zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &arr);
+}
+
+static void uri_query_params_unserialize(INTERNAL_FUNCTION_PARAMETERS)
+{
+ HashTable *data;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ARRAY_HT(data)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+ if (uri_query_params_object->is_initialized) {
+ /* Intentionally throw two exceptions for proper chaining. */
+ zend_throw_error(NULL, "Cannot modify readonly object of class %s", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ RETURN_THROWS();
+ }
+
+ /* Verify the expected number of elements, this implicitly ensures that no additional elements are present. */
+ if (zend_hash_num_elements(data) != 2) {
+ zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ RETURN_THROWS();
+ }
+
+ /* Unserialize state: "query" key in the first array */
+ const zval *arr = zend_hash_index_find(data, 0);
+ if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) {
+ zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ RETURN_THROWS();
+ }
+
+ /* Verify the expected number of elements inside the first array, this implicitly ensures that no additional elements are present. */
+ if (zend_hash_num_elements(Z_ARRVAL_P(arr)) != 1) {
+ zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ RETURN_THROWS();
+ }
+
+ const zval *uri_query_param_zv = zend_hash_str_find_ind(Z_ARRVAL_P(arr), ZEND_STRL(PHP_URI_SERIALIZE_URI_QUERY_PARAM_FIELD_NAME));
+ if (uri_query_param_zv == NULL || Z_TYPE_P(uri_query_param_zv) != IS_STRING) {
+ zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ RETURN_THROWS();
+ }
+
+ const zend_result result = SUCCESS; // TODO
+
+ if (result == FAILURE) {
+ RETURN_THROWS();
+ }
+
+ uri_query_params_object->is_initialized = true;
+
+ /* Unserialize regular properties: second array */
+ arr = zend_hash_index_find(data, 1);
+ if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) {
+ zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ RETURN_THROWS();
+ }
+
+ /* Verify that there is no regular property in the second array, because the query param classes have no properties and they are final. */
+ if (zend_hash_num_elements(Z_ARRVAL_P(arr)) > 0) {
+ zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uri_query_params_object->std.ce->name));
+ RETURN_THROWS();
+ }
+}
+
+PHP_METHOD(Uri_QueryParams, __unserialize)
+{
+ uri_query_params_unserialize(INTERNAL_FUNCTION_PARAM_PASSTHRU);
+}
+
+PHP_METHOD(Uri_QueryParams, __debugInfo)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_uri_query_params_object *uri_query_params_object = Z_URI_QUERY_PARAMS_OBJECT_P(ZEND_THIS);
+
+ RETURN_ARR(php_uri_query_params_get_debug_properties(uri_query_params_object));
+}
+
+PHP_METHOD(Uri_QueryParamOptions, __construct)
+{
+ zend_long parsing_max_query_string_length = PARSING_MAX_QUERY_STRING_LENGTH_DEFAULT;
+ zend_long parsing_max_param_count = PARSING_MAX_QUERY_PARAM_COUNT_DEFAULT;
+ zend_string *true_value = NULL;
+ zend_string *false_value = NULL;
+ bool use_null_as_empty_string = USE_NULL_AS_EMPTY_STRING_DEFAULT;
+
+ ZEND_PARSE_PARAMETERS_START(0, 5)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG(parsing_max_query_string_length)
+ Z_PARAM_LONG(parsing_max_param_count)
+ Z_PARAM_STR(true_value)
+ Z_PARAM_STR(false_value)
+ Z_PARAM_BOOL(use_null_as_empty_string)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (parsing_max_query_string_length < 0) {
+ zend_argument_value_error(1, "must be greater than or equal to 0");
+ RETURN_THROWS();
+ }
+
+ if (parsing_max_param_count < 0) {
+ zend_argument_value_error(2, "must be greater than or equal to 0");
+ RETURN_THROWS();
+ }
+
+ if (php_uri_query_param_options_update_properties(
+ Z_OBJ_P(ZEND_THIS), parsing_max_query_string_length, parsing_max_param_count, true_value, false_value,
+ use_null_as_empty_string
+ ) == FAILURE) {
+ RETURN_THROWS();
+ }
+}
+
+PHP_METHOD(Uri_QueryParamOptions, __debugInfo)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ const php_uri_query_param_options_object *object = Z_URI_QUERY_PARAM_OPTIONS_OBJECT_P(ZEND_THIS);
+
+ array_init(return_value);
+
+ add_assoc_long(return_value, "parsingMaxQueryStringLength", object->parsing_max_query_string_length);
+ add_assoc_long(return_value, "parsingMaxParamCount", object->parsing_max_param_count);
+ add_assoc_str(return_value, "trueValue", zend_string_copy(object->true_value));
+ add_assoc_str(return_value, "falseValue", zend_string_copy(object->false_value));
+ add_assoc_bool(return_value, "useNullAsLong", object->use_null_as_string);
+}
diff --git a/ext/uri/php_uri_query.h b/ext/uri/php_uri_query.h
new file mode 100644
index 000000000000..358bc4d31378
--- /dev/null
+++ b/ext/uri/php_uri_query.h
@@ -0,0 +1,42 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright © The PHP Group and Contributors. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to the Modified BSD License that is |
+ | bundled with this package in the file LICENSE, and is available |
+ | through the World Wide Web at . |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Máté Kocsis |
+ +----------------------------------------------------------------------+
+*/
+
+#ifndef PHP_URI_QUERY_H
+#define PHP_URI_QUERY_H
+
+#define php_uri_query_params_object_from_obj(object) ZEND_CONTAINER_OF(object, php_uri_query_params_object, std)
+
+#define Z_URI_QUERY_PARAMS_OBJECT_P(zv) php_uri_query_params_object_from_obj(Z_OBJ_P((zv)))
+
+#define php_uri_query_param_options_object_from_obj(object) ZEND_CONTAINER_OF(object, php_uri_query_param_options_object, std)
+
+#define Z_URI_QUERY_PARAM_OPTIONS_OBJECT_P(zv) php_uri_query_param_options_object_from_obj(Z_OBJ_P((zv)))
+
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_object_query_params_create_object(zend_class_entry *ce);
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_params_object_free(php_uri_query_params_object *uri_query_params_object);
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_params_object_handler_free(zend_object *object);
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_query_params_object_handler_clone(zend_object *object);
+ZEND_ATTRIBUTE_NONNULL zend_object_iterator *php_uri_object_query_params_get_iterator(zend_class_entry *ce, zval *object, int by_ref);
+
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_object_create_query_param_options(zend_class_entry *ce);
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_param_options_object_free(php_uri_query_param_options_object *uri_query_param_options_object);
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_param_options_object_handler_free(zend_object *object);
+ZEND_ATTRIBUTE_NONNULL zend_object *php_uri_query_param_options_object_handler_clone(zend_object *object);
+ZEND_ATTRIBUTE_NONNULL int php_uri_query_param_options_object_handler_has_property(zend_object *obj, zend_string *name, int check_empty, void **cache_slot);
+ZEND_ATTRIBUTE_NONNULL zval *php_uri_query_param_options_object_handler_read_property(zend_object *obj, zend_string *name, int type, void **cache_slot, zval *rv);
+ZEND_ATTRIBUTE_NONNULL zval *php_uri_query_param_options_object_handler_write_property(zend_object *obj, zend_string *name, zval *value, void **cache_slot);
+ZEND_ATTRIBUTE_NONNULL zval *php_uri_query_param_options_object_handler_get_property_ptr_ptr(zend_object *object, zend_string *member, int type, void **cache_slot);
+ZEND_ATTRIBUTE_NONNULL void php_uri_query_param_options_object_handler_unset_property(zend_object *obj, zend_string *name, void **cache_slot);
+
+#endif
diff --git a/ext/uri/tests/query_params/append_error_enum_unbacked.phpt b/ext/uri/tests/query_params/append_error_enum_unbacked.phpt
new file mode 100644
index 000000000000..b8fc66758473
--- /dev/null
+++ b/ext/uri/tests/query_params/append_error_enum_unbacked.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Test Uri\QueryParams::append() - error - unbacked enum
+--FILE--
+append("foo", TestEnum::Bar);
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+ValueError: Uri\QueryParams::append(): Argument #2 ($value) must not contain an object
diff --git a/ext/uri/tests/query_params/append_error_object.phpt b/ext/uri/tests/query_params/append_error_object.phpt
new file mode 100644
index 000000000000..624f95bd43b4
--- /dev/null
+++ b/ext/uri/tests/query_params/append_error_object.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\QueryParams::append() - error - object
+--FILE--
+append("foo", new stdClass());
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+ValueError: Uri\QueryParams::append(): Argument #2 ($value) must not contain an object
diff --git a/ext/uri/tests/query_params/append_error_resource.phpt b/ext/uri/tests/query_params/append_error_resource.phpt
new file mode 100644
index 000000000000..e5b82df313da
--- /dev/null
+++ b/ext/uri/tests/query_params/append_error_resource.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\QueryParams::append() - error - resource
+--FILE--
+append("foo", fopen("php://memory", "r"));
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+ValueError: Uri\QueryParams::append(): Argument #2 ($value) must not contain a resource
diff --git a/ext/uri/tests/query_params/append_success_array.phpt b/ext/uri/tests/query_params/append_success_array.phpt
new file mode 100644
index 000000000000..bd5e35727955
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_array.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Test Uri\QueryParams::append() - success - array value
+--FILE--
+append("baz", [null, false, "key2" => true, 123, "", ["bla"]]);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECTF--
+object(Uri\QueryParams)#%d (%d) {
+ ["baz[0]"]=>
+ NULL
+ ["baz[1]"]=>
+ string(1) "0"
+ ["baz[key2]"]=>
+ string(1) "1"
+ ["baz[2]"]=>
+ string(3) "123"
+ ["baz[3]"]=>
+ string(0) ""
+ ["baz[4][]"]=>
+ string(3) "bla"
+}
+string(87) "baz%5B0%5D&baz%5B1%5D=0&baz%5Bkey2%5D=1&baz%5B2%5D=123&baz%5B3%5D=&baz%5B4%5D%5B%5D=bla"
diff --git a/ext/uri/tests/query_params/append_success_array_empty.phpt b/ext/uri/tests/query_params/append_success_array_empty.phpt
new file mode 100644
index 000000000000..8cabc0e0668f
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_array_empty.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Test Uri\QueryParams::append() - success - empty array value
+--FILE--
+append("baz", []);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECTF--
+object(Uri\QueryParams)#%d (%d) {
+ ["params"]=>
+ array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "baz"
+ [1]=>
+ NULL
+ }
+ }
+}
+string(11) "foo=bar&baz"
diff --git a/ext/uri/tests/query_params/append_success_array_multi_level_list.phpt b/ext/uri/tests/query_params/append_success_array_multi_level_list.phpt
new file mode 100644
index 000000000000..3c970e5d9626
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_array_multi_level_list.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Test Uri\QueryParams::append() - success - multi-level array value
+--FILE--
+append("foo", [[0 => "bar"], [123]]);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECTF--
+object(Uri\QueryParams)#%d (%d) {
+ ["foo[][]"]=>
+ string(3) "bar"
+ ["foo[][]"]=>
+ string(3) "123"
+}
+string(39) "foo%5B%5D%5B%5D=bar&foo%5B%5D%5B%5D=123"
diff --git a/ext/uri/tests/query_params/append_success_array_multi_level_map.phpt b/ext/uri/tests/query_params/append_success_array_multi_level_map.phpt
new file mode 100644
index 000000000000..96b2ff530e86
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_array_multi_level_map.phpt
@@ -0,0 +1,61 @@
+--TEST--
+Test Uri\QueryParams::append() - success - multi-level array value
+--FILE--
+append("foo", ["bar" => [0 => "baz", 2 => 123, 3 => [], 4 => [1, 2, 3]]]);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECTF--
+object(Uri\QueryParams)#%d (%d) {
+ ["params"]=>
+ array(6) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(1) "0"
+ [1]=>
+ string(3) "baz"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(11) "foo[bar][2]"
+ [1]=>
+ string(3) "123"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(11) "foo[bar][3]"
+ [1]=>
+ NULL
+ }
+ [3]=>
+ array(2) {
+ [0]=>
+ string(13) "foo[bar][4][]"
+ [1]=>
+ string(1) "1"
+ }
+ [4]=>
+ array(2) {
+ [0]=>
+ string(13) "foo[bar][4][]"
+ [1]=>
+ string(1) "2"
+ }
+ [5]=>
+ array(2) {
+ [0]=>
+ string(13) "foo[bar][4][]"
+ [1]=>
+ string(1) "3"
+ }
+ }
+}
+string(133) "0=baz&foo%5Bbar%5D%5B2%5D=123&foo%5Bbar%5D%5B3%5D&foo%5Bbar%5D%5B4%5D%5B%5D=1&foo%5Bbar%5D%5B4%5D%5B%5D=2&foo%5Bbar%5D%5B4%5D%5B%5D=3"
diff --git a/ext/uri/tests/query_params/append_success_basic.phpt b/ext/uri/tests/query_params/append_success_basic.phpt
new file mode 100644
index 000000000000..a2d70d8a0a28
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_basic.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\QueryParams::append() - success - basic
+--FILE--
+append("baz", "qux");
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(3) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(3) "baz"
+ [1]=>
+ string(3) "qux"
+ }
+}
+string(23) "abc=123&foo=bar&baz=qux"
diff --git a/ext/uri/tests/query_params/append_success_bool.phpt b/ext/uri/tests/query_params/append_success_bool.phpt
new file mode 100644
index 000000000000..1c91adf3d1e9
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_bool.phpt
@@ -0,0 +1,45 @@
+--TEST--
+Test Uri\QueryParams::append() - success - bool value
+--FILE--
+append("baz", false);
+$params->append("qux", true);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(4) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(3) "baz"
+ [1]=>
+ string(1) "0"
+ }
+ [3]=>
+ array(2) {
+ [0]=>
+ string(3) "qux"
+ [1]=>
+ string(1) "1"
+ }
+}
+string(27) "abc=123&foo=bar&baz=0&qux=1"
diff --git a/ext/uri/tests/query_params/append_success_enum_backed.phpt b/ext/uri/tests/query_params/append_success_enum_backed.phpt
new file mode 100644
index 000000000000..9b56cc8092d7
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_enum_backed.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Test Uri\QueryParams::append() - success - backed enum
+--FILE--
+append("foo", StringEnum::Bar);
+$params->append("baz", IntEnum::Baz);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "baz"
+ [1]=>
+ string(3) "123"
+ }
+}
+string(15) "foo=bar&baz=123"
diff --git a/ext/uri/tests/query_params/append_success_existing.phpt b/ext/uri/tests/query_params/append_success_existing.phpt
new file mode 100644
index 000000000000..3f46db148f0d
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_existing.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\QueryParams::append() - success - existing parameter
+--FILE--
+append("foo", "bar");
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(3) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+}
+string(23) "abc=123&foo=bar&foo=bar"
diff --git a/ext/uri/tests/query_params/append_success_int.phpt b/ext/uri/tests/query_params/append_success_int.phpt
new file mode 100644
index 000000000000..772c1c3aa8a8
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_int.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\QueryParams::append() - success - integer value
+
+--FILE--
+append("baz", 0);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(3) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(3) "baz"
+ [1]=>
+ string(1) "0"
+ }
+}
+string(21) "abc=123&foo=bar&baz=0"
diff --git a/ext/uri/tests/query_params/append_success_list.phpt b/ext/uri/tests/query_params/append_success_list.phpt
new file mode 100644
index 000000000000..9b2b052c3034
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_list.phpt
@@ -0,0 +1,47 @@
+--TEST--
+Test Uri\QueryParams::append() - success - list
+--FILE--
+append("foo", [1, 2, 3]);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECTF--
+object(Uri\QueryParams)#%d (%d) {
+ ["params"]=>
+ array(4) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(1) "0"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(5) "foo[]"
+ [1]=>
+ string(1) "1"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(5) "foo[]"
+ [1]=>
+ string(1) "2"
+ }
+ [3]=>
+ array(2) {
+ [0]=>
+ string(5) "foo[]"
+ [1]=>
+ string(1) "3"
+ }
+ }
+}
+string(41) "foo=0&foo%5B%5D=1&foo%5B%5D=2&foo%5B%5D=3"
diff --git a/ext/uri/tests/query_params/append_success_null.phpt b/ext/uri/tests/query_params/append_success_null.phpt
new file mode 100644
index 000000000000..b2f48a161ce1
--- /dev/null
+++ b/ext/uri/tests/query_params/append_success_null.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\QueryParams::append() - success - null value
+--FILE--
+append("baz", null);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(3) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(3) "baz"
+ [1]=>
+ NULL
+ }
+}
+string(19) "abc=123&foo=bar&baz"
diff --git a/ext/uri/tests/query_params/construct_success_basic.phpt b/ext/uri/tests/query_params/construct_success_basic.phpt
new file mode 100644
index 000000000000..a6cb468cd2e7
--- /dev/null
+++ b/ext/uri/tests/query_params/construct_success_basic.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Test Uri\QueryParams::__construct() - success - basic
+--FILE--
+toRfc3986String());
+
+?>
+--EXPECTF--
+object(Uri\QueryParams)#%d (%d) {
+ ["options":"Uri\QueryParams":private]=>
+ object(Uri\QueryParamOptions)#%d (%d) {
+ ["parsingMaxQueryStringLength"]=>
+ int(10000)
+ ["parsingMaxParamCount"]=>
+ int(1000)
+ ["trueValue"]=>
+ string(1) "1"
+ ["falseValue"]=>
+ string(1) "0"
+ ["useNullAsLong"]=>
+ bool(false)
+ }
+ ["params"]=>
+ array(0) {
+ }
+}
+string(0) ""
diff --git a/ext/uri/tests/query_params/construct_success_options.phpt b/ext/uri/tests/query_params/construct_success_options.phpt
new file mode 100644
index 000000000000..b348a25ab476
--- /dev/null
+++ b/ext/uri/tests/query_params/construct_success_options.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Test Uri\QueryParams::__construct() - success - with options set
+--FILE--
+toRfc3986String());
+
+?>
+--EXPECTF--
+object(Uri\QueryParams)#%d (%d) {
+ ["options":"Uri\QueryParams":private]=>
+ object(Uri\QueryParamOptions)#%d (%d) {
+ ["parsingMaxQueryStringLength"]=>
+ int(100)
+ ["parsingMaxParamCount"]=>
+ int(10)
+ ["trueValue"]=>
+ string(4) "true"
+ ["falseValue"]=>
+ string(5) "false"
+ ["useNullAsLong"]=>
+ bool(true)
+ }
+ ["params"]=>
+ array(0) {
+ }
+}
+string(0) ""
diff --git a/ext/uri/tests/query_params/delete_success_existing.phpt b/ext/uri/tests/query_params/delete_success_existing.phpt
new file mode 100644
index 000000000000..1851910ea492
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_success_existing.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\QueryParams::delete() - success - basic
+--FILE--
+delete("abc");
+$params->delete("foo");
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(0) {
+}
+string(0) ""
diff --git a/ext/uri/tests/query_params/delete_success_multiple.phpt b/ext/uri/tests/query_params/delete_success_multiple.phpt
new file mode 100644
index 000000000000..83bc5fc3799f
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_success_multiple.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Test Uri\QueryParams::delete() - success - multiple values
+--FILE--
+delete("abc");
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(1) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+}
+string(7) "foo=bar"
diff --git a/ext/uri/tests/query_params/delete_success_non_existent.phpt b/ext/uri/tests/query_params/delete_success_non_existent.phpt
new file mode 100644
index 000000000000..4d90c35ce8b4
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_success_non_existent.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Test Uri\QueryParams::delete() - success - non-existent key
+--FILE--
+delete("xyz");
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+}
+string(15) "abc=123&foo=bar"
diff --git a/ext/uri/tests/query_params/delete_value_success_existing_key_missing_value.phpt b/ext/uri/tests/query_params/delete_value_success_existing_key_missing_value.phpt
new file mode 100644
index 000000000000..872d19ac613b
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_value_success_existing_key_missing_value.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\QueryParams::deleteValue() - success - existing key, missing value
+--FILE--
+deleteValue("abc", "");
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(3) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "456"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ NULL
+ }
+}
+string(19) "abc=123&abc=456&abc"
diff --git a/ext/uri/tests/query_params/delete_value_success_existing_key_value.phpt b/ext/uri/tests/query_params/delete_value_success_existing_key_value.phpt
new file mode 100644
index 000000000000..97ecba7a0afd
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_value_success_existing_key_value.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Test Uri\QueryParams::deleteValue() - success - existing key, existing value
+--FILE--
+deleteValue("abc", 456);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "789"
+ }
+}
+string(15) "abc=123&abc=789"
diff --git a/ext/uri/tests/query_params/delete_value_success_int.phpt b/ext/uri/tests/query_params/delete_value_success_int.phpt
new file mode 100644
index 000000000000..de266c89fad8
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_value_success_int.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Test Uri\QueryParams::deleteValue() - success - int value
+--FILE--
+deleteValue("abc", 123);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "456"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "foo"
+ }
+}
+string(15) "abc=456&abc=foo"
diff --git a/ext/uri/tests/query_params/delete_value_success_missing_key.phpt b/ext/uri/tests/query_params/delete_value_success_missing_key.phpt
new file mode 100644
index 000000000000..c4e03a96b810
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_value_success_missing_key.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\QueryParams::deleteValue() - success - missing key
+--FILE--
+deleteValue("def", 456);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(3) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "456"
+ }
+ [2]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "789"
+ }
+}
+string(23) "abc=123&abc=456&abc=789"
diff --git a/ext/uri/tests/query_params/delete_value_success_null.phpt b/ext/uri/tests/query_params/delete_value_success_null.phpt
new file mode 100644
index 000000000000..9e346a30b4c5
--- /dev/null
+++ b/ext/uri/tests/query_params/delete_value_success_null.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Test Uri\QueryParams::deleteValue() - success - null value
+--FILE--
+deleteValue("abc", null);
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "456"
+ }
+}
+string(15) "abc=123&abc=456"
diff --git a/ext/uri/tests/query_params/get_iterator_basic.phpt b/ext/uri/tests/query_params/get_iterator_basic.phpt
new file mode 100644
index 000000000000..b5f0456c4f84
--- /dev/null
+++ b/ext/uri/tests/query_params/get_iterator_basic.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Test Uri\QueryParams::getIterator() - success - basic
+--FILE--
+
+--EXPECT--
+array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "123"
+}
+array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "456"
+}
+array(2) {
+ [0]=>
+ string(3) "bar"
+ [1]=>
+ string(3) "789"
+}
diff --git a/ext/uri/tests/query_params/has_success_existing.phpt b/ext/uri/tests/query_params/has_success_existing.phpt
new file mode 100644
index 000000000000..d419158fe23b
--- /dev/null
+++ b/ext/uri/tests/query_params/has_success_existing.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\QueryParams::has() - success - existing
+--FILE--
+has("abc"));
+
+?>
+--EXPECT--
+bool(true)
diff --git a/ext/uri/tests/query_params/has_success_non_existent.phpt b/ext/uri/tests/query_params/has_success_non_existent.phpt
new file mode 100644
index 000000000000..d3d2a3f866f1
--- /dev/null
+++ b/ext/uri/tests/query_params/has_success_non_existent.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\QueryParams::has() - success - non-existent
+--FILE--
+has("bar"));
+
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/uri/tests/query_params/has_value_success_existing_key_missing_value.phpt b/ext/uri/tests/query_params/has_value_success_existing_key_missing_value.phpt
new file mode 100644
index 000000000000..8515b9a34e15
--- /dev/null
+++ b/ext/uri/tests/query_params/has_value_success_existing_key_missing_value.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\QueryParams::hasValue() - success - existing key, missing value
+--FILE--
+hasValue("foo", 123));
+
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/uri/tests/query_params/has_value_success_existing_key_value.phpt b/ext/uri/tests/query_params/has_value_success_existing_key_value.phpt
new file mode 100644
index 000000000000..a5e4a53d43d5
--- /dev/null
+++ b/ext/uri/tests/query_params/has_value_success_existing_key_value.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\QueryParams::hasValue() - success - existing key, existing value
+--FILE--
+hasValue("foo", "bar"));
+
+?>
+--EXPECT--
+bool(true)
diff --git a/ext/uri/tests/query_params/has_value_success_int.phpt b/ext/uri/tests/query_params/has_value_success_int.phpt
new file mode 100644
index 000000000000..260ac3c1c118
--- /dev/null
+++ b/ext/uri/tests/query_params/has_value_success_int.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\QueryParams::hasValue() - success - int value
+--FILE--
+hasValue("abc", "123"));
+
+?>
+--EXPECT--
+bool(true)
diff --git a/ext/uri/tests/query_params/has_value_success_missing_key.phpt b/ext/uri/tests/query_params/has_value_success_missing_key.phpt
new file mode 100644
index 000000000000..89627230ea88
--- /dev/null
+++ b/ext/uri/tests/query_params/has_value_success_missing_key.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\QueryParams::hasValue() - success - missing key
+--FILE--
+hasValue("baz", "abc"));
+
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_basic.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_basic.phpt
new file mode 100644
index 000000000000..41eaf6e674cd
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_basic.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - basic
+--FILE--
+list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "abc"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "bar"
+ }
+}
+string(15) "abc=123&foo=bar"
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_empty.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_empty.phpt
new file mode 100644
index 000000000000..cb1c04f51886
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_empty.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - empty string
+--FILE--
+list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(0) {
+}
+string(0) ""
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_empty_key.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_empty_key.phpt
new file mode 100644
index 000000000000..281c069840a1
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_empty_key.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - empty query param pair
+--FILE--
+list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(0) ""
+ [1]=>
+ string(1) "1"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(0) ""
+ [1]=>
+ string(1) "2"
+ }
+}
+string(5) "=1&=2"
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_empty_pair.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_empty_pair.phpt
new file mode 100644
index 000000000000..dc5020c0b595
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_empty_pair.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - empty query param pair
+--FILE--
+list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(1) "a"
+ [1]=>
+ string(1) "1"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(1) "b"
+ [1]=>
+ string(1) "1"
+ }
+}
+string(7) "a=1&b=1"
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_empty_value.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_empty_value.phpt
new file mode 100644
index 000000000000..160b3ba83774
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_empty_value.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - empty query param pair
+--FILE--
+list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(1) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(1) "a"
+ [1]=>
+ string(0) ""
+ }
+}
+string(2) "a="
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_null.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_null.phpt
new file mode 100644
index 000000000000..c96ea048b610
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_null.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - null value
+--FILE--
+list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(1) "a"
+ [1]=>
+ NULL
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(1) "b"
+ [1]=>
+ string(1) "c"
+ }
+}
+string(5) "a&b=c"
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_reserved.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_reserved.phpt
new file mode 100644
index 000000000000..d23ea53b7fd2
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_reserved.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - reserved characters are not validated
+--FILE--
+=b@r");
+
+var_dump($params->list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(4) "ab:c"
+ [1]=>
+ string(4) "#123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(5) ""
+ [1]=>
+ string(3) "b@r"
+ }
+}
+string(25) "ab:c=%23123&%3Cfoo%3E=b@r"
diff --git a/ext/uri/tests/query_params/parse_rfc3986_success_space.phpt b/ext/uri/tests/query_params/parse_rfc3986_success_space.phpt
new file mode 100644
index 000000000000..37b4d1ff09f7
--- /dev/null
+++ b/ext/uri/tests/query_params/parse_rfc3986_success_space.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Test Uri\QueryParams::parseRfc3986() - success - space character
+--FILE--
+list());
+var_dump($params->toRfc3986String());
+
+?>
+--EXPECT--
+array(2) {
+ [0]=>
+ array(2) {
+ [0]=>
+ string(3) "a c"
+ [1]=>
+ string(3) "123"
+ }
+ [1]=>
+ array(2) {
+ [0]=>
+ string(3) "foo"
+ [1]=>
+ string(3) "b r"
+ }
+}
+string(19) "a%20c=123&foo=b%20r"
diff --git a/ext/uri/tests/query_params/to_array_success_multi_dimension.phpt b/ext/uri/tests/query_params/to_array_success_multi_dimension.phpt
new file mode 100644
index 000000000000..9d2bf9dc0e55
--- /dev/null
+++ b/ext/uri/tests/query_params/to_array_success_multi_dimension.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Test Uri\QueryParams::toArray() - success - multi-dimension array
+--FILE--
+toArray());
+
+?>
+--EXPECT--
+array(1) {
+ ["foo"]=>
+ array(2) {
+ ["bar"]=>
+ array(1) {
+ ["baz"]=>
+ string(3) "123"
+ }
+ ["qux"]=>
+ string(4) "quux"
+ }
+}
diff --git a/ext/uri/tests/query_params/to_array_success_multiple_keys_with_brackets.phpt b/ext/uri/tests/query_params/to_array_success_multiple_keys_with_brackets.phpt
new file mode 100644
index 000000000000..4ef744e92a75
--- /dev/null
+++ b/ext/uri/tests/query_params/to_array_success_multiple_keys_with_brackets.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Test Uri\QueryParams::toArray() - success - multiple keys, using brackets
+--FILE--
+toArray());
+
+?>
+--EXPECT--
+array(1) {
+ ["abc"]=>
+ array(2) {
+ [0]=>
+ string(3) "123"
+ [1]=>
+ string(3) "456"
+ }
+}
diff --git a/ext/uri/tests/query_params/to_array_success_multiple_keys_without_brackets.phpt b/ext/uri/tests/query_params/to_array_success_multiple_keys_without_brackets.phpt
new file mode 100644
index 000000000000..c65e35d89358
--- /dev/null
+++ b/ext/uri/tests/query_params/to_array_success_multiple_keys_without_brackets.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Test Uri\QueryParams::toArray() - success - multiple keys, without brackets
+--FILE--
+toArray());
+
+?>
+--EXPECT--
+array(1) {
+ ["abc"]=>
+ array(2) {
+ [0]=>
+ string(3) "123"
+ [1]=>
+ string(3) "456"
+ }
+}
diff --git a/ext/uri/tests/query_params/to_array_success_one_key_with_brackets.phpt b/ext/uri/tests/query_params/to_array_success_one_key_with_brackets.phpt
new file mode 100644
index 000000000000..72a55f71cff3
--- /dev/null
+++ b/ext/uri/tests/query_params/to_array_success_one_key_with_brackets.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\QueryParams::toArray() - success - one key, using the bracket syntax
+--FILE--
+toArray());
+
+?>
+--EXPECT--
+array(1) {
+ ["abc"]=>
+ array(1) {
+ [0]=>
+ string(3) "123"
+ }
+}
diff --git a/ext/uri/tests/query_params/to_array_success_one_key_without_brackets.phpt b/ext/uri/tests/query_params/to_array_success_one_key_without_brackets.phpt
new file mode 100644
index 000000000000..fe105deeadaf
--- /dev/null
+++ b/ext/uri/tests/query_params/to_array_success_one_key_without_brackets.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Test Uri\QueryParams::toArray() - success - one key, without brackets
+--FILE--
+toArray());
+
+?>
+--EXPECT--
+array(1) {
+ ["abc"]=>
+ string(3) "123"
+}
diff --git a/ext/uri/uri_parser_rfc3986.c b/ext/uri/uri_parser_rfc3986.c
index 88201914d593..0014f5b1b103 100644
--- a/ext/uri/uri_parser_rfc3986.c
+++ b/ext/uri/uri_parser_rfc3986.c
@@ -679,6 +679,90 @@ ZEND_ATTRIBUTE_NONNULL static zend_string *php_uri_parser_rfc3986_to_string(void
return uri_string;
}
+zend_string *php_uri_parser_rfc3986_query_component_percent_decode(const char *str, const size_t str_length, const bool plus_to_space)
+{
+ if (UNEXPECTED(str == NULL)) {
+ return NULL;
+ }
+
+ if (UNEXPECTED(str_length == 0)) {
+ return zend_empty_string;
+ }
+
+ const bool needs_percent_decoding = memchr(str, '%', strlen(str));
+ if (EXPECTED(!needs_percent_decoding)) {
+ return zend_string_init(str, str_length, false);
+ }
+
+ zend_string *result = zend_string_alloc(str_length, false);
+
+ memcpy(ZSTR_VAL(result), str, str_length);
+ ZSTR_VAL(result)[str_length] = '\0';
+
+ const char *end = uriUnescapeInPlaceExA(ZSTR_VAL(result), plus_to_space, URI_BR_DONT_TOUCH);
+
+ ZSTR_LEN(result) = (size_t)(end - ZSTR_VAL(result));
+
+ return result;
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_rfc3986_query_component_percent_encode(const char *str, const size_t str_length, const bool space_to_plus)
+{
+ zend_string *result = zend_string_alloc(str_length * 3, false);
+
+ const char *end;
+
+ if (space_to_plus) {
+ end = uriEscapeExA(str, str + str_length, ZSTR_VAL(result), space_to_plus, URI_BR_DONT_TOUCH);
+ } else {
+ end = uriEscapeQueryRfc3986ExA(str, str + str_length, ZSTR_VAL(result), URI_BR_DONT_TOUCH);
+ }
+
+ ZSTR_LEN(result) = (size_t)(end - ZSTR_VAL(result));
+
+ return result;
+}
+
+ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_rfc3986_query_component_append_to_string(
+ const zend_string *component, const bool space_to_plus, smart_str *buf
+) {
+ const bool needs_percent_encoding = true; // TODO
+ if (UNEXPECTED(needs_percent_encoding)) {
+ smart_str_append(buf, php_uri_parser_rfc3986_query_component_percent_encode(ZSTR_VAL(component), ZSTR_LEN(component), space_to_plus));
+ } else {
+ smart_str_appendl(buf, ZSTR_VAL(component), ZSTR_LEN(component));
+ }
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_rfc3986_query_params_to_string(
+ const php_uri_query_params_object *query_params_object, const bool space_to_plus
+) {
+ smart_str buf = {0};
+ bool is_first = true;
+
+ ZEND_HASH_FOREACH_PTR(&query_params_object->entry_list, php_uri_query_param_list_entry *entry) {
+ const zend_string *name = entry->key;
+ const zend_string *value = entry->value;
+
+ if (name == NULL) {
+ continue;
+ }
+
+ if (!is_first) {
+ smart_str_appendc(&buf, '&');
+ }
+ is_first = false;
+
+ php_uri_parser_rfc3986_query_component_append_to_string(name, space_to_plus, &buf);
+ if (value != NULL) {
+ smart_str_appendc(&buf, '=');
+ php_uri_parser_rfc3986_query_component_append_to_string(value, space_to_plus, &buf);
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return buf.s != NULL ? smart_str_extract(&buf) : ZSTR_EMPTY_ALLOC();
+}
+
static void php_uri_parser_rfc3986_destroy(void *uri)
{
php_uri_parser_rfc3986_uris *uriparser_uris = uri;
diff --git a/ext/uri/uri_parser_rfc3986.h b/ext/uri/uri_parser_rfc3986.h
index 9a9f0b38a0c4..c71422b3af86 100644
--- a/ext/uri/uri_parser_rfc3986.h
+++ b/ext/uri/uri_parser_rfc3986.h
@@ -43,4 +43,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(2,3,4,5,6,7,8) php_uri_parser_rfc3986_uris *php_uri_
const zval *path, const zval *query, const zval *fragment
);
+zend_string *php_uri_parser_rfc3986_query_component_percent_decode(const char *str, size_t str_length, bool plus_to_space);
+ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_rfc3986_query_params_to_string(const php_uri_query_params_object *query_params_object, bool space_to_plus);
+
#endif
diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c
index 6d46feb51aae..a39d367adc6b 100644
--- a/ext/uri/uri_parser_whatwg.c
+++ b/ext/uri/uri_parser_whatwg.c
@@ -559,6 +559,22 @@ static zend_result php_uri_parser_whatwg_fragment_write(void *uri, const zval *v
return SUCCESS;
}
+ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_query_component_percent_decode(const char *str, const size_t str_length)
+{
+ lexbor_str_t result = {0};
+
+ const lxb_status_t status = lxb_url_percent_decode_plus(
+ (const lxb_char_t *) str, (const lxb_char_t *) (str + str_length),
+ &result, &lexbor_mraw
+ );
+
+ if (status != LXB_STATUS_OK) {
+ return NULL;
+ }
+
+ return zend_string_init((const char *) result.data, result.length, 0);
+}
+
PHP_RINIT_FUNCTION(uri_parser_whatwg)
{
lxb_status_t status;
diff --git a/ext/uri/uri_parser_whatwg.h b/ext/uri/uri_parser_whatwg.h
index f714ee483680..12e66143ba01 100644
--- a/ext/uri/uri_parser_whatwg.h
+++ b/ext/uri/uri_parser_whatwg.h
@@ -25,6 +25,8 @@ ZEND_ATTRIBUTE_NONNULL void php_uri_parser_whatwg_host_type_read(const lxb_url_t
lxb_url_t *php_uri_parser_whatwg_parse_ex(const char *uri_str, size_t uri_str_len, const lxb_url_t *lexbor_base_url, zval *errors, bool silent);
+ZEND_ATTRIBUTE_NONNULL zend_string *php_uri_parser_whatwg_query_component_percent_decode(const char *str, size_t str_length);
+
PHP_RINIT_FUNCTION(uri_parser_whatwg);
ZEND_MODULE_POST_ZEND_DEACTIVATE_D(uri_parser_whatwg);
diff --git a/ext/uri/uriparser/include/uriparser/Uri.h b/ext/uri/uriparser/include/uriparser/Uri.h
index ddbbd26f9e44..f8e6b5f5100f 100644
--- a/ext/uri/uriparser/include/uriparser/Uri.h
+++ b/ext/uri/uriparser/include/uriparser/Uri.h
@@ -444,6 +444,66 @@ URI_PUBLIC URI_CHAR * URI_FUNC(EscapeEx)(const URI_CHAR * inFirst,
URI_PUBLIC URI_CHAR * URI_FUNC(Escape)(const URI_CHAR * in, URI_CHAR * out,
UriBool spaceToPlus, UriBool normalizeBreaks);
+/**
+ * Percent-encodes all but unreserved characters from the input string and
+ * writes the encoded version to the output string.
+ *
+ * NOTE: Be sure to allocate 3 times the space of the input buffer for
+ * the output buffer for normalizeBreaks == URI_FALSE and 6 times
+ * the space for normalizeBreaks == URI_TRUE
+ * (since e.g. "\x0d" becomes "%0D%0A" in that case).
+ *
+ * NOTE: The implementation treats (both char and) wchar_t units
+ * as code point integers, which works well for code points U+0001 to U+00ff
+ * in host-native endianness but nothing more;
+ * in particular, using uriEscapeQueryRfc3986ExW with arbitrary Unicode input will
+ * not produce healthy results.
+ * Passing UTF-8 input to uriEscapeQueryRfc3986ExA may be useful in some scenarios.
+ * Keep in mind that uriparser is about %URI (RFC 3986) not %IRI (RFC 3987).
+ *
+ * @param inFirst IN: Pointer to first character of the input text
+ * @param inAfterLast IN: Pointer after the last character of the input text
+ * @param out OUT: Encoded text destination
+ * @param normalizeBreaks IN: Whether to convert CR and LF to CR-LF or not.
+ * @return Position of terminator in output string
+ *
+ * @see uriEscapeQueryRfc3986A
+ * @see uriUnescapeInPlaceExA
+ * @since 0.5.2
+ */
+URI_PUBLIC URI_CHAR * URI_FUNC(EscapeQueryRfc3986Ex)(const URI_CHAR * inFirst,
+ const URI_CHAR * inAfterLast, URI_CHAR * out,
+ UriBool normalizeBreaks);
+
+/**
+ * Percent-encodes all but unreserved characters from the input string and
+ * writes the encoded version to the output string.
+ *
+ * NOTE: Be sure to allocate 3 times the space of the input buffer for
+ * the output buffer for normalizeBreaks == URI_FALSE and 6 times
+ * the space for normalizeBreaks == URI_TRUE
+ * (since e.g. "\x0d" becomes "%0D%0A" in that case).
+ *
+ * NOTE: The implementation treats (both char and) wchar_t units
+ * as code point integers, which works well for code points U+0001 to U+00ff
+ * in host-native endianness but nothing more;
+ * in particular, using uriEscapeQueryRfc3986W with arbitrary Unicode input will
+ * not produce healthy results.
+ * Passing UTF-8 input to uriEEscapeQueryRfc3986A may be useful in some scenarios.
+ * Keep in mind that uriparser is about %URI (RFC 3986) not %IRI (RFC 3987).
+ *
+ * @param in IN: Text source
+ * @param out OUT: Encoded text destination
+ * @param normalizeBreaks IN: Whether to convert CR and LF to CR-LF or not.
+ * @return Position of terminator in output string
+ *
+ * @see uriEscapeQueryRfc3986ExA
+ * @see uriUnescapeInPlaceA
+ * @since 0.5.0
+ */
+URI_PUBLIC URI_CHAR * URI_FUNC(EscapeQueryRfc3986)(const URI_CHAR * in, URI_CHAR * out,
+ UriBool normalizeBreaks);
+
/**
* Unescapes percent-encoded groups in a given string.
* E.g. "%20" will become " ". Unescaping is done in place.
diff --git a/ext/uri/uriparser/src/UriEscape.c b/ext/uri/uriparser/src/UriEscape.c
index a1763f97153c..17406bb05afc 100644
--- a/ext/uri/uriparser/src/UriEscape.c
+++ b/ext/uri/uriparser/src/UriEscape.c
@@ -65,14 +65,9 @@
# include "UriSets.h"
# endif
-URI_CHAR * URI_FUNC(Escape)(const URI_CHAR * in, URI_CHAR * out, UriBool spaceToPlus,
- UriBool normalizeBreaks) {
- return URI_FUNC(EscapeEx)(in, NULL, out, spaceToPlus, normalizeBreaks);
-}
-
-URI_CHAR * URI_FUNC(EscapeEx)(const URI_CHAR * inFirst, const URI_CHAR * inAfterLast,
- URI_CHAR * out, UriBool spaceToPlus,
- UriBool normalizeBreaks) {
+URI_CHAR * URI_FUNC(EscapeQueryEngine)(const URI_CHAR * inFirst, const URI_CHAR * inAfterLast,
+ URI_CHAR * out, UriBool spaceToPlus,
+ UriBool encodeReserved, UriBool normalizeBreaks) {
const URI_CHAR * read = inFirst;
URI_CHAR * write = out;
UriBool prevWasCr = URI_FALSE;
@@ -155,6 +150,26 @@ URI_CHAR * URI_FUNC(EscapeEx)(const URI_CHAR * inFirst, const URI_CHAR * inAfter
prevWasCr = URI_TRUE;
break;
+ case URI_SET_SUB_DELIMS(_UT):
+ /* intentional fallback */
+ case _UT(':'):
+ /* intentional fallback */
+ case _UT('@'):
+ /* intentional fallback */
+ case _UT('/'):
+ /* intentional fallback */
+ case _UT('?'):
+ if (!encodeReserved) {
+ /* Copy unmodified */
+ write[0] = read[0];
+ write++;
+
+ prevWasCr = URI_FALSE;
+ break;
+ }
+
+ /* intentional fallback */
+
default:
/* Percent encode */
{
@@ -175,6 +190,27 @@ URI_CHAR * URI_FUNC(EscapeEx)(const URI_CHAR * inFirst, const URI_CHAR * inAfter
}
}
+URI_CHAR * URI_FUNC(Escape)(const URI_CHAR * in, URI_CHAR * out, UriBool spaceToPlus,
+ UriBool normalizeBreaks) {
+ return URI_FUNC(EscapeEx)(in, NULL, out, spaceToPlus, normalizeBreaks);
+}
+
+URI_CHAR * URI_FUNC(EscapeEx)(const URI_CHAR * inFirst, const URI_CHAR * inAfterLast,
+ URI_CHAR * out, UriBool spaceToPlus,
+ UriBool normalizeBreaks) {
+ return URI_FUNC(EscapeQueryEngine)(inFirst, inAfterLast, out, spaceToPlus, URI_TRUE, normalizeBreaks);
+}
+
+URI_CHAR * URI_FUNC(EscapeQueryRfc3986)(const URI_CHAR * in, URI_CHAR * out,
+ UriBool normalizeBreaks) {
+ return URI_FUNC(EscapeQueryRfc3986Ex)(in, NULL, out, normalizeBreaks);
+}
+
+URI_CHAR * URI_FUNC(EscapeQueryRfc3986Ex)(const URI_CHAR * inFirst, const URI_CHAR * inAfterLast,
+ URI_CHAR * out, UriBool normalizeBreaks) {
+ return URI_FUNC(EscapeQueryEngine)(inFirst, inAfterLast, out, URI_FALSE, URI_FALSE, normalizeBreaks);
+}
+
const URI_CHAR * URI_FUNC(UnescapeInPlace)(URI_CHAR * inout) {
return URI_FUNC(UnescapeInPlaceEx)(inout, URI_FALSE, URI_BR_DONT_TOUCH);
}