Commit 1bf64cee938 for php.net

commit 1bf64cee9387bec1fdee84e57ff74bea79311aad
Author: Máté Kocsis <kocsismate@woohoolabs.com>
Date:   Sun May 24 23:35:44 2026 +0200

    Implement "Followup improvements for ext/uri" RFC - URI type detection (#22075)

    RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection

    ---------

    Co-authored-by: Tim Düsterhus <timwolla@googlemail.com>

diff --git a/NEWS b/NEWS
index 4caae2db920..bb873c2619d 100644
--- a/NEWS
+++ b/NEWS
@@ -241,6 +241,10 @@ PHP                                                                        NEWS
   . Fixed bug #49874 (ftell() and fseek() inconsistency when using stream
     filters). (Jakub Zelenka)

+- URI:
+  . Added Uri\Rfc3986\Uri:getUriType() and Uri\WhatWg\Url:isSpecialScheme().
+    (kocsismate)
+
 - Zip:
   . Fixed ZipArchive callback being called after executor has shut down.
     (ilutov)
diff --git a/UPGRADING b/UPGRADING
index f2b6303a923..38858a27ea9 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -211,6 +211,10 @@ PHP 8.6 UPGRADE NOTES
     options.
   . Allowed casting casting filtered streams as file descriptor for select.

+- URI:
+  . Added Uri\Rfc3986\Uri:getUriType() and Uri\WhatWg\Url:isSpecialScheme().
+    RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection
+
 ========================================
 3. Changes in SAPI modules
 ========================================
diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c
index 4cf6e970b07..007517ae04d 100644
--- a/ext/uri/php_uri.c
+++ b/ext/uri/php_uri.c
@@ -31,6 +31,7 @@
 #include "uriparser/Uri.h"

 zend_class_entry *php_uri_ce_rfc3986_uri;
+zend_class_entry *php_uri_ce_rfc3986_uri_type;
 zend_class_entry *php_uri_ce_whatwg_url;
 zend_class_entry *php_uri_ce_comparison_mode;
 zend_class_entry *php_uri_ce_exception;
@@ -508,6 +509,16 @@ PHP_METHOD(Uri_WhatWg_Url, __construct)
 	create_whatwg_uri(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
 }

+PHP_METHOD(Uri_Rfc3986_Uri, getUriType)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	php_uri_object *uri_object = Z_URI_OBJECT_P(ZEND_THIS);
+	ZEND_ASSERT(uri_object->uri != NULL);
+
+	php_uri_parser_rfc3986_uri_type_read(uri_object->uri, return_value);
+}
+
 PHP_METHOD(Uri_Rfc3986_Uri, getScheme)
 {
 	php_uri_property_read_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_URI_PROPERTY_NAME_SCHEME, PHP_URI_COMPONENT_READ_MODE_NORMALIZED_ASCII);
@@ -883,6 +894,16 @@ PHP_METHOD(Uri_WhatWg_Url, withScheme)
 	php_uri_property_write_str_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_URI_PROPERTY_NAME_SCHEME);
 }

