[php-src] master: ext/intl: introduce SpoofChecker::areBidiConfusable.
David Carlier <[email protected]> Mon, 3 Aug 2026 19:43:41 +0000
| Newsgroups | gmane.comp.php.cvs.general |
|---|---|
| Message-ID | <[email protected]> |
Author: David Carlier (devnexen)
Date: 2026-08-03T20:43:27+01:00
Commit: https://github.com/php/php-src/commit/f985f774c6c3c84bc98ec71d85b2e818d2f5019a
Raw diff: https://github.com/php/php-src/commit/f985f774c6c3c84bc98ec71d85b2e818d2f5019a.diff
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
Changed paths:
A ext/intl/tests/spoofchecker_ubidi.phpt
M NEWS
M UPGRADING
M ext/intl/spoofchecker/spoofchecker.stub.php
M ext/intl/spoofchecker/spoofchecker_arginfo.h
M ext/intl/spoofchecker/spoofchecker_main.cpp
M ext/intl/tests/spoofchecker_self_references.phpt
M ext/intl/tests/spoofchecker_typed_references.phpt
Diff:
diff --git a/NEWS b/NEWS
index 9e4651c305af..186d6eb5fe63 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 9ac2c2e6f2cb..b988847c8f53 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 51a9c7d39074..f3de2fcfcf10 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 8704065e479b..cc4274d8bfe6 100644
--- a/ext/intl/spoofchecker/spoofchecker_arginfo.h
+++ b/ext/intl/spoofchecker/spoofchecker_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit spoofchecker.stub.php instead.
- * Stub hash: 4634f8ef9157fb3670a2ddc5e3246340660fc68c */
+ * Stub hash: 528a85d28312688d9ba2c78ede2f07356bbff0f6 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Spoofchecker___construct, 0, 0, 0)
ZEND_END_ARG_INFO()
@@ -32,6 +32,15 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Spoofchecker_setAllowedCha
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, patternOptions, IS_LONG, 0, "0")
ZEND_END_ARG_INFO()
+#if U_ICU_VERSION_MAJOR_NUM >= 74
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Spoofchecker_areBidiConfusable, 0, 3, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, direction, IS_LONG, 0)
+ ZEND_ARG_TYPE_INFO(0, string1, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, string2, IS_STRING, 0)
+ ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, errorCode, "null")
+ZEND_END_ARG_INFO()
+#endif
+
ZEND_METHOD(Spoofchecker, __construct);
ZEND_METHOD(Spoofchecker, isSuspicious);
ZEND_METHOD(Spoofchecker, areConfusable);
@@ -39,6 +48,9 @@ ZEND_METHOD(Spoofchecker, setAllowedLocales);
ZEND_METHOD(Spoofchecker, setChecks);
ZEND_METHOD(Spoofchecker, setRestrictionLevel);
ZEND_METHOD(Spoofchecker, setAllowedChars);
+#if U_ICU_VERSION_MAJOR_NUM >= 74
+ZEND_METHOD(Spoofchecker, areBidiConfusable);
+#endif
static const zend_function_entry class_Spoofchecker_methods[] = {
ZEND_ME(Spoofchecker, __construct, arginfo_class_Spoofchecker___construct, ZEND_ACC_PUBLIC)
@@ -48,6 +60,9 @@ static const zend_function_entry class_Spoofchecker_methods[] = {
ZEND_ME(Spoofchecker, setChecks, arginfo_class_Spoofchecker_setChecks, ZEND_ACC_PUBLIC)
ZEND_ME(Spoofchecker, setRestrictionLevel, arginfo_class_Spoofchecker_setRestrictionLevel, ZEND_ACC_PUBLIC)
ZEND_ME(Spoofchecker, setAllowedChars, arginfo_class_Spoofchecker_setAllowedChars, ZEND_ACC_PUBLIC)
+#if U_ICU_VERSION_MAJOR_NUM >= 74
+ ZEND_ME(Spoofchecker, areBidiConfusable, arginfo_class_Spoofchecker_areBidiConfusable, ZEND_ACC_PUBLIC)
+#endif
ZEND_FE_END
};
@@ -175,6 +190,20 @@ static zend_class_entry *register_class_Spoofchecker(void)
zend_declare_typed_class_constant(class_entry, const_SIMPLE_CASE_INSENSITIVE_name, &const_SIMPLE_CASE_INSENSITIVE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release_ex(const_SIMPLE_CASE_INSENSITIVE_name, true);
#endif
+#if U_ICU_VERSION_MAJOR_NUM >= 74
+
+ zval const_LTR_value;
+ ZVAL_LONG(&const_LTR_value, UBIDI_LTR);
+ zend_string *const_LTR_name = zend_string_init_interned("LTR", sizeof("LTR") - 1, true);
+ zend_declare_typed_class_constant(class_entry, const_LTR_name, &const_LTR_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+ zend_string_release_ex(const_LTR_name, true);
+
+ zval const_RTL_value;
+ ZVAL_LONG(&const_RTL_value, UBIDI_RTL);
+ zend_string *const_RTL_name = zend_string_init_interned("RTL", sizeof("RTL") - 1, true);
+ zend_declare_typed_class_constant(class_entry, const_RTL_name, &const_RTL_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+ zend_string_release_ex(const_RTL_name, true);
+#endif
return class_entry;
}
diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp
index 1e8e9ac2a78d..d80ada93660f 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 d2feaa77a32f..7b1dc3778839 100644
--- a/ext/intl/tests/spoofchecker_self_references.phpt
+++ b/ext/intl/tests/spoofchecker_self_references.phpt
@@ -11,6 +11,11 @@ $checker->isSuspicious("", $checker);
$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 5508497072da..5d1fa4554ce0 100644
--- a/ext/intl/tests/spoofchecker_typed_references.phpt
+++ b/ext/intl/tests/spoofchecker_typed_references.phpt
@@ -23,6 +23,20 @@ $checker = new Spoofchecker();
$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 000000000000..6e1f7b35fc64
--- /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)