Commit f985f774c6c for php.net
commit f985f774c6c3c84bc98ec71d85b2e818d2f5019a
Author: David Carlier <devnexen@gmail.com>
Date: Mon Aug 3 11:54:37 2026 +0100
ext/intl: introduce SpoofChecker::areBidiConfusable.
Adding a new more refined spoofchecker method in addition of the existing
Spoofchecker::areConfusable which takes in account the text direction
left to right and right to left, along with the Spoofchecker::LTR and
Spoofchecker::RTL constants.
The self and typed references tests are extended accordingly, guarded at
runtime rather than through a SKIPIF, so that the older ICU releases keep
their coverage.
Close GH-13469
diff --git a/NEWS b/NEWS
index 9e4651c305a..186d6eb5fe6 100644
--- a/NEWS
+++ b/NEWS
@@ -20,6 +20,7 @@ PHP NEWS
after successful calls. (Weilin Du)
. Implemented GH-20255 (Add a predefined calendar constant in
IntlDateFormatter for the proleptic gregorian calendar). (David Carlier)
+ . Added SpoofChecker::areBidiConfusable(). (David Carlier)
- Reflection:
. Added ReflectionAttribute::inNamespace(),
diff --git a/UPGRADING b/UPGRADING
index 9ac2c2e6f2c..b988847c8f5 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -328,6 +328,10 @@ PHP 8.6 UPGRADE NOTES
IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY and
IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks.
It is supported from icu 63.
+ . Added SpoofChecker::areBidiConfusable() to check whether two strings are
+ confusable for a given text direction, along with the SpoofChecker::LTR
+ and SpoofChecker::RTL direction constants.
+ It is supported from icu 74.
- IO:
. Added new polling API.
@@ -483,6 +487,7 @@ PHP 8.6 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/grapheme_strrev
. Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue()
RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue
+ . SpoofChecker::areBidiConfusable()
- mysqli:
. Added mysqli::quote_string() and mysqli_quote_string().
diff --git a/ext/intl/spoofchecker/spoofchecker.stub.php b/ext/intl/spoofchecker/spoofchecker.stub.php
index 51a9c7d3907..f3de2fcfcf1 100644
--- a/ext/intl/spoofchecker/spoofchecker.stub.php
+++ b/ext/intl/spoofchecker/spoofchecker.stub.php
@@ -49,6 +49,13 @@ class Spoofchecker
public const int SIMPLE_CASE_INSENSITIVE = UNKNOWN;
#endif
+#if U_ICU_VERSION_MAJOR_NUM >= 74
+ /** @cvalue UBIDI_LTR */
+ public const int LTR = UNKNOWN;
+ /** @cvalue UBIDI_RTL */
+ public const int RTL = UNKNOWN;
+#endif
+
public function __construct() {}
/**
@@ -72,4 +79,11 @@ public function setChecks(int $checks): void {}
/** @tentative-return-type */
public function setRestrictionLevel(int $level): void {}
public function setAllowedChars(string $pattern, int $patternOptions = 0): void {}
+
+#if U_ICU_VERSION_MAJOR_NUM >= 74
+ /**
+ * @param int $errorCode
+ */
+ public function areBidiConfusable(int $direction, string $string1, string $string2, &$errorCode = null): bool {}
+#endif
}
diff --git a/ext/intl/spoofchecker/spoofchecker_arginfo.h b/ext/intl/spoofchecker/spoofchecker_arginfo.h
index 8704065e479..cc4274d8bfe 100644
Binary files a/ext/intl/spoofchecker/spoofchecker_arginfo.h and b/ext/intl/spoofchecker/spoofchecker_arginfo.h differ
diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp
index 1e8e9ac2a78..d80ada93660 100644
--- a/ext/intl/spoofchecker/spoofchecker_main.cpp
+++ b/ext/intl/spoofchecker/spoofchecker_main.cpp
@@ -223,3 +223,46 @@ U_CFUNC PHP_METHOD(Spoofchecker, setAllowedChars)
php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co)));
}
}
+
+#if U_ICU_VERSION_MAJOR_NUM >= 74
+/* {{{ Checks if a given text contains any confusable characters, for a given text direction */
+U_CFUNC PHP_METHOD(Spoofchecker, areBidiConfusable)
+{
+ uint32_t ret = 0;
+ zend_long direction;
+ zend_string *s1, *s2;
+ zval *error_code = NULL;
+ SPOOFCHECKER_METHOD_INIT_VARS;
+
+ ZEND_PARSE_PARAMETERS_START(3, 4)
+ Z_PARAM_LONG(direction)
+ Z_PARAM_STR(s1)
+ Z_PARAM_STR(s2)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_ZVAL(error_code)
+ ZEND_PARSE_PARAMETERS_END();
+
+ SPOOFCHECKER_METHOD_FETCH_OBJECT;
+
+ if (direction != UBIDI_LTR && direction != UBIDI_RTL) {
+ zend_argument_value_error(1, "must be either Spoofchecker::LTR or Spoofchecker::RTL");
+ RETURN_THROWS();
+ }
+
+ if (UNEXPECTED(ZSTR_LEN(s1) > INT32_MAX || ZSTR_LEN(s2) > INT32_MAX)) {
+ SPOOFCHECKER_ERROR_CODE(co) = U_BUFFER_OVERFLOW_ERROR;
+ } else {
+ ret = uspoof_areBidiConfusableUTF8(co->uspoof, (UBiDiDirection)direction, ZSTR_VAL(s1), (int32_t)ZSTR_LEN(s1), ZSTR_VAL(s2), (int32_t)ZSTR_LEN(s2), SPOOFCHECKER_ERROR_CODE_P(co));
+ }
+ if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) {
+ php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co)));
+ RETURN_TRUE;
+ }
+
+ if (error_code) {
+ ZEND_TRY_ASSIGN_REF_LONG(error_code, ret);
+ }
+ RETVAL_BOOL(ret != 0);
+}
+/* }}} */
+#endif
diff --git a/ext/intl/tests/spoofchecker_self_references.phpt b/ext/intl/tests/spoofchecker_self_references.phpt
index d2feaa77a32..7b1dc377883 100644
--- a/ext/intl/tests/spoofchecker_self_references.phpt
+++ b/ext/intl/tests/spoofchecker_self_references.phpt
@@ -11,6 +11,11 @@
$checker = new Spoofchecker();
$checker->areConfusable("", "", $checker);
+if (version_compare(INTL_ICU_VERSION, '74.0') >= 0) {
+ $checker = new Spoofchecker();
+ $checker->areBidiConfusable(Spoofchecker::LTR, "", "", $checker);
+}
+
echo "Done\n";
?>
diff --git a/ext/intl/tests/spoofchecker_typed_references.phpt b/ext/intl/tests/spoofchecker_typed_references.phpt
index 5508497072d..5d1fa4554ce 100644
--- a/ext/intl/tests/spoofchecker_typed_references.phpt
+++ b/ext/intl/tests/spoofchecker_typed_references.phpt
@@ -23,6 +23,20 @@ class Test {
$checker->areConfusable("", "", $test->x);
var_dump($test);
+if (version_compare(INTL_ICU_VERSION, '74.0') >= 0) {
+ $test = new Test;
+ $test->x = "";
+
+ $checker = new Spoofchecker();
+ $checker->areBidiConfusable(Spoofchecker::LTR, "", "", $test->x);
+ /* Asserted quietly rather than dumped, so that the expected output stays
+ the same on ICU < 74, where the method does not exist. */
+ if ($test->x !== "1") {
+ echo "unexpected value: ";
+ var_dump($test->x);
+ }
+}
+
?>
--EXPECT--
object(Test)#1 (1) {
diff --git a/ext/intl/tests/spoofchecker_ubidi.phpt b/ext/intl/tests/spoofchecker_ubidi.phpt
new file mode 100644
index 00000000000..6e1f7b35fc6
--- /dev/null
+++ b/ext/intl/tests/spoofchecker_ubidi.phpt
@@ -0,0 +1,54 @@
+--TEST--
+Spoofchecker::areBidiConfusable() checks if strings are confusable in a given direction.
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (version_compare(INTL_ICU_VERSION, '74.0') < 0) die('skip for ICU >= 74.0'); ?>
+--FILE--
+<?php
+$s = new Spoofchecker();
+
+try {
+ $s->areBidiConfusable(Spoofchecker::RTL + 1, "a", "a");
+} catch (ValueError $e) {
+ echo $e->getMessage() . PHP_EOL;
+}
+
+/* "A1<aleph>" and "A<aleph>1" both display as "A1<aleph>" in a left to right
+ * context, but differ in a right to left one. */
+var_dump($s->areBidiConfusable(Spoofchecker::LTR, "A1\u{05D0}", "A\u{05D0}1"));
+var_dump($s->areBidiConfusable(Spoofchecker::RTL, "A1\u{05D0}", "A\u{05D0}1"));
+
+/* Mirror case: confusable in a right to left context only. */
+var_dump($s->areBidiConfusable(Spoofchecker::LTR, "\u{05D0}A_1", "\u{05D0}1_A"));
+var_dump($s->areBidiConfusable(Spoofchecker::RTL, "\u{05D0}A_1", "\u{05D0}1_A"));
+
+/* Neither direction reorders these into each other. */
+var_dump($s->areBidiConfusable(Spoofchecker::LTR, "Mark_", "_Mark"));
+var_dump($s->areBidiConfusable(Spoofchecker::RTL, "Mark_", "_Mark"));
+
+/* areConfusable() ignores the text direction and misses both cases above. */
+var_dump($s->areConfusable("A1\u{05D0}", "A\u{05D0}1"));
+var_dump($s->areConfusable("\u{05D0}A_1", "\u{05D0}1_A"));
+
+$errorCode = null;
+var_dump($s->areBidiConfusable(Spoofchecker::LTR, "A1\u{05D0}", "A\u{05D0}1", $errorCode));
+var_dump($errorCode === Spoofchecker::MIXED_SCRIPT_CONFUSABLE);
+
+var_dump($s->areBidiConfusable(Spoofchecker::LTR, "Mark_", "_Mark", $errorCode));
+var_dump($errorCode);
+?>
+--EXPECT--
+Spoofchecker::areBidiConfusable(): Argument #1 ($direction) must be either Spoofchecker::LTR or Spoofchecker::RTL
+bool(true)
+bool(false)
+bool(false)
+bool(true)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(true)
+bool(true)
+bool(false)
+int(0)