+PHP_METHOD(Uri_WhatWg_Url, isSpecialScheme)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	php_uri_object *uri_object = Z_URI_OBJECT_P(ZEND_THIS);
+	ZEND_ASSERT(uri_object->uri != NULL);
+
+	RETVAL_BOOL(php_uri_parser_whatwg_is_special(uri_object->uri));
+}
+
 PHP_METHOD(Uri_WhatWg_Url, withUsername)
 {
 	php_uri_property_write_str_or_null_helper(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_URI_PROPERTY_NAME_USERNAME);
@@ -1078,6 +1099,8 @@ static PHP_MINIT_FUNCTION(uri)
 	object_handlers_rfc3986_uri.free_obj = php_uri_object_handler_free;
 	object_handlers_rfc3986_uri.clone_obj = php_uri_object_handler_clone;

+	php_uri_ce_rfc3986_uri_type = register_class_Uri_Rfc3986_UriType();
+
 	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 6d4b2c3517a..03e8a483b0b 100644
--- a/ext/uri/php_uri.stub.php
+++ b/ext/uri/php_uri.stub.php
@@ -29,6 +29,14 @@ enum UriComparisonMode
 }

 namespace Uri\Rfc3986 {
+    enum UriType
+    {
+        case AbsolutePathReference;
+        case RelativePathReference;
+        case NetworkPathReference;
+        case Uri;
+    }
+
     /** @strict-properties */
     final readonly class Uri
     {
@@ -36,6 +44,8 @@ public static function parse(string $uri, ?\Uri\Rfc3986\Uri $baseUrl = null): ?s

         public function __construct(string $uri, ?\Uri\Rfc3986\Uri $baseUrl = null) {}

+        public function getUriType(): ?\Uri\Rfc3986\UriType {}
+
         public function getScheme(): ?string {}

         public function getRawScheme(): ?string {}
@@ -165,6 +175,8 @@ public function getScheme(): string {}

         public function withScheme(string $scheme): static {}

+        public function isSpecialScheme(): bool {}
+
         /** @implementation-alias Uri\Rfc3986\Uri::getUsername */
         public function getUsername(): ?string {}

diff --git a/ext/uri/php_uri_arginfo.h b/ext/uri/php_uri_arginfo.h
index 18d7f4adf78..e88d439fa16 100644
Binary files a/ext/uri/php_uri_arginfo.h and b/ext/uri/php_uri_arginfo.h differ
diff --git a/ext/uri/php_uri_common.h b/ext/uri/php_uri_common.h
index 65e9cff89a1..4c0ac466da7 100644
--- a/ext/uri/php_uri_common.h
+++ b/ext/uri/php_uri_common.h
@@ -18,6 +18,7 @@
 #include "php_uri_decl.h"

 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_whatwg_url;
 extern zend_class_entry *php_uri_ce_comparison_mode;
 extern zend_class_entry *php_uri_ce_exception;
diff --git a/ext/uri/php_uri_decl.h b/ext/uri/php_uri_decl.h
index 3c069f3abe6..947858515db 100644
Binary files a/ext/uri/php_uri_decl.h and b/ext/uri/php_uri_decl.h differ
diff --git a/ext/uri/tests/rfc3986/getters/uri_type_success_absolute_path_reference.phpt b/ext/uri/tests/rfc3986/getters/uri_type_success_absolute_path_reference.phpt
new file mode 100644
index 00000000000..1cddc15fa33
--- /dev/null
+++ b/ext/uri/tests/rfc3986/getters/uri_type_success_absolute_path_reference.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\Rfc3986\Uri getter - uri type - Absolute path reference
+--FILE--
+<?php
+
+$uri = Uri\Rfc3986\Uri::parse("/foo/bar");
+
+var_dump($uri->getUriType());
+
+?>
+--EXPECT--
+enum(Uri\Rfc3986\UriType::AbsolutePathReference)
diff --git a/ext/uri/tests/rfc3986/getters/uri_type_success_network_path_reference.phpt b/ext/uri/tests/rfc3986/getters/uri_type_success_network_path_reference.phpt
new file mode 100644
index 00000000000..d7debc5ba66
--- /dev/null
+++ b/ext/uri/tests/rfc3986/getters/uri_type_success_network_path_reference.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\Rfc3986\Uri getter - uri type - Network path reference
+--FILE--
+<?php
+
+$uri = Uri\Rfc3986\Uri::parse("//example.com/foo/bar");
+
+var_dump($uri->getUriType());
+
+?>
+--EXPECT--
+enum(Uri\Rfc3986\UriType::NetworkPathReference)
diff --git a/ext/uri/tests/rfc3986/getters/uri_type_success_relative_path_reference.phpt b/ext/uri/tests/rfc3986/getters/uri_type_success_relative_path_reference.phpt
new file mode 100644
index 00000000000..6cb51405f92
--- /dev/null
+++ b/ext/uri/tests/rfc3986/getters/uri_type_success_relative_path_reference.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\Rfc3986\Uri getter - uri type - Relative path reference
+--FILE--
+<?php
+
+$uri = Uri\Rfc3986\Uri::parse("foo/bar");
+
+var_dump($uri->getUriType());
+
+?>
+--EXPECT--
+enum(Uri\Rfc3986\UriType::RelativePathReference)
diff --git a/ext/uri/tests/rfc3986/getters/uri_type_success_uri_basic.phpt b/ext/uri/tests/rfc3986/getters/uri_type_success_uri_basic.phpt
new file mode 100644
index 00000000000..262300103f9
--- /dev/null
+++ b/ext/uri/tests/rfc3986/getters/uri_type_success_uri_basic.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\Rfc3986\Uri getter - uri type - URI
+--FILE--
+<?php
+
+$uri = Uri\Rfc3986\Uri::parse("https://example.com");
+
+var_dump($uri->getUriType());
+
+?>
+--EXPECT--
+enum(Uri\Rfc3986\UriType::Uri)
diff --git a/ext/uri/tests/rfc3986/getters/uri_type_success_uri_empty_authority.phpt b/ext/uri/tests/rfc3986/getters/uri_type_success_uri_empty_authority.phpt
new file mode 100644
index 00000000000..ad8f2f27a8c
--- /dev/null
+++ b/ext/uri/tests/rfc3986/getters/uri_type_success_uri_empty_authority.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\Rfc3986\Uri getter - uri type - URI with empty authority
+--FILE--
+<?php
+
+$uri = Uri\Rfc3986\Uri::parse("https://");
+
+var_dump($uri->getUriType());
+
+?>
+--EXPECT--
+enum(Uri\Rfc3986\UriType::Uri)
diff --git a/ext/uri/tests/rfc3986/getters/uri_type_success_uri_no_authority.phpt b/ext/uri/tests/rfc3986/getters/uri_type_success_uri_no_authority.phpt
new file mode 100644
index 00000000000..e266502ce90
--- /dev/null
+++ b/ext/uri/tests/rfc3986/getters/uri_type_success_uri_no_authority.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\Rfc3986\Uri getter - uri type - URI without authority
+--FILE--
+<?php
+
+$uri = Uri\Rfc3986\Uri::parse("https:");
+
+var_dump($uri->getUriType());
+
+?>
+--EXPECT--
+enum(Uri\Rfc3986\UriType::Uri)
diff --git a/ext/uri/tests/whatwg/getters/is_special_scheme_success_false.phpt b/ext/uri/tests/whatwg/getters/is_special_scheme_success_false.phpt
new file mode 100644
index 00000000000..9bbb8575584
--- /dev/null
+++ b/ext/uri/tests/whatwg/getters/is_special_scheme_success_false.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Test Uri\WhatWg\Url::isSpecialScheme() - success - not special
+--FILE--
+<?php
+
+$url = Uri\WhatWg\Url::parse("scheme://example.com");
+
+var_dump($url->isSpecialScheme());
+
+?>
+--EXPECT--
+bool(false)
diff --git a/ext/uri/tests/whatwg/getters/is_special_scheme_success_true.phpt b/ext/uri/tests/whatwg/getters/is_special_scheme_success_true.phpt
new file mode 100644
index 00000000000..1e19641b560
--- /dev/null
+++ b/ext/uri/tests/whatwg/getters/is_special_scheme_success_true.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Test Uri\WhatWg\Url::isSpecialScheme() - success - special
+--FILE--
+<?php
+
+$url = Uri\WhatWg\Url::parse("http://example.com");
+var_dump($url->isSpecialScheme());
+
+$url = Uri\WhatWg\Url::parse("https://example.com");
+var_dump($url->isSpecialScheme());
+
+$url = Uri\WhatWg\Url::parse("ws://example.com");
+var_dump($url->isSpecialScheme());
+
+$url = Uri\WhatWg\Url::parse("wss://example.com");
+var_dump($url->isSpecialScheme());
+
+$url = Uri\WhatWg\Url::parse("ftp://example.com");
+var_dump($url->isSpecialScheme());
+
+$url = Uri\WhatWg\Url::parse("file://example.com");
+var_dump($url->isSpecialScheme());
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/uri/uri_parser_rfc3986.c b/ext/uri/uri_parser_rfc3986.c
index 172d7f08f14..cdf88013252 100644
--- a/ext/uri/uri_parser_rfc3986.c
+++ b/ext/uri/uri_parser_rfc3986.c
@@ -15,6 +15,7 @@
 #include "php.h"
 #include "uri_parser_rfc3986.h"
 #include "php_uri_common.h"
+#include "Zend/zend_enum.h"
 #include "Zend/zend_smart_str.h"
 #include "Zend/zend_exceptions.h"

@@ -110,6 +111,25 @@ ZEND_ATTRIBUTE_NONNULL static UriUriA *get_uri_for_writing(php_uri_parser_rfc398
 	return &uriparser_uris->uri;
 }

+ZEND_ATTRIBUTE_NONNULL void php_uri_parser_rfc3986_uri_type_read(php_uri_parser_rfc3986_uris *uri, zval *retval)
+{
+	const UriUriA *uriparser_uri = get_uri_for_reading(uri, PHP_URI_COMPONENT_READ_MODE_RAW);
+
+	const char *type;
+
+	if (has_text_range(&uriparser_uri->scheme)) {
+		type = "Uri";
+	} else if (has_text_range(&uriparser_uri->hostText)) {
+		type = "NetworkPathReference";
+	} else if (uriparser_uri->absolutePath) {
+		type = "AbsolutePathReference";
+	} else {
+		type = "RelativePathReference";
+	}
+
+	ZVAL_OBJ_COPY(retval, zend_enum_get_case_cstr(php_uri_ce_rfc3986_uri_type, type));
+}
+
 ZEND_ATTRIBUTE_NONNULL static zend_result php_uri_parser_rfc3986_scheme_read(void *uri, php_uri_component_read_mode read_mode, zval *retval)
 {
 	const UriUriA *uriparser_uri = get_uri_for_reading(uri, read_mode);
diff --git a/ext/uri/uri_parser_rfc3986.h b/ext/uri/uri_parser_rfc3986.h
index 21f938c370a..bf8a0733fc8 100644
--- a/ext/uri/uri_parser_rfc3986.h
+++ b/ext/uri/uri_parser_rfc3986.h
@@ -21,6 +21,8 @@ PHPAPI extern const php_uri_parser php_uri_parser_rfc3986;

 typedef struct php_uri_parser_rfc3986_uris php_uri_parser_rfc3986_uris;

+ZEND_ATTRIBUTE_NONNULL void php_uri_parser_rfc3986_uri_type_read(php_uri_parser_rfc3986_uris *uri, zval *retval);
+
 zend_result php_uri_parser_rfc3986_userinfo_read(php_uri_parser_rfc3986_uris *uri, php_uri_component_read_mode read_mode, zval *retval);
 zend_result php_uri_parser_rfc3986_userinfo_write(php_uri_parser_rfc3986_uris *uri, zval *value, zval *errors);

diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c
index d140357c18e..4ca7073847e 100644
--- a/ext/uri/uri_parser_whatwg.c
+++ b/ext/uri/uri_parser_whatwg.c
@@ -273,6 +273,11 @@ static zend_result php_uri_parser_whatwg_scheme_write(void *uri, zval *value, zv
 	return SUCCESS;
 }

+ZEND_ATTRIBUTE_NONNULL bool php_uri_parser_whatwg_is_special(const lxb_url_t *lexbor_uri)
+{
+	return lxb_url_is_special(lexbor_uri);
+}
+
 /* 4.2. URL miscellaneous: A URL includes credentials if its username or password is not the empty string. */
 static bool includes_credentials(const lxb_url_t *lexbor_uri)
 {
diff --git a/ext/uri/uri_parser_whatwg.h b/ext/uri/uri_parser_whatwg.h
index 3e9e2824e42..1ed2b4ac0e1 100644
--- a/ext/uri/uri_parser_whatwg.h
+++ b/ext/uri/uri_parser_whatwg.h
@@ -20,6 +20,8 @@

 PHPAPI extern const php_uri_parser php_uri_parser_whatwg;

+ZEND_ATTRIBUTE_NONNULL bool php_uri_parser_whatwg_is_special(const lxb_url_t *lexbor_uri);
+
 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);

 PHP_RINIT_FUNCTION(uri_parser_whatwg);