Commit fbf8bf84dbe for php.net

commit fbf8bf84dbe3bed33a179a668e2c3d5d109d469a
Author: Kamil Tekiela <tekiela246@gmail.com>
Date:   Sun Apr 5 18:43:57 2026 +0100

    Implement mysqli::quote_string method (#20729)

diff --git a/NEWS b/NEWS
index 1759f6e2ee7..11a0dbb34a0 100644
--- a/NEWS
+++ b/NEWS
@@ -64,6 +64,9 @@ PHP                                                                        NEWS
   . Fixed bug GH-21223; mb_guess_encoding no longer crashes when passed huge
     list of candidate encodings (with 200,000+ entries). (Jordi Kroon)

+- Opcache:
+  . Added mysqli_quote_string() and mysqli::quote_string(). (Kamil Tekiela)
+
 - Opcache:
   . Fixed bug GH-20051 (apache2 shutdowns when restart is requested during
     preloading). (Arnaud, welcomycozyhom)
diff --git a/UPGRADING b/UPGRADING
index 467387a9ea3..18f34eca198 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -151,6 +151,10 @@ PHP 8.6 UPGRADE NOTES
   . `grapheme_strrev()` returns strrev for grapheme cluster unit.
     RFC: https://wiki.php.net/rfc/grapheme_strrev

+- mysqli:
+  . Added `mysqli::quote_string()` and `mysqli_quote_string()`.
+    RFC: https://wiki.php.net/rfc/mysqli_quote_string
+
 - Standard:
   . `clamp()` returns the given value if in range, else return the nearest
     bound.
diff --git a/ext/mysqli/mysqli.stub.php b/ext/mysqli/mysqli.stub.php
index 7ca2a20ca1b..cc6b8c57404 100644
--- a/ext/mysqli/mysqli.stub.php
+++ b/ext/mysqli/mysqli.stub.php
@@ -906,6 +906,11 @@ public function real_connect(
      */
     public function real_escape_string(string $string): string {}

+    /**
+     * @alias mysqli_quote_string
+     */
+    public function quote_string(string $string): string {}
+
     /**
      * @tentative-return-type
      * @alias mysqli_reap_async_query
@@ -1547,6 +1552,8 @@ function mysqli_real_escape_string(mysqli $mysql, string $string): string {}
 /** @alias mysqli_real_escape_string */
 function mysqli_escape_string(mysqli $mysql, string $string): string {}

+function mysqli_quote_string(mysqli $mysql, string $string): string {}
+
 function mysqli_real_query(mysqli $mysql, string $query): bool {}

 /** @refcount 1 */
diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c
index 9473f4a06c1..259fc128237 100644
--- a/ext/mysqli/mysqli_api.c
+++ b/ext/mysqli/mysqli_api.c
@@ -1198,7 +1198,7 @@ PHP_FUNCTION(mysqli_options)
 		zend_argument_value_error(ERROR_ARG_POS(2), "must be MYSQLI_INIT_COMMAND, MYSQLI_SET_CHARSET_NAME, MYSQLI_SERVER_PUBLIC_KEY, or one of the MYSQLI_OPT_* constants");
 		RETURN_THROWS();
 	}
-
+
 	if (expected_type != Z_TYPE_P(mysql_value)) {
 		switch (expected_type) {
 			case IS_STRING:
@@ -1363,6 +1363,29 @@ PHP_FUNCTION(mysqli_real_escape_string) {
 	RETURN_NEW_STR(newstr);
 }

+PHP_FUNCTION(mysqli_quote_string) {
+	MY_MYSQL	*mysql;
+	zval		*mysql_link = NULL;
+	char		*escapestr;
+	size_t		escapestr_len;
+	zend_string *newstr;
+
+	if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "Os", &mysql_link, mysqli_link_class_entry, &escapestr, &escapestr_len) == FAILURE) {
+		RETURN_THROWS();
+	}
+	MYSQLI_FETCH_RESOURCE_CONN(mysql, mysql_link, MYSQLI_STATUS_VALID);
+
+	newstr = zend_string_safe_alloc(2, escapestr_len, 2, 0);
+	char *out = ZSTR_VAL(newstr);
+	*out++ = '\'';
+	out += mysql_real_escape_string(mysql->mysql, out, escapestr, escapestr_len);
+	*out++ = '\'';
+	*out = '\0';
+	newstr = zend_string_truncate(newstr, out - ZSTR_VAL(newstr), 0);
+
+	RETURN_NEW_STR(newstr);
+}
+
 /* {{{ Undo actions from current transaction */
 PHP_FUNCTION(mysqli_rollback)
 {
diff --git a/ext/mysqli/mysqli_arginfo.h b/ext/mysqli/mysqli_arginfo.h
index 4439908e55d..0121f36f3cf 100644
Binary files a/ext/mysqli/mysqli_arginfo.h and b/ext/mysqli/mysqli_arginfo.h differ
diff --git a/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt b/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt
index bada1d85a5c..ccac6710edd 100644
--- a/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt
+++ b/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt
@@ -43,6 +43,7 @@
         'ping'					=> true,
         'prepare'				=> true,
         'query'					=> true,
+        'quote_string'			=> true,
         'real_connect'			=> true,
         'real_escape_string'	=> true,
         'real_query'			=> true,
diff --git a/ext/mysqli/tests/mysqli_quote_string.phpt b/ext/mysqli/tests/mysqli_quote_string.phpt
new file mode 100644
index 00000000000..b4a25aa3996
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_quote_string.phpt
@@ -0,0 +1,86 @@
+--TEST--
+mysqli_quote_string()
+--EXTENSIONS--
+mysqli
+--SKIPIF--
+<?php
+require_once 'skipifconnectfailure.inc';
+?>
+--FILE--
+<?php
+
+require_once 'connect.inc';
+mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
+
+echo mysqli_quote_string($link, '\\') . "\n";
+echo mysqli_quote_string($link, '"') . "\n";
+echo mysqli_quote_string($link, "'") . "\n";
+
+$escaped = mysqli_quote_string($link, "\' \ \"");
+echo $escaped . "\n";
+$result = $link->query("SELECT $escaped AS test");
+$value = $result->fetch_column();
+echo $value . "\n";
+
+$escaped = mysqli_quote_string($link, '" OR 1=1 -- foo');
+echo $escaped . "\n";
+$result = $link->query("SELECT $escaped AS test");
+$value = $result->fetch_column();
+echo $value . "\n";
+
+$escaped = mysqli_quote_string($link, "\n");
+if ($escaped !== "'\\n'") {
+    printf("[001] Expected '\\n', got %s\n", $escaped);
+}
+
+$escaped =  mysqli_quote_string($link, "\r");
+if ($escaped !== "'\\r'") {
+    printf("[002] Expected '\\r', got %s\n", $escaped);
+}
+
+$escaped =  mysqli_quote_string($link, "foo" . chr(0) . "bar");
+if ($escaped !== "'foo\\0bar'") {
+    printf("[003] Expected 'foo\\0bar', got %s\n", $escaped);
+}
+
+echo "=====================\n";
+
+// Test that the SQL injection is impossible with NO_BACKSLASH_ESCAPES mode
+$link->query('SET @@sql_mode="NO_BACKSLASH_ESCAPES"');
+
+echo $link->quote_string('\\') . "\n";
+echo $link->quote_string('"') . "\n";
+echo $link->quote_string("'") . "\n";
+
+$escaped = $link->quote_string("\' \ \"");
+echo $escaped . "\n";
+$result = $link->query("SELECT $escaped AS test");
+$value = $result->fetch_column();
+echo $value . "\n";
+
+$escaped = $link->quote_string('" OR 1=1 -- foo');
+echo $escaped . "\n";
+$result = $link->query("SELECT $escaped AS test");
+$value = $result->fetch_column();
+echo $value . "\n";
+
+echo "done!";
+?>
+--EXPECT--
+'\\'
+'\"'
+'\''
+'\\\' \\ \"'
+\' \ "
+'\" OR 1=1 -- foo'
+" OR 1=1 -- foo
+=====================
+'\'
+'"'
+''''
+'\'' \ "'
+\' \ "
+'" OR 1=1 -- foo'
+" OR 1=1 -- foo
+done!