diff --git a/NEWS b/NEWS
index 3029480c18b0..1569f2e064d5 100644
--- a/NEWS
+++ b/NEWS
@@ -17,6 +17,8 @@ PHP NEWS
2GB on some platforms, e.g. Windows. (Sjoerd Langkemper)
- DOM:
+ . Fixed a typo in the DOMException message for INUSE_ATTRIBUTE_ERR.
+ (Weilin Du)
. Fixed bug GH-22624 (use-after-free via DOMNameSpaceNode after
DOMDocument::xinclude()). (David Carlier)
. Fixed a use-after-free when cloning a DOMNameSpaceNode after
diff --git a/UPGRADING b/UPGRADING
index 350d30b12631..67024bad7e66 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -445,6 +445,8 @@ PHP 8.6 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/uri_followup#host_type_detection
. Added Uri\Rfc3986\UriBuilder.
RFC: https://wiki.php.net/rfc/uri_followup#uri_building
+ . Added Uri\WhatWg\UrlBuilder.
+ RFC: https://wiki.php.net/rfc/uri_followup#uri_building
========================================
3. Changes in SAPI modules
diff --git a/ext/dom/domexception.c b/ext/dom/domexception.c
index a7bea009dabc..7d9cc751e275 100644
--- a/ext/dom/domexception.c
+++ b/ext/dom/domexception.c
@@ -73,7 +73,7 @@ void php_dom_throw_error(dom_exception_code error_code, bool strict_error)
error_message = "Not Supported Error";
break;
case INUSE_ATTRIBUTE_ERR:
- error_message = "Inuse Attribute Error";
+ error_message = "In Use Attribute Error";
break;
case INVALID_STATE_ERR:
error_message = "Invalid State Error";
diff --git a/ext/dom/tests/modern/spec/Element_setAttributeNode_inuse.phpt b/ext/dom/tests/modern/spec/Element_setAttributeNode_inuse.phpt
index ddbfbde7e616..f6509c07a192 100644
--- a/ext/dom/tests/modern/spec/Element_setAttributeNode_inuse.phpt
+++ b/ext/dom/tests/modern/spec/Element_setAttributeNode_inuse.phpt
@@ -20,5 +20,5 @@ echo $dom1->saveHtml(), "\n";
?>
--EXPECT--
-Inuse Attribute Error
+In Use Attribute Error
diff --git a/ext/opcache/zend_shared_alloc.c b/ext/opcache/zend_shared_alloc.c
index 9a731d10bab2..3d6d2a840c33 100644
--- a/ext/opcache/zend_shared_alloc.c
+++ b/ext/opcache/zend_shared_alloc.c
@@ -17,15 +17,11 @@
+----------------------------------------------------------------------+
*/
-#ifdef HAVE_CONFIG_H
-# include
-#endif
-
-#if defined(__linux__) && defined(HAVE_MEMFD_CREATE)
-# ifndef _GNU_SOURCE
-# define _GNU_SOURCE
+#if defined(__linux__)
+# include
+# if defined(HAVE_MEMFD_CREATE)
+# include
# endif
-# include
#endif
#include
diff --git a/ext/snmp/snmp.c b/ext/snmp/snmp.c
index c45e3a6522b8..0c3e53614a6a 100644
--- a/ext/snmp/snmp.c
+++ b/ext/snmp/snmp.c
@@ -1061,20 +1061,48 @@ static ZEND_ATTRIBUTE_NONNULL bool snmp_session_set_sec_protocol(struct snmp_ses
s->securityPrivProtoLen = USM_PRIV_PROTO_AES_LEN;
return true;
}
+
+# ifdef NETSNMP_DRAFT_BLUMENTHAL_AES_04
+ if (zend_string_equals_literal_ci(prot, "AES192")) {
+ s->securityPrivProto = usmAES192PrivProtocol;
+ s->securityPrivProtoLen = OID_LENGTH(usmAES192PrivProtocol);
+ return true;
+ }
+
+ if (zend_string_equals_literal_ci(prot, "AES256")) {
+ s->securityPrivProto = usmAES256PrivProtocol;
+ s->securityPrivProtoLen = OID_LENGTH(usmAES256PrivProtocol);
+ return true;
+ }
+
+ if (zend_string_equals_literal_ci(prot, "AES192C")) {
+ s->securityPrivProto = usmAES192CiscoPrivProtocol;
+ s->securityPrivProtoLen = OID_LENGTH(usmAES192CiscoPrivProtocol);
+ return true;
+ }
+
+ if (zend_string_equals_literal_ci(prot, "AES256C")) {
+ s->securityPrivProto = usmAES256CiscoPrivProtocol;
+ s->securityPrivProtoLen = OID_LENGTH(usmAES256CiscoPrivProtocol);
+ return true;
+ }
+# endif
#endif
#ifdef HAVE_AES
-# ifndef NETSNMP_DISABLE_DES
- zend_value_error("Security protocol must be one of \"DES\", \"AES128\", or \"AES\"");
-# else
- zend_value_error("Security protocol must be one of \"AES128\", or \"AES\"");
+zend_value_error("Security protocol must be one of "
+# ifndef NETSNMP_DISABLE_DES
+ "\"DES\", "
+# endif
+# ifdef NETSNMP_DRAFT_BLUMENTHAL_AES_04
+ "\"AES256\", \"AES256C\", \"AES192\", \"AES192C\", "
# endif
-#else
-# ifndef NETSNMP_DISABLE_DES
+ "\"AES128\", or \"AES\""
+);
+#elif !defined(NETSNMP_DISABLE_DES)
zend_value_error("Security protocol must be \"DES\"");
-# else
+#else
zend_value_error("No security protocol supported");
-# endif
#endif
return false;
}
diff --git a/ext/snmp/tests/snmp-object-setSecurity_error.phpt b/ext/snmp/tests/snmp-object-setSecurity_error.phpt
index 576ec8380804..56565bf5855e 100644
--- a/ext/snmp/tests/snmp-object-setSecurity_error.phpt
+++ b/ext/snmp/tests/snmp-object-setSecurity_error.phpt
@@ -72,8 +72,8 @@ bool(false)
Warning: SNMP::setSecurity(): Error generating a key for authentication pass phrase 'te': Generic error (The supplied password length is too short.) in %s on line %d
bool(false)
-Security protocol must be one of "DES", "AES128", or "AES"
-Security protocol must be one of "DES", "AES128", or "AES"
+Security protocol must be one of %s
+Security protocol must be one of %s
Warning: SNMP::setSecurity(): Error generating a key for privacy pass phrase '': Generic error (The supplied password length is too short.) in %s on line %d
bool(false)
diff --git a/ext/snmp/tests/snmp3-error.phpt b/ext/snmp/tests/snmp3-error.phpt
index 5f39f88ffe14..8dd6c87b011e 100644
--- a/ext/snmp/tests/snmp3-error.phpt
+++ b/ext/snmp/tests/snmp3-error.phpt
@@ -69,7 +69,7 @@ bool(false)
Warning: snmp3_get(): Error generating a key for authentication pass phrase 'te': Generic error (The supplied password length is too short.) in %s on line %d
bool(false)
-Security protocol must be one of "DES", "AES128", or "AES"
+Security protocol must be one of %s "AES128", or "AES"
Warning: snmp3_get(): Error generating a key for privacy pass phrase '': Generic error (The supplied password length is too short.) in %s on line %d
bool(false)
diff --git a/ext/sockets/tests/socket_cmsg_space_num_range_64bit.phpt b/ext/sockets/tests/socket_cmsg_space_num_range_64bit.phpt
new file mode 100644
index 000000000000..7168d7e1029e
--- /dev/null
+++ b/ext/sockets/tests/socket_cmsg_space_num_range_64bit.phpt
@@ -0,0 +1,21 @@
+--TEST--
+socket_cmsg_space() rejects a $num larger than INT_MAX on 64-bit platforms
+--EXTENSIONS--
+sockets
+--SKIPIF--
+
+--FILE--
+getMessage(), "\n";
+}
+?>
+--EXPECT--
+ValueError: socket_cmsg_space(): Argument #3 ($num) must be between -2147483648 and 2147483647
diff --git a/ext/sockets/tests/socket_cmsg_space_return_type.phpt b/ext/sockets/tests/socket_cmsg_space_return_type.phpt
index f8b0b7364611..38a70666ed61 100644
--- a/ext/sockets/tests/socket_cmsg_space_return_type.phpt
+++ b/ext/sockets/tests/socket_cmsg_space_return_type.phpt
@@ -31,17 +31,8 @@ try {
echo $e::class, ': ', $e->getMessage(), "\n";
}
-// $num overflows int (64-bit only: PHP_INT_MAX > INT_MAX)
-if (PHP_INT_SIZE >= 8) {
- try {
- socket_cmsg_space(SOL_SOCKET, SCM_RIGHTS, PHP_INT_MAX);
- } catch (Throwable $e) {
- echo $e::class, ': ', $e->getMessage(), "\n";
- }
-}
?>
--EXPECT--
string(3) "int"
ValueError: Pair level 999999 and/or type 999999 is not supported
ValueError: socket_cmsg_space(): Argument #3 ($num) must be greater than or equal to 0
-ValueError: socket_cmsg_space(): Argument #3 ($num) must be between -2147483648 and 2147483647
diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c
index c774130f5237..4caeca9f1465 100644
--- a/ext/uri/php_uri.c
+++ b/ext/uri/php_uri.c
@@ -34,6 +34,7 @@ zend_class_entry *php_uri_ce_rfc3986_uri_builder;
zend_class_entry *php_uri_ce_rfc3986_uri;
zend_class_entry *php_uri_ce_rfc3986_uri_type;
zend_class_entry *php_uri_ce_rfc3986_uri_host_type;
+zend_class_entry *php_uri_ce_whatwg_url_builder;
zend_class_entry *php_uri_ce_whatwg_url;
zend_class_entry *php_uri_ce_comparison_mode;
zend_class_entry *php_uri_ce_exception;
@@ -75,6 +76,17 @@ static zend_always_inline zval *php_uri_deref(zval *zv)
#define Z_RFC3986_URI_PROP_QUERY_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 5))
#define Z_RFC3986_URI_PROP_FRAGMENT_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 6))
+#define Z_WHATWG_URL_PROP_SCHEME_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 0)
+#define Z_WHATWG_URL_PROP_SCHEME_DEREF_P(zv) php_uri_deref(Z_WHATWG_URL_PROP_SCHEME_P(zv))
+#define Z_WHATWG_URL_PROP_USERNAME_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 1))
+#define Z_WHATWG_URL_PROP_PASSWORD_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 2))
+#define Z_WHATWG_URL_PROP_HOST_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 3))
+#define Z_WHATWG_URL_PROP_PORT_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 4))
+#define Z_WHATWG_URL_PROP_PATH_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 5)
+#define Z_WHATWG_URL_PROP_PATH_DEREF_P(zv) php_uri_deref(Z_WHATWG_URL_PROP_PATH_P(zv))
+#define Z_WHATWG_URL_PROP_QUERY_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 6))
+#define Z_WHATWG_URL_PROP_FRAGMENT_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 7))
+
static HashTable *uri_get_debug_properties(php_uri_object *object)
{
const HashTable *std_properties = zend_std_get_properties(&object->std);
@@ -1244,6 +1256,139 @@ PHP_METHOD(Uri_Rfc3986_UriBuilder, build)
uri_object->uri = uriparser_uris;
}
+PHP_METHOD(Uri_WhatWg_UrlBuilder, reset)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ zend_object *object = Z_OBJ_P(ZEND_THIS);
+ zval *property = object->properties_table;
+ const zval *end = property + object->ce->default_properties_count;
+
+ while (property != end) {
+ zend_object_dtor_property(object, property);
+ ZVAL_NULL(property);
+ property++;
+ }
+
+ ZVAL_EMPTY_STRING(Z_WHATWG_URL_PROP_SCHEME_P(ZEND_THIS));
+ ZVAL_EMPTY_STRING(Z_WHATWG_URL_PROP_PATH_P(ZEND_THIS));
+
+ RETVAL_COPY(ZEND_THIS);
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setScheme)
+{
+ php_uri_builder_set_component_string(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("scheme"),
+ php_uri_parser_whatwg_validate_scheme
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setUsername)
+{
+ php_uri_builder_set_component_string_or_null(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("username"),
+ php_uri_parser_whatwg_validate_none
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setPassword)
+{
+ php_uri_builder_set_component_string_or_null(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("password"),
+ php_uri_parser_whatwg_validate_none
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setHost)
+{
+ php_uri_builder_set_component_string_or_null(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("host"),
+ php_uri_parser_whatwg_validate_host
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setPort)
+{
+ php_uri_builder_set_component_long_or_null(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("port"),
+ php_uri_parser_whatwg_validate_port
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setPath)
+{
+ php_uri_builder_set_component_string(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("path"),
+ php_uri_parser_whatwg_validate_none
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setQuery)
+{
+ php_uri_builder_set_component_string_or_null(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("query"),
+ php_uri_parser_whatwg_validate_none
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, setFragment)
+{
+ php_uri_builder_set_component_string_or_null(
+ INTERNAL_FUNCTION_PARAM_PASSTHRU,
+ ZEND_STRL("fragment"),
+ php_uri_parser_whatwg_validate_none
+ );
+}
+
+PHP_METHOD(Uri_WhatWg_UrlBuilder, build)
+{
+ zval *base_url_zv = NULL;
+ zval *errors = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(0, 2)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(base_url_zv, php_uri_ce_whatwg_url)
+ Z_PARAM_ZVAL(errors)
+ ZEND_PARSE_PARAMETERS_END();
+
+ const zval *scheme = Z_WHATWG_URL_PROP_SCHEME_DEREF_P(ZEND_THIS);
+ const zval *username = Z_WHATWG_URL_PROP_USERNAME_DEREF_P(ZEND_THIS);
+ const zval *password = Z_WHATWG_URL_PROP_PASSWORD_DEREF_P(ZEND_THIS);
+ const zval *host = Z_WHATWG_URL_PROP_HOST_DEREF_P(ZEND_THIS);
+ const zval *port = Z_WHATWG_URL_PROP_PORT_DEREF_P(ZEND_THIS);
+ const zval *path = Z_WHATWG_URL_PROP_PATH_DEREF_P(ZEND_THIS);
+ const zval *query = Z_WHATWG_URL_PROP_QUERY_DEREF_P(ZEND_THIS);
+ const zval *fragment = Z_WHATWG_URL_PROP_FRAGMENT_DEREF_P(ZEND_THIS);
+
+ lxb_url_t *base_url = NULL;
+ if (base_url_zv != NULL) {
+ zend_argument_error(NULL, 1, "is not supported yet, and therefore, null must be passed");
+ RETURN_THROWS();
+ base_url = Z_URI_OBJECT_P(base_url_zv)->uri;
+ }
+
+ lxb_url_t *lexbor_url = php_uri_parser_whatwg_build_from_zval(
+ base_url, scheme, username, password, host, port, path, query, fragment,
+ errors
+ );
+ if (lexbor_url == NULL) {
+ RETURN_THROWS();
+ }
+
+ object_init_ex(return_value, php_uri_ce_whatwg_url);
+ php_uri_object *uri_object = Z_URI_OBJECT_P(return_value);
+ uri_object->parser = &php_uri_parser_whatwg;
+ uri_object->uri = lexbor_url;
+}
+
PHPAPI php_uri_object *php_uri_object_create(zend_class_entry *class_type, const php_uri_parser *parser)
{
php_uri_object *uri_object = zend_object_alloc(sizeof(*uri_object), class_type);
@@ -1326,6 +1471,8 @@ static PHP_MINIT_FUNCTION(uri)
php_uri_ce_rfc3986_uri_type = register_class_Uri_Rfc3986_UriType();
php_uri_ce_rfc3986_uri_host_type = register_class_Uri_Rfc3986_UriHostType();
+ php_uri_ce_whatwg_url_builder = register_class_Uri_WhatWg_UrlBuilder();
+
php_uri_ce_whatwg_url = register_class_Uri_WhatWg_Url();
php_uri_ce_whatwg_url->create_object = php_uri_object_create_whatwg;
php_uri_ce_whatwg_url->default_object_handlers = &object_handlers_whatwg_uri;
diff --git a/ext/uri/php_uri.stub.php b/ext/uri/php_uri.stub.php
index 81a766c0e16d..98f8873d9845 100644
--- a/ext/uri/php_uri.stub.php
+++ b/ext/uri/php_uri.stub.php
@@ -211,6 +211,39 @@ enum UrlHostType
case Empty;
}
+ final class UrlBuilder
+ {
+ private string $scheme = "";
+ private ?string $username = null;
+ private ?string $password = null;
+ private ?string $host = null;
+ private ?int $port = null;
+ private string $path = "";
+ private ?string $query = null;
+ private ?string $fragment = null;
+
+ public function reset(): static {}
+
+ public function setScheme(string $scheme): static {}
+
+ public function setUsername(?string $username): static {}
+
+ public function setPassword(#[\SensitiveParameter] ?string $password): static {}
+
+ public function setHost(?string $host): static {}
+
+ public function setPort(?int $port): static {}
+
+ public function setPath(string $path): static {}
+
+ public function setQuery(?string $query): static {}
+
+ public function setFragment(?string $fragment): static {}
+
+ /** @param array $errors */
+ public function build(?\Uri\WhatWg\Url $baseUrl = null, &$errors = null): \Uri\WhatWg\Url {}
+ }
+
/** @strict-properties */
final readonly class Url
{
diff --git a/ext/uri/php_uri_arginfo.h b/ext/uri/php_uri_arginfo.h
index 1231408fbeb6..93cc3ee45a2b 100644
--- a/ext/uri/php_uri_arginfo.h
+++ b/ext/uri/php_uri_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit php_uri.stub.php instead.
- * Stub hash: bfd9247fa79baf877d600134c9ef615ec5ca9cea
+ * Stub hash: 54e953b1da0d08c64509666b9278c59483d1e171
* Has decl header: yes */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_Rfc3986_UriBuilder_reset, 0, 0, IS_STATIC, 0)
@@ -141,6 +141,35 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Uri_WhatWg_UrlValidationError___construct,
ZEND_ARG_TYPE_INFO(0, failure, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
+#define arginfo_class_Uri_WhatWg_UrlBuilder_reset arginfo_class_Uri_Rfc3986_UriBuilder_reset
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_UrlBuilder_setScheme, 0, 1, IS_STATIC, 0)
+ ZEND_ARG_TYPE_INFO(0, scheme, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_UrlBuilder_setUsername, 0, 1, IS_STATIC, 0)
+ ZEND_ARG_TYPE_INFO(0, username, IS_STRING, 1)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_UrlBuilder_setPassword, 0, 1, IS_STATIC, 0)
+ ZEND_ARG_TYPE_INFO(0, password, IS_STRING, 1)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Uri_WhatWg_UrlBuilder_setHost arginfo_class_Uri_Rfc3986_UriBuilder_setHost
+
+#define arginfo_class_Uri_WhatWg_UrlBuilder_setPort arginfo_class_Uri_Rfc3986_UriBuilder_setPort
+
+#define arginfo_class_Uri_WhatWg_UrlBuilder_setPath arginfo_class_Uri_Rfc3986_UriBuilder_setPath
+
+#define arginfo_class_Uri_WhatWg_UrlBuilder_setQuery arginfo_class_Uri_Rfc3986_UriBuilder_setQuery
+
+#define arginfo_class_Uri_WhatWg_UrlBuilder_setFragment arginfo_class_Uri_Rfc3986_UriBuilder_setFragment
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Uri_WhatWg_UrlBuilder_build, 0, 0, Uri\\WhatWg\\\125rl, 0)
+ ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, baseUrl, Uri\\WhatWg\\\125rl, 1, "null")
+ ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, errors, "null")
+ZEND_END_ARG_INFO()
+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_Url_parse, 0, 1, IS_STATIC, 1)
ZEND_ARG_TYPE_INFO(0, uri, IS_STRING, 0)
ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, baseUrl, Uri\\WhatWg\\\125rl, 1, "null")
@@ -155,24 +184,18 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Uri_WhatWg_Url_getScheme arginfo_class_Uri_Rfc3986_Uri_getPath
-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)
-ZEND_END_ARG_INFO()
+#define arginfo_class_Uri_WhatWg_Url_withScheme arginfo_class_Uri_WhatWg_UrlBuilder_setScheme
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_Url_isSpecialScheme, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
#define arginfo_class_Uri_WhatWg_Url_getUsername arginfo_class_Uri_Rfc3986_Uri_getScheme
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_Url_withUsername, 0, 1, IS_STATIC, 0)
- ZEND_ARG_TYPE_INFO(0, username, IS_STRING, 1)
-ZEND_END_ARG_INFO()
+#define arginfo_class_Uri_WhatWg_Url_withUsername arginfo_class_Uri_WhatWg_UrlBuilder_setUsername
#define arginfo_class_Uri_WhatWg_Url_getPassword arginfo_class_Uri_Rfc3986_Uri_getScheme
-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Uri_WhatWg_Url_withPassword, 0, 1, IS_STATIC, 0)
- ZEND_ARG_TYPE_INFO(0, password, IS_STRING, 1)
-ZEND_END_ARG_INFO()
+#define arginfo_class_Uri_WhatWg_Url_withPassword arginfo_class_Uri_WhatWg_UrlBuilder_setPassword
#define arginfo_class_Uri_WhatWg_Url_getAsciiHost arginfo_class_Uri_Rfc3986_Uri_getScheme
@@ -265,6 +288,16 @@ ZEND_METHOD(Uri_Rfc3986_Uri, __unserialize);
ZEND_METHOD(Uri_Rfc3986_Uri, __debugInfo);
ZEND_METHOD(Uri_WhatWg_InvalidUrlException, __construct);
ZEND_METHOD(Uri_WhatWg_UrlValidationError, __construct);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, reset);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setScheme);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setUsername);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setPassword);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setHost);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setPort);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setPath);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setQuery);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, setFragment);
+ZEND_METHOD(Uri_WhatWg_UrlBuilder, build);
ZEND_METHOD(Uri_WhatWg_Url, parse);
ZEND_METHOD(Uri_WhatWg_Url, __construct);
ZEND_METHOD(Uri_WhatWg_Url, getScheme);
@@ -345,6 +378,20 @@ static const zend_function_entry class_Uri_WhatWg_UrlValidationError_methods[] =
ZEND_FE_END
};
+static const zend_function_entry class_Uri_WhatWg_UrlBuilder_methods[] = {
+ ZEND_ME(Uri_WhatWg_UrlBuilder, reset, arginfo_class_Uri_WhatWg_UrlBuilder_reset, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setScheme, arginfo_class_Uri_WhatWg_UrlBuilder_setScheme, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setUsername, arginfo_class_Uri_WhatWg_UrlBuilder_setUsername, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setPassword, arginfo_class_Uri_WhatWg_UrlBuilder_setPassword, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setHost, arginfo_class_Uri_WhatWg_UrlBuilder_setHost, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setPort, arginfo_class_Uri_WhatWg_UrlBuilder_setPort, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setPath, arginfo_class_Uri_WhatWg_UrlBuilder_setPath, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setQuery, arginfo_class_Uri_WhatWg_UrlBuilder_setQuery, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, setFragment, arginfo_class_Uri_WhatWg_UrlBuilder_setFragment, ZEND_ACC_PUBLIC)
+ ZEND_ME(Uri_WhatWg_UrlBuilder, build, arginfo_class_Uri_WhatWg_UrlBuilder_build, ZEND_ACC_PUBLIC)
+ ZEND_FE_END
+};
+
static const zend_function_entry class_Uri_WhatWg_Url_methods[] = {
ZEND_ME(Uri_WhatWg_Url, parse, arginfo_class_Uri_WhatWg_Url_parse, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
ZEND_ME(Uri_WhatWg_Url, __construct, arginfo_class_Uri_WhatWg_Url___construct, ZEND_ACC_PUBLIC)
@@ -629,6 +676,51 @@ static zend_class_entry *register_class_Uri_WhatWg_UrlHostType(void)
return class_entry;
}
+static zend_class_entry *register_class_Uri_WhatWg_UrlBuilder(void)
+{
+ zend_class_entry ce, *class_entry;
+
+ INIT_NS_CLASS_ENTRY(ce, "Uri\\WhatWg", "UrlBuilder", class_Uri_WhatWg_UrlBuilder_methods);
+ class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL);
+
+ zval property_scheme_default_value;
+ ZVAL_EMPTY_STRING(&property_scheme_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_SCHEME), &property_scheme_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
+
+ zval property_username_default_value;
+ ZVAL_NULL(&property_username_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_USERNAME), &property_username_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL));
+
+ zval property_password_default_value;
+ ZVAL_NULL(&property_password_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_PASSWORD), &property_password_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL));
+
+ zval property_host_default_value;
+ ZVAL_NULL(&property_host_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_HOST), &property_host_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL));
+
+ zval property_port_default_value;
+ ZVAL_NULL(&property_port_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_PORT), &property_port_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG|MAY_BE_NULL));
+
+ zval property_path_default_value;
+ ZVAL_EMPTY_STRING(&property_path_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_PATH), &property_path_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
+
+ zval property_query_default_value;
+ ZVAL_NULL(&property_query_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_QUERY), &property_query_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL));
+
+ zval property_fragment_default_value;
+ ZVAL_NULL(&property_fragment_default_value);
+ zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_FRAGMENT), &property_fragment_default_value, ZEND_ACC_PRIVATE, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL));
+
+
+ zend_add_parameter_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "setpassword", sizeof("setpassword") - 1), 0, ZSTR_KNOWN(ZEND_STR_SENSITIVEPARAMETER), 0);
+
+ return class_entry;
+}
+
static zend_class_entry *register_class_Uri_WhatWg_Url(void)
{
zend_class_entry ce, *class_entry;
diff --git a/ext/uri/php_uri_common.c b/ext/uri/php_uri_common.c
index 0644afda3105..e4850d8059ee 100644
--- a/ext/uri/php_uri_common.c
+++ b/ext/uri/php_uri_common.c
@@ -40,6 +40,31 @@ static zend_string *get_known_string_by_property_name(php_uri_property_name prop
}
}
+zend_result php_uri_pass_errors_by_ref_and_free(zval *errors_zv, zval *errors)
+{
+ ZEND_ASSERT(Z_TYPE_P(errors) == IS_UNDEF || Z_TYPE_P(errors) == IS_ARRAY);
+
+ /* There was no error during parsing */
+ if (Z_ISUNDEF_P(errors)) {
+ return SUCCESS;
+ }
+
+ /* The errors parameter is an array, but the pass-by ref argument stored by
+ * errors_zv was not passed - the URI implementation either doesn't support
+ * returning additional error information, or the caller is not interested in it */
+ if (errors_zv == NULL) {
+ zval_ptr_dtor(errors);
+ return SUCCESS;
+ }
+
+ ZEND_TRY_ASSIGN_REF_TMP(errors_zv, errors);
+ if (EG(exception)) {
+ return FAILURE;
+ }
+
+ return SUCCESS;
+}
+
void php_uri_property_read_helper(INTERNAL_FUNCTION_PARAMETERS, php_uri_property_name property_name, php_uri_component_read_mode component_read_mode)
{
ZEND_PARSE_PARAMETERS_NONE();
diff --git a/ext/uri/php_uri_common.h b/ext/uri/php_uri_common.h
index 9106f6acd15f..31ef1dd2130c 100644
--- a/ext/uri/php_uri_common.h
+++ b/ext/uri/php_uri_common.h
@@ -21,6 +21,7 @@ extern zend_class_entry *php_uri_ce_rfc3986_uri_builder;
extern zend_class_entry *php_uri_ce_rfc3986_uri;
extern zend_class_entry *php_uri_ce_rfc3986_uri_type;
extern zend_class_entry *php_uri_ce_rfc3986_uri_host_type;
+extern zend_class_entry *php_uri_ce_whatwg_url_builder;
extern zend_class_entry *php_uri_ce_whatwg_url;
extern zend_class_entry *php_uri_ce_comparison_mode;
extern zend_class_entry *php_uri_ce_exception;
@@ -186,6 +187,7 @@ static inline const php_uri_property_handler *php_uri_parser_property_handler_by
}
}
+zend_result php_uri_pass_errors_by_ref_and_free(zval *errors_zv, zval *errors);
void php_uri_property_read_helper(INTERNAL_FUNCTION_PARAMETERS, php_uri_property_name property_name, php_uri_component_read_mode component_read_mode);
void php_uri_property_write_str_helper(INTERNAL_FUNCTION_PARAMETERS, php_uri_property_name property_name);
void php_uri_property_write_str_or_null_helper(INTERNAL_FUNCTION_PARAMETERS, php_uri_property_name property_name);
diff --git a/ext/uri/php_uri_decl.h b/ext/uri/php_uri_decl.h
index 9c817659cf76..71f748b71070 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: bfd9247fa79baf877d600134c9ef615ec5ca9cea */
+ * Stub hash: 54e953b1da0d08c64509666b9278c59483d1e171 */
-#ifndef ZEND_PHP_URI_DECL_bfd9247fa79baf877d600134c9ef615ec5ca9cea_H
-#define ZEND_PHP_URI_DECL_bfd9247fa79baf877d600134c9ef615ec5ca9cea_H
+#ifndef ZEND_PHP_URI_DECL_54e953b1da0d08c64509666b9278c59483d1e171_H
+#define ZEND_PHP_URI_DECL_54e953b1da0d08c64509666b9278c59483d1e171_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_bfd9247fa79baf877d600134c9ef615ec5ca9cea_H */
+#endif /* ZEND_PHP_URI_DECL_54e953b1da0d08c64509666b9278c59483d1e171_H */
diff --git a/ext/uri/tests/rfc3986/builder/host_error_ipv6_closing_brace.phpt b/ext/uri/tests/rfc3986/builder/host_error_ipv6_closing_brace.phpt
index 45eef13faf71..feda6170b38e 100644
--- a/ext/uri/tests/rfc3986/builder/host_error_ipv6_closing_brace.phpt
+++ b/ext/uri/tests/rfc3986/builder/host_error_ipv6_closing_brace.phpt
@@ -6,7 +6,7 @@ Test Uri\Rfc3986\UriBuilder::setHost() - error - missing IPv6 closing brace
$builder = new Uri\Rfc3986\UriBuilder();
try {
- $builder->setHost("[2001:%30db8:85a3:0000:0000:8a2e:0370:7334");
+ $builder->setHost("[2001:db8:85a3:0000:0000:8a2e:0370:7334");
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
diff --git a/ext/uri/tests/whatwg/builder/all_success_with_reset.phpt b/ext/uri/tests/whatwg/builder/all_success_with_reset.phpt
new file mode 100644
index 000000000000..224f07b7ef91
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/all_success_with_reset.phpt
@@ -0,0 +1,72 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder all components - success - calling reset() afterwards
+--FILE--
+setScheme("https")
+ ->setUsername("user")
+ ->setPassword("pass")
+ ->setHost("example.com")
+ ->setPort(444)
+ ->setPath("/foo/bar/baz")
+ ->setQuery("foo=1&bar=baz")
+ ->setFragment("fragment");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+$url = $builder
+ ->reset()
+ ->setScheme("scheme")
+ ->setHost("www.example.com")
+ ->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(68) "https://user:pass@example.com:444/foo/bar/baz?foo=1&bar=baz#fragment"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ string(4) "user"
+ ["password"]=>
+ string(4) "pass"
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ int(444)
+ ["path"]=>
+ string(12) "/foo/bar/baz"
+ ["query"]=>
+ string(13) "foo=1&bar=baz"
+ ["fragment"]=>
+ string(8) "fragment"
+}
+bool(true)
+string(24) "scheme://www.example.com"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(6) "scheme"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(15) "www.example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(0) ""
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/basic_error_with_base.phpt b/ext/uri/tests/whatwg/builder/basic_error_with_base.phpt
new file mode 100644
index 000000000000..6f65cdacdb4a
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/basic_error_with_base.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder basic - error - with base URL
+--FILE--
+setPath("/foo/bar/baz");
+
+try {
+ $builder->build(new Uri\WhatWg\Url("/foo/bar"));
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URI is malformed (MissingSchemeNonRelativeUrl)
diff --git a/ext/uri/tests/whatwg/builder/basic_success_with_base.phpt b/ext/uri/tests/whatwg/builder/basic_success_with_base.phpt
new file mode 100644
index 000000000000..1fc354581db6
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/basic_success_with_base.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder basic - success - with base URL
+--XFAIL--
+Support for passing $baseUrl to Uri\WhatWg\UrlBuilder::build() is not implemented yet.
+--FILE--
+setPath("/foo/bar/baz");
+$url = $builder->build(new Uri\WhatWg\Url("https://example.com"));
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(31) "https://example.com/foo/bar/baz"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(12) "/foo/bar/baz"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_hashmark.phpt b/ext/uri/tests/whatwg/builder/fragment_success_hashmark.phpt
new file mode 100644
index 000000000000..430ba470ba79
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/fragment_success_hashmark.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setFragment() - success - leading hashmark is ignored
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("#foo");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(24) "https://example.com/#foo"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ string(3) "foo"
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt b/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
new file mode 100644
index 000000000000..f4940291f75d
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/fragment_success_ignorable_char.phpt
@@ -0,0 +1,51 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setFragment() - success - contains tab and newline characters
+--FILE--
+setScheme("foo");
+$builder->setHost("example.com");
+$builder->setFragment("\tfo\no");
+$errors = [];
+$url = $builder->build(errors: $errors);
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($errors);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(21) "foo://example.com#foo"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(3) "foo"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(0) ""
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ string(3) "foo"
+}
+array(%d) {
+ [0]=>
+ object(Uri\WhatWg\UrlValidationError)#%d (%d) {
+ ["context"]=>
+ string(5) " fo
+o"
+ ["type"]=>
+ enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit)
+ ["failure"]=>
+ bool(false)
+ }
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_null.phpt b/ext/uri/tests/whatwg/builder/fragment_success_null.phpt
new file mode 100644
index 000000000000..bff74443bbb9
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/fragment_success_null.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setFragment() - success - null
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("foo");
+$builder->setFragment(null);
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_special_char.phpt b/ext/uri/tests/whatwg/builder/fragment_success_special_char.phpt
new file mode 100644
index 000000000000..d153274de0fc
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/fragment_success_special_char.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setFragment() - success - contains special character
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment(" ");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(33) "https://example.com/#%20%3Cfoo%3E"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ string(12) "%20%3Cfoo%3E"
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/fragment_success_unicode_char.phpt b/ext/uri/tests/whatwg/builder/fragment_success_unicode_char.phpt
new file mode 100644
index 000000000000..0153432eedb4
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/fragment_success_unicode_char.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setFragment() - success - contains Unicode character
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setFragment("főő");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(34) "https://example.com/#f%C5%91%C5%91"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ string(13) "f%C5%91%C5%91"
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/host_error_ipv6_closing_brace_opaque.phpt b/ext/uri/tests/whatwg/builder/host_error_ipv6_closing_brace_opaque.phpt
new file mode 100644
index 000000000000..7f3fbb35da63
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_error_ipv6_closing_brace_opaque.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - error - missing IPv6 closing brace of an opaque host
+--FILE--
+setScheme("scheme");
+
+try {
+ $builder->setHost("[2001:db8:85a3:0000:0000:8a2e:0370:7334");
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (Ipv6Unclosed)
diff --git a/ext/uri/tests/whatwg/builder/host_error_ipv6_closing_brace_special.phpt b/ext/uri/tests/whatwg/builder/host_error_ipv6_closing_brace_special.phpt
new file mode 100644
index 000000000000..94ba827ca325
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_error_ipv6_closing_brace_special.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - error - missing IPv6 closing brace of a special host
+--FILE--
+setScheme("https");
+
+try {
+ $builder->setHost("[2001:%30db8:85a3:0000:0000:8a2e:0370:7334");
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (Ipv6Unclosed)
diff --git a/ext/uri/tests/whatwg/builder/host_error_null_special.phpt b/ext/uri/tests/whatwg/builder/host_error_null_special.phpt
new file mode 100644
index 000000000000..973f4ef9aca3
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_error_null_special.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - null in case of special URLs
+--FILE--
+setScheme("https");
+$builder->setHost(null);
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (HostMissing)
diff --git a/ext/uri/tests/whatwg/builder/host_error_percent_encoding1.phpt b/ext/uri/tests/whatwg/builder/host_error_percent_encoding1.phpt
new file mode 100644
index 000000000000..2dc8c76189fd
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_error_percent_encoding1.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - error - invalid percent encoding octet in registered name
+--FILE--
+setScheme("https");
+$builder->setHost("ex%3mple.co");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (DomainInvalidCodePoint)
diff --git a/ext/uri/tests/whatwg/builder/host_error_percent_encoding2.phpt b/ext/uri/tests/whatwg/builder/host_error_percent_encoding2.phpt
new file mode 100644
index 000000000000..90a228ded22b
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_error_percent_encoding2.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - error - invalid percent encoded octet in IPv6
+--FILE--
+setScheme("https");
+
+try {
+ $builder->setHost("[2001:%308:85a3:0000:0000:8a2e:0370:7334]");
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (Ipv6InvalidCodePoint)
diff --git a/ext/uri/tests/whatwg/builder/host_error_percent_encoding3.phpt b/ext/uri/tests/whatwg/builder/host_error_percent_encoding3.phpt
new file mode 100644
index 000000000000..aee9f95d1f44
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_error_percent_encoding3.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - error - invalid percent encoded octet in IPv4
+--FILE--
+setScheme("https");
+$builder->setHost("192.168.%8.1");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified host is malformed (DomainInvalidCodePoint)
diff --git a/ext/uri/tests/whatwg/builder/host_success_empty_opaque.phpt b/ext/uri/tests/whatwg/builder/host_success_empty_opaque.phpt
new file mode 100644
index 000000000000..e9668f466c43
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_success_empty_opaque.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - empty in case of opaque URLs
+--FILE--
+setScheme("scheme");
+$builder->setHost("");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(9) "scheme://"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(6) "scheme"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(0) ""
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/host_success_file.phpt b/ext/uri/tests/whatwg/builder/host_success_file.phpt
new file mode 100644
index 000000000000..2264c2faffbe
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_success_file.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - file scheme
+--FILE--
+setScheme("file");
+$builder->setHost("example.com");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(19) "file://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(4) "file"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
+
diff --git a/ext/uri/tests/whatwg/builder/host_success_ipv4.phpt b/ext/uri/tests/whatwg/builder/host_success_ipv4.phpt
new file mode 100644
index 000000000000..2dad23e1f214
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_success_ipv4.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - IPv4 address
+--FILE--
+setScheme("https");
+$builder->setHost("192.168.0.1");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://192.168.0.1/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "192.168.0.1"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/host_success_ipv4_percent_encoding.phpt b/ext/uri/tests/whatwg/builder/host_success_ipv4_percent_encoding.phpt
new file mode 100644
index 000000000000..955796860d66
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_success_ipv4_percent_encoding.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - percent-encoding in IPv4 address
+--FILE--
+setScheme("https");
+$builder->setHost("192.168.%30.1");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://192.168.0.1/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "192.168.0.1"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/host_success_ipv6.phpt b/ext/uri/tests/whatwg/builder/host_success_ipv6.phpt
new file mode 100644
index 000000000000..48e37d22a8ce
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_success_ipv6.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - IPv6 address
+--FILE--
+setScheme("https");
+$builder->setHost("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(39) "https://[2001:db8:85a3::8a2e:370:7334]/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(30) "[2001:db8:85a3::8a2e:370:7334]"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/host_success_opaque_null.phpt b/ext/uri/tests/whatwg/builder/host_success_opaque_null.phpt
new file mode 100644
index 000000000000..9921721af17c
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_success_opaque_null.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - null in case of opaque hosts
+--FILE--
+setScheme("scheme");
+$builder->setHost("example.com");
+$builder->setHost(null);
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(9) "scheme://"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(6) "scheme"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(0) ""
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/host_success_regname.phpt b/ext/uri/tests/whatwg/builder/host_success_regname.phpt
new file mode 100644
index 000000000000..d72b919c0afb
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/host_success_regname.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setHost() - success - Registered name
+--FILE--
+setScheme("https");
+$builder->setHost("www.example.com");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(24) "https://www.example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(15) "www.example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/password_error_empty_opaque_host.phpt b/ext/uri/tests/whatwg/builder/password_error_empty_opaque_host.phpt
new file mode 100644
index 000000000000..5c79856b93c9
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/password_error_empty_opaque_host.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPassword() - error - empty opaque host
+--FILE--
+setScheme("scheme");
+$builder->setHost("");
+$builder->setPassword("password");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have password
diff --git a/ext/uri/tests/whatwg/builder/password_error_file_scheme.phpt b/ext/uri/tests/whatwg/builder/password_error_file_scheme.phpt
new file mode 100644
index 000000000000..e6f4f993c4d2
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/password_error_file_scheme.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPassword() - error - file scheme
+--FILE--
+setScheme("file");
+$builder->setPath("C:/a.txt");
+$builder->setPassword("password");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have password
diff --git a/ext/uri/tests/whatwg/builder/password_error_missing_host.phpt b/ext/uri/tests/whatwg/builder/password_error_missing_host.phpt
new file mode 100644
index 000000000000..9b3ba0876336
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/password_error_missing_host.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPassword() - error - missing host
+--FILE--
+setScheme("https");
+$builder->setPassword("pass");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have password
diff --git a/ext/uri/tests/whatwg/builder/password_error_missing_opaque_host.phpt b/ext/uri/tests/whatwg/builder/password_error_missing_opaque_host.phpt
new file mode 100644
index 000000000000..efc14b06a413
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/password_error_missing_opaque_host.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPassword() - error - missing opaque host
+--FILE--
+setScheme("scheme");
+$builder->setPassword("password");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have password
diff --git a/ext/uri/tests/whatwg/builder/password_success_null.phpt b/ext/uri/tests/whatwg/builder/password_success_null.phpt
new file mode 100644
index 000000000000..842b85807d79
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/password_success_null.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPassword() - success - null
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setPassword("pass");
+$builder->setPassword(null);
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/path_success_empty_string.phpt b/ext/uri/tests/whatwg/builder/path_success_empty_string.phpt
new file mode 100644
index 000000000000..d3a4509cbf63
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/path_success_empty_string.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPath() - success - empty string
+--FILE--
+setScheme("file");
+$builder->setPath("/foo/bar");
+$builder->setPath("");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(8) "file:///"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(4) "file"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/path_success_first_segment_colon.phpt b/ext/uri/tests/whatwg/builder/path_success_first_segment_colon.phpt
new file mode 100644
index 000000000000..544dcc0e346f
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/path_success_first_segment_colon.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPath() - success - contains a colon in the first segment when the scheme is present
+--FILE--
+setScheme("file");
+$builder->setPath(":foo/bar/baz");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "file:///:foo/bar/baz"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(4) "file"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(13) "/:foo/bar/baz"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/path_success_leading_double_slash1.phpt b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash1.phpt
new file mode 100644
index 000000000000..b979e704550c
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash1.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPath() - success - begins with double slashes when the host is not present
+--FILE--
+setScheme("file");
+$builder->setPath("//foo/bar/baz");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "file:////foo/bar/baz"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(4) "file"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(13) "//foo/bar/baz"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/path_success_leading_double_slash2.phpt b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash2.phpt
new file mode 100644
index 000000000000..cc090409a39d
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/path_success_leading_double_slash2.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPath() - success - begins with double slashes when the opaque URL contains a host
+--FILE--
+setScheme("scheme");
+$builder->setHost("example.com");
+$builder->setPath("//foo/bar/baz");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(33) "scheme://example.com//foo/bar/baz"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(6) "scheme"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(13) "//foo/bar/baz"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/path_success_special_char.phpt b/ext/uri/tests/whatwg/builder/path_success_special_char.phpt
new file mode 100644
index 000000000000..b47a44b1e133
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/path_success_special_char.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPath() - success - contains special character
+--FILE--
+setScheme("scheme");
+$builder->setPath("#foo");
+
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(16) "scheme:///%23foo"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(6) "scheme"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(7) "/%23foo"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/path_success_without_leading_slash.phpt b/ext/uri/tests/whatwg/builder/path_success_without_leading_slash.phpt
new file mode 100644
index 000000000000..1f66344ae64d
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/path_success_without_leading_slash.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPath() - success - without a leading slash when the URL contains a host
+--FILE--
+setScheme("https");
+$builder->setPath("foo/bar/baz");
+$builder->setHost("example.com");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(31) "https://example.com/foo/bar/baz"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(12) "/foo/bar/baz"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/port_error_empty_opaque_host.phpt b/ext/uri/tests/whatwg/builder/port_error_empty_opaque_host.phpt
new file mode 100644
index 000000000000..acf82a1a12cb
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_error_empty_opaque_host.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - error - empty opaque host
+--FILE--
+setScheme("scheme");
+$builder->setHost("");
+$builder->setPort(443);
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have port
diff --git a/ext/uri/tests/whatwg/builder/port_error_file_scheme.phpt b/ext/uri/tests/whatwg/builder/port_error_file_scheme.phpt
new file mode 100644
index 000000000000..08559dbfd208
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_error_file_scheme.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - error - file scheme
+--FILE--
+setScheme("file");
+$builder->setPath("C:/a.txt");
+$builder->setPort(443);
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have port
diff --git a/ext/uri/tests/whatwg/builder/port_error_file_with_host_and_port.phpt b/ext/uri/tests/whatwg/builder/port_error_file_with_host_and_port.phpt
new file mode 100644
index 000000000000..9e4e17ca3ae3
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_error_file_with_host_and_port.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - error - file scheme with host and port
+--FILE--
+setScheme("file");
+$builder->setHost("example.com");
+$builder->setPort(123);
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have port
diff --git a/ext/uri/tests/whatwg/builder/port_error_large.phpt b/ext/uri/tests/whatwg/builder/port_error_large.phpt
new file mode 100644
index 000000000000..46e80e1689b7
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_error_large.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - error - too large number
+--FILE--
+setPort(PHP_INT_MAX);
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified port is malformed (PortOutOfRange)
diff --git a/ext/uri/tests/whatwg/builder/port_error_missing_opaque_host.phpt b/ext/uri/tests/whatwg/builder/port_error_missing_opaque_host.phpt
new file mode 100644
index 000000000000..3130b27cfcb7
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_error_missing_opaque_host.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - error - missing opaque host
+--FILE--
+setScheme("scheme");
+$builder->setPort(443);
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have port
diff --git a/ext/uri/tests/whatwg/builder/port_error_negative.phpt b/ext/uri/tests/whatwg/builder/port_error_negative.phpt
new file mode 100644
index 000000000000..a1a723f43ca5
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_error_negative.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - error - negative number
+--FILE--
+setPort(-1);
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified port is malformed (PortInvalid)
diff --git a/ext/uri/tests/whatwg/builder/port_success_default.phpt b/ext/uri/tests/whatwg/builder/port_success_default.phpt
new file mode 100644
index 000000000000..cb4f55c609ca
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_success_default.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - success - default port
+--FILE--
+setScheme("https");
+$builder->setPort(443);
+$builder->setHost("example.com");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/port_success_non_default.phpt b/ext/uri/tests/whatwg/builder/port_success_non_default.phpt
new file mode 100644
index 000000000000..919e9eb6983c
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_success_non_default.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - success - non-default port
+--FILE--
+setScheme("https");
+$builder->setPort(444);
+$builder->setHost("example.com");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(24) "https://example.com:444/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ int(444)
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/port_success_null.phpt b/ext/uri/tests/whatwg/builder/port_success_null.phpt
new file mode 100644
index 000000000000..2c097112092f
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/port_success_null.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setPort() - success - null
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setPort(123);
+$builder->setPort(null);
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/query_success_basic.phpt b/ext/uri/tests/whatwg/builder/query_success_basic.phpt
new file mode 100644
index 000000000000..e3257c5d0cef
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/query_success_basic.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setQuery() - success - basic
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setQuery("foo=1&bar=baz");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(34) "https://example.com/?foo=1&bar=baz"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ string(13) "foo=1&bar=baz"
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/query_success_null.phpt b/ext/uri/tests/whatwg/builder/query_success_null.phpt
new file mode 100644
index 000000000000..a634a5403752
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/query_success_null.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setQuery() - success - null
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setQuery("foo");
+$builder->setQuery(null);
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/query_success_question_mark.phpt b/ext/uri/tests/whatwg/builder/query_success_question_mark.phpt
new file mode 100644
index 000000000000..b88a4aacb627
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/query_success_question_mark.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setQuery() - success - leading question mark is ignored
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setQuery("?foo");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(24) "https://example.com/?foo"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ string(3) "foo"
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/query_success_unicode_char.phpt b/ext/uri/tests/whatwg/builder/query_success_unicode_char.phpt
new file mode 100644
index 000000000000..9874fb9a47df
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/query_success_unicode_char.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setQuery() - success - contains Unicode character
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setQuery("főő");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(34) "https://example.com/?f%C5%91%C5%91"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ string(13) "f%C5%91%C5%91"
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/scheme_error_c0_control_space_char.phpt b/ext/uri/tests/whatwg/builder/scheme_error_c0_control_space_char.phpt
new file mode 100644
index 000000000000..285e91132469
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/scheme_error_c0_control_space_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setScheme() - error - contains leading and trailing C0 control and space characters
+--FILE--
+setScheme(" \x01https \x02");
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified scheme is malformed (MissingSchemeNonRelativeUrl)
diff --git a/ext/uri/tests/whatwg/builder/scheme_error_empty.phpt b/ext/uri/tests/whatwg/builder/scheme_error_empty.phpt
new file mode 100644
index 000000000000..f4090a9c4aac
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/scheme_error_empty.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setScheme() - error - empty
+--FILE--
+setScheme("");
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified scheme is malformed (MissingSchemeNonRelativeUrl)
diff --git a/ext/uri/tests/whatwg/builder/scheme_error_first_char.phpt b/ext/uri/tests/whatwg/builder/scheme_error_first_char.phpt
new file mode 100644
index 000000000000..34a70c043aa3
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/scheme_error_first_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setScheme() - error - first character is not alpha
+--FILE--
+setScheme("1");
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified scheme is malformed (MissingSchemeNonRelativeUrl)
diff --git a/ext/uri/tests/whatwg/builder/scheme_error_special_char.phpt b/ext/uri/tests/whatwg/builder/scheme_error_special_char.phpt
new file mode 100644
index 000000000000..c37abc1ebb7c
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/scheme_error_special_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setScheme() - error - contains invalid special character
+--FILE--
+setScheme(":");
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified scheme is malformed (MissingSchemeNonRelativeUrl)
diff --git a/ext/uri/tests/whatwg/builder/scheme_success_ignorable_char.phpt b/ext/uri/tests/whatwg/builder/scheme_success_ignorable_char.phpt
new file mode 100644
index 000000000000..d60b3e274b34
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/scheme_success_ignorable_char.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setScheme() - success - contains ignorable newline and tab characters
+--FILE--
+setScheme("ht\ttp\ns");
+$builder->setHost("example.com");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/scheme_success_non_special.phpt b/ext/uri/tests/whatwg/builder/scheme_success_non_special.phpt
new file mode 100644
index 000000000000..c805105ee992
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/scheme_success_non_special.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setScheme() - success - non-special scheme
+--FILE--
+setScheme("foo");
+
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(6) "foo://"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(3) "foo"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(0) ""
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/scheme_success_special.phpt b/ext/uri/tests/whatwg/builder/scheme_success_special.phpt
new file mode 100644
index 000000000000..60a175a5f207
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/scheme_success_special.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setScheme() - success - contains digit & special characters
+--FILE--
+setScheme("my-12+34.scheme");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(18) "my-12+34.scheme://"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(15) "my-12+34.scheme"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(0) ""
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(0) ""
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/username_error_empty_opaque_host.phpt b/ext/uri/tests/whatwg/builder/username_error_empty_opaque_host.phpt
new file mode 100644
index 000000000000..c97ffd49afad
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/username_error_empty_opaque_host.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setUsername() - error - empty opaque host
+--FILE--
+setScheme("scheme");
+$builder->setHost("");
+$builder->setUsername("user");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have username
diff --git a/ext/uri/tests/whatwg/builder/username_error_file_scheme.phpt b/ext/uri/tests/whatwg/builder/username_error_file_scheme.phpt
new file mode 100644
index 000000000000..aee81ee3b835
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/username_error_file_scheme.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setUsername() - error - file scheme
+--FILE--
+setScheme("file");
+$builder->setPath("C:/a.txt");
+$builder->setUsername("user");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have username
diff --git a/ext/uri/tests/whatwg/builder/username_error_missing_opaque_host.phpt b/ext/uri/tests/whatwg/builder/username_error_missing_opaque_host.phpt
new file mode 100644
index 000000000000..257266f97c91
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/username_error_missing_opaque_host.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setUsername() - error - missing opaque host
+--FILE--
+setScheme("scheme");
+$builder->setUsername("user");
+
+try {
+ $builder->build();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\WhatWg\InvalidUrlException: The specified URL cannot have username
diff --git a/ext/uri/tests/whatwg/builder/username_success_invalid_percent_encoding.phpt b/ext/uri/tests/whatwg/builder/username_success_invalid_percent_encoding.phpt
new file mode 100644
index 000000000000..273fc18c72e3
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/username_success_invalid_percent_encoding.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setUsername() - success - invalid percent encoding is untouched
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setUsername("%3");
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(23) "https://%3@example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ string(2) "%3"
+ ["password"]=>
+ string(0) ""
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/username_success_null.phpt b/ext/uri/tests/whatwg/builder/username_success_null.phpt
new file mode 100644
index 000000000000..fae92f661a2d
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/username_success_null.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setUsername() - success - null
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setUsername("user");
+$builder->setUsername(null);
+$url = $builder->build();
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(20) "https://example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ NULL
+ ["password"]=>
+ NULL
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/username_success_special_char.phpt b/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
new file mode 100644
index 000000000000..5e18a2c66cc4
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/username_success_special_char.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setUsername() - success - contains special characters
+--FILE--
+setScheme("https");
+$builder->setHost("example.com");
+$builder->setUsername("~%#");
+$errors = [];
+$url = $builder->build(null, $errors);
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECTF--
+string(26) "https://~%%23@example.com/"
+object(Uri\WhatWg\Url)#%d (%d) {
+ ["scheme"]=>
+ string(5) "https"
+ ["username"]=>
+ string(5) "~%%23"
+ ["password"]=>
+ string(0) ""
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(1) "/"
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+bool(true)
diff --git a/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt b/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
new file mode 100644
index 000000000000..956bd1653ad8
--- /dev/null
+++ b/ext/uri/tests/whatwg/builder/username_success_tab_newline.phpt
@@ -0,0 +1,51 @@
+--TEST--
+Test Uri\WhatWg\UrlBuilder::setUsername() - success - contains tab and newline characters
+--FILE--
+setScheme("\tfo\no");
+$builder->setHost("example.com");
+$builder->setUsername("f\no\ro\t");
+$errors = [];
+$url = $builder->build(errors: $errors);
+
+var_dump($url->toAsciiString());
+var_dump($url);
+var_dump($errors);
+var_dump($url->equals(new Uri\WhatWg\Url($url->toAsciiString())));
+
+?>
+--EXPECT--
+string(30) "foo://f%0Ao%0Do%09@example.com"
+object(Uri\WhatWg\Url)#4 (8) {
+ ["scheme"]=>
+ string(3) "foo"
+ ["username"]=>
+ string(12) "f%0Ao%0Do%09"
+ ["password"]=>
+ string(0) ""
+ ["host"]=>
+ string(11) "example.com"
+ ["port"]=>
+ NULL
+ ["path"]=>
+ string(0) ""
+ ["query"]=>
+ NULL
+ ["fragment"]=>
+ NULL
+}
+array(1) {
+ [0]=>
+ object(Uri\WhatWg\UrlValidationError)#2 (3) {
+ ["context"]=>
+ string(5) " fo
+o"
+ ["type"]=>
+ enum(Uri\WhatWg\UrlValidationErrorType::InvalidUrlUnit)
+ ["failure"]=>
+ bool(false)
+ }
+}
+bool(true)
diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c
index 6d46feb51aae..b7a74fc1e231 100644
--- a/ext/uri/uri_parser_whatwg.c
+++ b/ext/uri/uri_parser_whatwg.c
@@ -28,7 +28,7 @@ ZEND_TLS lxb_unicode_idna_t lexbor_idna = {0};
static const size_t lexbor_mraw_byte_size = 8192;
-static zend_always_inline void zval_string_or_null_to_lexbor_str(const zval *value, lexbor_str_t *lexbor_str)
+ZEND_ATTRIBUTE_NONNULL static zend_always_inline void zval_string_or_null_to_lexbor_str(const zval *value, lexbor_str_t *lexbor_str)
{
if (Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) > 0) {
lexbor_str->data = (lxb_char_t *) Z_STRVAL_P(value);
@@ -40,7 +40,7 @@ static zend_always_inline void zval_string_or_null_to_lexbor_str(const zval *val
}
}
-static zend_always_inline void zval_long_or_null_to_lexbor_str(const zval *value, lexbor_str_t *lexbor_str)
+ZEND_ATTRIBUTE_NONNULL static zend_always_inline void zval_long_or_null_to_lexbor_str(const zval *value, lexbor_str_t *lexbor_str)
{
if (Z_TYPE_P(value) == IS_LONG) {
char buf[MAX_LENGTH_OF_LONG + 1];
@@ -53,20 +53,107 @@ static zend_always_inline void zval_long_or_null_to_lexbor_str(const zval *value
}
}
+ZEND_ATTRIBUTE_NONNULL static bool get_reason_from_error_type(const lxb_url_error_type_t error_type, const char **error_str)
+{
+ switch (error_type) {
+ case LXB_URL_ERROR_TYPE_DOMAIN_TO_ASCII:
+ *error_str = "DomainToAscii";
+ return true;
+ case LXB_URL_ERROR_TYPE_DOMAIN_TO_UNICODE:
+ *error_str = "DomainToUnicode";
+ return false;
+ case LXB_URL_ERROR_TYPE_DOMAIN_INVALID_CODE_POINT:
+ *error_str = "DomainInvalidCodePoint";
+ return true;
+ case LXB_URL_ERROR_TYPE_HOST_INVALID_CODE_POINT:
+ *error_str = "HostInvalidCodePoint";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV4_EMPTY_PART:
+ *error_str = "Ipv4EmptyPart";
+ return false;
+ case LXB_URL_ERROR_TYPE_IPV4_TOO_MANY_PARTS:
+ *error_str = "Ipv4TooManyParts";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV4_NON_NUMERIC_PART:
+ *error_str = "Ipv4NonNumericPart";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV4_NON_DECIMAL_PART:
+ *error_str = "Ipv4NonDecimalPart";
+ return false;
+ case LXB_URL_ERROR_TYPE_IPV4_OUT_OF_RANGE_PART:
+ *error_str = "Ipv4OutOfRangePart";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV6_UNCLOSED:
+ *error_str = "Ipv6Unclosed";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV6_INVALID_COMPRESSION:
+ *error_str = "Ipv6InvalidCompression";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV6_TOO_MANY_PIECES:
+ *error_str = "Ipv6TooManyPieces";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV6_MULTIPLE_COMPRESSION:
+ *error_str = "Ipv6MultipleCompression";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV6_INVALID_CODE_POINT:
+ *error_str = "Ipv6InvalidCodePoint";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV6_TOO_FEW_PIECES:
+ *error_str = "Ipv6TooFewPieces";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_MANY_PIECES:
+ *error_str = "Ipv4InIpv6TooManyPieces";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_INVALID_CODE_POINT:
+ *error_str = "Ipv4InIpv6InvalidCodePoint";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_OUT_OF_RANGE_PART:
+ *error_str = "Ipv4InIpv6OutOfRangePart";
+ return true;
+ case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_FEW_PARTS:
+ *error_str = "Ipv4InIpv6TooFewParts";
+ return true;
+ case LXB_URL_ERROR_TYPE_INVALID_URL_UNIT:
+ *error_str = "InvalidUrlUnit";
+ return false;
+ case LXB_URL_ERROR_TYPE_SPECIAL_SCHEME_MISSING_FOLLOWING_SOLIDUS:
+ *error_str = "SpecialSchemeMissingFollowingSolidus";
+ return false;
+ case LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL:
+ *error_str = "MissingSchemeNonRelativeUrl";
+ return true;
+ case LXB_URL_ERROR_TYPE_INVALID_REVERSE_SOLIDUS:
+ *error_str = "InvalidReverseSoldius";
+ return false;
+ case LXB_URL_ERROR_TYPE_INVALID_CREDENTIALS:
+ *error_str = "InvalidCredentials";
+ return false;
+ case LXB_URL_ERROR_TYPE_HOST_MISSING:
+ *error_str = "HostMissing";
+ return true;
+ case LXB_URL_ERROR_TYPE_PORT_OUT_OF_RANGE:
+ *error_str = "PortOutOfRange";
+ return true;
+ case LXB_URL_ERROR_TYPE_PORT_INVALID:
+ *error_str = "PortInvalid";
+ return true;
+ case LXB_URL_ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER:
+ *error_str = "FileInvalidWindowsDriveLetter";
+ return false;
+ case LXB_URL_ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER_HOST:
+ *error_str = "FileInvalidWindowsDriveLetterHost";
+ return false;
+ default: ZEND_UNREACHABLE();
+ }
+}
+
/**
* Creates a Uri\WhatWg\UrlValidationError class by mapping error codes listed in
* https://url.spec.whatwg.org/#writing to a Uri\WhatWg\UrlValidationErrorType enum.
* The result is passed by reference to the errors parameter.
*/
-static const char *fill_errors(zval *errors)
+ZEND_ATTRIBUTE_NONNULL static const char *fill_errors_inner(HashTable *errors)
{
- size_t log_len;
- if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) {
- ZVAL_EMPTY_ARRAY(errors);
- return NULL;
- }
-
- array_init_size(errors, log_len);
const char *result = NULL;
lexbor_plog_entry_t *lxb_error;
@@ -77,125 +164,8 @@ static const char *fill_errors(zval *errors)
const char *error_str;
zval failure;
- switch (lxb_error->id) {
- case LXB_URL_ERROR_TYPE_DOMAIN_TO_ASCII:
- error_str = "DomainToAscii";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_DOMAIN_TO_UNICODE:
- error_str = "DomainToUnicode";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_DOMAIN_INVALID_CODE_POINT:
- error_str = "DomainInvalidCodePoint";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_HOST_INVALID_CODE_POINT:
- error_str = "HostInvalidCodePoint";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_EMPTY_PART:
- error_str = "Ipv4EmptyPart";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_TOO_MANY_PARTS:
- error_str = "Ipv4TooManyParts";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_NON_NUMERIC_PART:
- error_str = "Ipv4NonNumericPart";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_NON_DECIMAL_PART:
- error_str = "Ipv4NonDecimalPart";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_OUT_OF_RANGE_PART:
- error_str = "Ipv4OutOfRangePart";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV6_UNCLOSED:
- error_str = "Ipv6Unclosed";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV6_INVALID_COMPRESSION:
- error_str = "Ipv6InvalidCompression";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV6_TOO_MANY_PIECES:
- error_str = "Ipv6TooManyPieces";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV6_MULTIPLE_COMPRESSION:
- error_str = "Ipv6MultipleCompression";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV6_INVALID_CODE_POINT:
- error_str = "Ipv6InvalidCodePoint";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV6_TOO_FEW_PIECES:
- error_str = "Ipv6TooFewPieces";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_MANY_PIECES:
- error_str = "Ipv4InIpv6TooManyPieces";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_INVALID_CODE_POINT:
- error_str = "Ipv4InIpv6InvalidCodePoint";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_OUT_OF_RANGE_PART:
- error_str = "Ipv4InIpv6OutOfRangePart";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_FEW_PARTS:
- error_str = "Ipv4InIpv6TooFewParts";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_INVALID_URL_UNIT:
- error_str = "InvalidUrlUnit";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_SPECIAL_SCHEME_MISSING_FOLLOWING_SOLIDUS:
- error_str = "SpecialSchemeMissingFollowingSolidus";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL:
- error_str = "MissingSchemeNonRelativeUrl";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_INVALID_REVERSE_SOLIDUS:
- error_str = "InvalidReverseSoldius";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_INVALID_CREDENTIALS:
- error_str = "InvalidCredentials";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_HOST_MISSING:
- error_str = "HostMissing";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_PORT_OUT_OF_RANGE:
- error_str = "PortOutOfRange";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_PORT_INVALID:
- error_str = "PortInvalid";
- ZVAL_TRUE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER:
- error_str = "FileInvalidWindowsDriveLetter";
- ZVAL_FALSE(&failure);
- break;
- case LXB_URL_ERROR_TYPE_FILE_INVALID_WINDOWS_DRIVE_LETTER_HOST:
- error_str = "FileInvalidWindowsDriveLetterHost";
- ZVAL_FALSE(&failure);
- break;
- default: ZEND_UNREACHABLE();
- }
+
+ ZVAL_BOOL(&failure, get_reason_from_error_type(lxb_error->id, &error_str));
zval error_type;
ZVAL_OBJ(&error_type, zend_enum_get_case_cstr(php_uri_ce_whatwg_url_validation_error_type, error_str));
@@ -207,12 +177,30 @@ static const char *fill_errors(zval *errors)
result = error_str;
}
- add_next_index_zval(errors, &error);
+ zend_hash_next_index_insert(errors, &error);
}
return result;
}
+/**
+ * Creates a Uri\WhatWg\UrlValidationError class by mapping error codes listed in
+ * https://url.spec.whatwg.org/#writing to a Uri\WhatWg\UrlValidationErrorType enum.
+ * The result is passed by reference to the errors parameter.
+ */
+ZEND_ATTRIBUTE_NONNULL static const char *fill_errors(zval *errors)
+{
+ size_t log_len;
+ if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) {
+ ZVAL_EMPTY_ARRAY(errors);
+ return NULL;
+ }
+
+ array_init_size(errors, log_len);
+
+ return fill_errors_inner(Z_ARRVAL_P(errors));
+}
+
static void throw_invalid_url_exception_during_write(zval *errors, const char *component)
{
zval err;
@@ -672,6 +660,348 @@ static void php_uri_parser_whatwg_destroy(void *uri)
lxb_url_destroy(lexbor_uri);
}
+ZEND_ATTRIBUTE_NONNULL static zend_always_inline zend_result php_uri_parser_whatwg_component_error(
+ const char *component_name, const lxb_url_error_type_t error_type
+) {
+ const char *reason = "";
+ if (error_type != LXB_URL_ERROR_TYPE__LAST_ENTRY) {
+ get_reason_from_error_type(error_type, &reason);
+ }
+
+ zend_throw_exception_ex(php_uri_ce_whatwg_invalid_url_exception,
+ 0,
+ "The specified %s is malformed%s%s%s",
+ component_name,
+ reason ? " (" : "",
+ reason ? reason : "",
+ reason ? ")" : ""
+ );
+
+ return FAILURE;
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_validate_none(const zend_string *component)
+{
+ return SUCCESS;
+}
+
+static zend_always_inline bool php_uri_whatwg_is_ascii_tab_or_newline(const unsigned char c)
+{
+ return c == '\t' || c == '\n' || c == '\r';
+}
+
+static zend_always_inline bool php_uri_parser_whatwg_is_alpha(const unsigned char c)
+{
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+}
+
+static zend_always_inline bool php_uri_parser_whatwg_is_digit(const unsigned char c)
+{
+ return c >= '0' && c <= '9';
+}
+
+static zend_always_inline unsigned char php_uri_ascii_to_lowercase(const unsigned char c)
+{
+ return c >= 'A' && c <= 'Z' ? (unsigned char) (c + ('a' - 'A')) : c;
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_validate_scheme(const zend_string *scheme)
+{
+ const char *p = ZSTR_VAL(scheme);
+ const char *end = p + ZSTR_LEN(scheme);
+ bool seen_first = false;
+
+ for (const char *c = p; c < end; c++) {
+ const unsigned char uc = (unsigned char) *c;
+
+ if (php_uri_whatwg_is_ascii_tab_or_newline(uc)) {
+ continue;
+ }
+
+ if (!seen_first) {
+ if (!php_uri_parser_whatwg_is_alpha(uc)) {
+ return php_uri_parser_whatwg_component_error("scheme", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
+ }
+ seen_first = true;
+ } else {
+ if (!php_uri_parser_whatwg_is_alpha(uc) && !php_uri_parser_whatwg_is_digit(uc) && uc != '+' && uc != '-' && uc != '.') {
+ return php_uri_parser_whatwg_component_error("scheme", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
+ }
+ }
+ }
+
+ if (!seen_first) {
+ return php_uri_parser_whatwg_component_error("scheme", LXB_URL_ERROR_TYPE_MISSING_SCHEME_NON_RELATIVE_URL);
+ }
+
+ return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_validate_host(const zend_string *host)
+{
+ if (ZSTR_LEN(host) == 0) {
+ /* Skip validation - an empty string may or may not be a valid host depending on whether the URL is special */
+ return SUCCESS;
+ }
+
+ const char *first = ZSTR_VAL(host);
+ const char *last = ZSTR_VAL(host) + ZSTR_LEN(host);
+
+ while (first < last && php_uri_whatwg_is_ascii_tab_or_newline((unsigned char) *first)) {
+ first++;
+ }
+
+ if (*first != '[') {
+ /* Skip validation - The host is not an IPv6 address */
+ return SUCCESS;
+ }
+
+ while (first < last && php_uri_whatwg_is_ascii_tab_or_newline((unsigned char) *last)) {
+ last--;
+ }
+
+ lxb_char_t *stripped_host = emalloc(last - first + 1);
+ size_t stripped_host_len = 0;
+
+ for (const char *i = first; i < last; i++) {
+ const unsigned char uc = (unsigned char) *i;
+
+ if (!php_uri_whatwg_is_ascii_tab_or_newline(uc)) {
+ stripped_host[stripped_host_len++] = (lxb_char_t) uc;
+ }
+ }
+ stripped_host[stripped_host_len] = '\0';
+
+ uint16_t ipv6[8];
+ const lxb_status_t result = lxb_url_parse_host_ipv6(
+ &lexbor_parser,
+ stripped_host,
+ stripped_host_len,
+ ipv6
+ );
+
+ efree(stripped_host);
+
+ if (result != LXB_STATUS_OK) {
+ lexbor_plog_entry_t *lxb_error;
+
+ if (lexbor_parser.log != NULL && (lxb_error = lexbor_array_obj_pop(&lexbor_parser.log->list)) != NULL) {
+ return php_uri_parser_whatwg_component_error("host", lxb_error->id);
+ } else {
+ return php_uri_parser_whatwg_component_error("host", LXB_URL_ERROR_TYPE__LAST_ENTRY);
+ }
+ }
+
+ return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_validate_port(const zend_long port)
+{
+ if (port < 0) {
+ return php_uri_parser_whatwg_component_error("port", LXB_URL_ERROR_TYPE_PORT_INVALID);
+ }
+
+ if (port > 65535) {
+ return php_uri_parser_whatwg_component_error("port", LXB_URL_ERROR_TYPE_PORT_OUT_OF_RANGE);
+ }
+
+ return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NONNULL static lxb_url_scheme_type_t php_uri_parser_whatwg_get_special_scheme(const zend_string *scheme)
+{
+ const char *p = ZSTR_VAL(scheme);
+ const char *end = p + ZSTR_LEN(scheme);
+
+ /*
+ * Create a normalized buffer from the scheme by leaving out tab and newline characters.
+ * The longest special scheme "https" is 5 characters, therefore 6 bytes is enough to store.
+ */
+ char buf[6];
+ size_t buf_len = 0;
+
+ for (const char *c = p; c < end; c++) {
+ const unsigned char uc = (unsigned char) *c;
+
+ if (php_uri_whatwg_is_ascii_tab_or_newline(uc)) {
+ continue;
+ }
+
+ if (buf_len == sizeof(buf) - 1) {
+ /* Longer than any special scheme */
+ return false;
+ }
+
+ buf[buf_len++] = (char) php_uri_ascii_to_lowercase(uc);
+ }
+
+ switch (buf_len) {
+ case 2:
+ if (memcmp(buf, "ws", 2) == 0) {
+ return LXB_URL_SCHEMEL_TYPE_WS;
+ }
+
+ break;
+ case 3:
+ if (memcmp(buf, "ftp", 3) == 0) {
+ return LXB_URL_SCHEMEL_TYPE_FTP;
+ }
+
+ if (memcmp(buf, "wss", 3) == 0) {
+ return LXB_URL_SCHEMEL_TYPE_WSS;
+ }
+
+ break;
+ case 4:
+ if (memcmp(buf, "http", 4) == 0) {
+ return LXB_URL_SCHEMEL_TYPE_HTTP;
+ }
+
+ if (memcmp(buf, "file", 4) == 0) {
+ return LXB_URL_SCHEMEL_TYPE_FILE;
+ }
+
+ break;
+ case 5:
+ if (memcmp(buf, "https", 5) == 0) {
+ return LXB_URL_SCHEMEL_TYPE_HTTPS;
+ }
+
+ break;
+ }
+
+ return LXB_URL_SCHEMEL_TYPE__UNDEF;
+}
+
+ZEND_ATTRIBUTE_NONNULL static void php_uri_parser_whatwg_build_errors(zval *errors)
+{
+ size_t log_len;
+
+ if (lexbor_parser.log == NULL || (log_len = lexbor_plog_length(lexbor_parser.log)) == 0) {
+ return;
+ }
+
+ if (Z_TYPE_P(errors) != IS_ARRAY) {
+ zval_ptr_dtor(errors);
+ array_init_size(errors, log_len);
+ }
+
+ fill_errors_inner(Z_ARRVAL_P(errors));
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_from_zval(
+ lxb_url_t *lexbor_base_url, const zval *scheme, const zval *username, const zval *password,
+ const zval *host, const zval *port, const zval *path, const zval *query, const zval *fragment,
+ zval *errors_zv
+) {
+ if (Z_TYPE_P(host) == IS_NULL ||
+ Z_STRLEN_P(host) == 0 ||
+ php_uri_parser_whatwg_get_special_scheme(Z_STR_P(scheme)) == LXB_URL_SCHEMEL_TYPE_FILE
+ ) {
+ if (Z_TYPE_P(username) != IS_NULL) {
+ zend_throw_exception_ex(php_uri_ce_whatwg_invalid_url_exception, 0, "The specified URL cannot have username");
+ return NULL;
+ }
+
+ if (Z_TYPE_P(password) != IS_NULL) {
+ zend_throw_exception_ex(php_uri_ce_whatwg_invalid_url_exception, 0, "The specified URL cannot have password");
+ return NULL;
+ }
+
+ if (Z_TYPE_P(port) != IS_NULL) {
+ zend_throw_exception_ex(php_uri_ce_whatwg_invalid_url_exception, 0, "The specified URL cannot have port");
+ return NULL;
+ }
+ }
+
+ lxb_url_parser_clean(&lexbor_parser);
+
+ lxb_url_t *lexbor_url = lexbor_mraw_calloc(lexbor_parser.mraw, sizeof(*lexbor_url));
+ if (lexbor_url == NULL) {
+ zend_throw_exception(php_uri_ce_whatwg_invalid_url_exception, "Memory allocation error", 0);
+ return NULL;
+ }
+
+ lexbor_url->mraw = lexbor_parser.mraw;
+
+ /*
+ * The URL is initialized as LXB_URL_SCHEMEL_TYPE__UNDEF but this would prevent the scheme to be updated
+ * in case of non-special schemes due to https://github.com/php/php-src/blob/27d7b799c0a13578ee0506b428b8ddc209ffb010/ext/lexbor/lexbor/url/url.c#L1402
+ */
+ if (php_uri_parser_whatwg_get_special_scheme(Z_STR_P(scheme)) == LXB_URL_SCHEMEL_TYPE__UNDEF) {
+ lexbor_url->scheme.type = LXB_URL_SCHEMEL_TYPE__UNKNOWN;
+ }
+
+ zval errors;
+ ZVAL_UNDEF(&errors);
+
+ zend_result result = php_uri_parser_whatwg_scheme_write(lexbor_url, scheme, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ result = php_uri_parser_whatwg_host_write(lexbor_url, host, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ /* Intentionally writing username after host to avoid error when the username is set but the host is missing */
+ result = php_uri_parser_whatwg_username_write(lexbor_url, username, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ /* Intentionally writing password after host to avoid error when the password is set but the password is missing */
+ result = php_uri_parser_whatwg_password_write(lexbor_url, password, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ /* Intentionally writing port after host to avoid error when the port is set but the host is missing */
+ result = php_uri_parser_whatwg_port_write(lexbor_url, port, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ result = php_uri_parser_whatwg_path_write(lexbor_url, path, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ result = php_uri_parser_whatwg_query_write(lexbor_url, query, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ result = php_uri_parser_whatwg_fragment_write(lexbor_url, fragment, NULL);
+ php_uri_parser_whatwg_build_errors(&errors);
+ if (result == FAILURE) {
+ goto failure;
+ }
+
+ if (lexbor_base_url != NULL) {
+ /* TODO */
+ }
+
+ if (php_uri_pass_errors_by_ref_and_free(errors_zv, &errors) == FAILURE) {
+ goto failure;
+ }
+
+ return lexbor_url;
+
+failure:
+ zval_ptr_dtor(&errors);
+ lxb_url_destroy(lexbor_url);
+ return NULL;
+}
+
PHPAPI const php_uri_parser php_uri_parser_whatwg = {
.name = PHP_URI_PARSER_WHATWG,
.parse = php_uri_parser_whatwg_parse,
diff --git a/ext/uri/uri_parser_whatwg.h b/ext/uri/uri_parser_whatwg.h
index f714ee483680..0a03c8e76a93 100644
--- a/ext/uri/uri_parser_whatwg.h
+++ b/ext/uri/uri_parser_whatwg.h
@@ -25,7 +25,19 @@ 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_result php_uri_parser_whatwg_validate_none(const zend_string *component);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_validate_scheme(const zend_string *scheme);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_validate_host(const zend_string *host);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_whatwg_validate_port(zend_long port);
+
PHP_RINIT_FUNCTION(uri_parser_whatwg);
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(2, 3, 4, 5, 6, 7, 8, 9) lxb_url_t *php_uri_parser_whatwg_build_from_zval(
+ lxb_url_t *lexbor_base_url, const zval *scheme, const zval *username, const zval *password,
+ const zval *host, const zval *port, const zval *path, const zval *query, const zval *fragment,
+ zval *errors_zv
+);
+
ZEND_MODULE_POST_ZEND_DEACTIVATE_D(uri_parser_whatwg);
#endif