You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Website/URL field rejects valid internationalized domain names (IDNs) whose hostname contains
accented characters. Reported by an Elite customer in Help Scout ticket 257002 for a .ch domain
spelled with an umlaut. .ch explicitly permits accented vowels, so the domain is legitimately
registrable.
The cause is a single host-matching regex, duplicated in PHP and JS. Its host character class [\da-z\.-] is ASCII-only:
classes/models/fields/FrmFieldUrl.php in FrmFieldUrl::validate()
js/formidable.js in checkUrlField()
js/formidable.min.js (the literal survives verbatim, since npm run minimize compiles at WHITESPACE level)
Formidable Pro has zero copies of this pattern.
Why this is safe rather than a loosening
Three things make this a consistency fix rather than a new capability:
The punycode spelling of the same domain already passes.xn---encoded hosts have always
validated, and they resolve to the identical host. Accepting the Unicode spelling adds nothing
a submitter could not already do.
Non-ASCII in the path, query and fragment already passes. The regex only constrains the
host, so the field already stored accented characters happily. Only the hostname was rejected.
esc_url_raw() already preserves the bytes. WordPress's URL sanitizer explicitly allows the
high byte range, so sanitizing was never the thing rejecting these values.
Sanitizing, escaping, storage and the scheme allowlist are all untouched. Chrome's native type="url" constraint already accepts every IDN form here, so the browser was not the blocker
either.
Verification
Exhaustive ASCII equivalence. The old and new patterns were compared across 1408 ASCII
byte/position combinations and are byte-for-byte identical. No previously accepted or rejected
ASCII url changes behaviour. This was brute-forced, not sampled.
Injection set unchanged.javascript:, data:, vbscript:, <script> and CRLF-header
inputs all still fail exactly as before. The only behavioural delta across the whole matrix is
the IDN cases.
Pattern read back from the edited file (rather than a copy) accepts 10 IDN forms, including
Cyrillic and CJK, and rejects 8 malformed hosts.
The trap worth knowing about in review
The two regexes deliberately do not use the same character range:
PHP matches UTF-8 bytes (no /u modifier), so the raw byte range covers all non-ASCII.
JS matches UTF-16 code units, so it needs \u0080-\uFFFF. The PHP byte range would cover an
umlaut but not Cyrillic or CJK.
Copying one literal into both would accept those hosts server side while silently rejecting them in
the browser, which is the same class of split this ticket is about. test_url_field_js_regex_parity()
asserts the two JS files carry the code unit form and never the PHP one.
/u is also deliberately absent from the PHP pattern: preg_match() returns false on invalid
UTF-8, and because the result is negated that would report valid Latin-1 input as invalid. test_url_non_utf8_host_byte() guards this, with a precondition so it cannot pass vacuously.
One more subtlety, confirmed empirically: appending the range after the trailing hyphen
([\da-z\.-\x80-\xff]) is not a syntax error. PCRE reads it as the range 0x2E-0x80, which
silently admits / ? : @ < > [ inside the hostname. The negative rows https://a/b.com and https://a?b.com exist to fail if anyone ever reorders the class that way.
Tests
test_url_idn_validation() - 11 must-pass values (accented Latin, Cyrillic, CJK, uppercase
non-ASCII, punycode, ASCII baseline, localhost, non-ASCII in path/query/fragment, and the
no-scheme form) and 4 must-fail values.
test_url_non_utf8_host_byte() - the /u guard.
test_url_field_js_regex_parity() - asserts the rule rather than pinning the literal, and checks
the minified artifact carries the same host class as its source. Mutation-proven: green on the
real files, red when formidable.min.js is reverted alone, red when the PHP range is copied into
the JS source.
3 rows appended to the shared expected_format_errors() table.
One Cypress it() with "Validate this form with javascript" enabled, so it exercises the
committed minified artifact through a real browser rather than passing via the server-side path.
php -l and node --check are clean. phpcs, phpstan, phpunit and eslint need composer install
plus a WordPress test database, which is not set up on this machine, so they run here in CI under
the labels below. Reviewer note: core CI is label-gated, so this PR carries run tests, run analysis and run e2e tests. Without them only typos and psalm would run.
Manual QA note
The front end serves the combined js/frm.min.js, which is rebuilt from js/formidable.min.js
only by FrmAppHelper::save_combined_js() (called from FrmMigrate and FrmAddon). When testing
this by hand, delete js/frm.min.js or set SCRIPT_DEBUG, otherwise a stale combined file will
serve the old regex and the fix will look like it did nothing.
Deliberately not fixed here
These are pre-existing and adjacent, left out to keep the change narrow. Each is a separate
behaviour decision, and they are grouped into a follow-up issue rather than lost:
Underscore in the host is rejected.
IPv6 literals are rejected.
ftp:, mailto:, news:, feed: and telnet: are rejected by the format check even though the
scheme allowlist a few lines above explicitly admits them - a real internal contradiction.
Garbage such as https://-.- and https://.... passes, so this check was never a strong quality
gate in the first place.
Help Scout ticket 257002.
Summary by CodeRabbit
Bug Fixes
URL fields now accept valid internationalized domain names, including domains containing accented or other non-ASCII characters.
Existing HTTP, HTTPS, and localhost validation rules remain supported.
Improved validation continues to reject malformed domains, dotless hostnames, invalid paths, and unsupported URL formats.
Tests
Added coverage for internationalized domains and consistency between browser and server-side URL validation.
The host pattern in FrmFieldUrl::validate() and its twin in checkUrlField()
allowed ASCII only, so valid internationalized domains were rejected. The
punycode spelling of the same domain already passed, and non-ASCII in the
path, query and fragment already passed, so this removes an inconsistency
rather than granting anything new.
The two character ranges differ by design. PHP matches UTF-8 bytes, so it
uses the raw byte range; JS matches UTF-16 code units, so it needs the code
unit range. Copying one literal into both would accept Cyrillic and CJK hosts
server side while silently rejecting them in the browser, so a test asserts
the two JS files carry the code unit form and never the PHP one.
The /u modifier is deliberately not added to the PHP pattern: preg_match()
returns false on invalid UTF-8, and because the result is negated that would
report valid Latin-1 input as invalid.
Sanitizing, escaping, storage and the scheme allowlist are untouched. The new
pattern was compared byte for byte with the old one across 1408 ASCII inputs
with no difference, so no ASCII url changes behaviour.
Help Scout ticket 257002.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@NathanaelJonesIreland, you've reached your PR review limit, so we couldn't start this review.
Next review available in:7 minutes
Limit details: You’ve used the included review currently available.
You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.
How can I continue?
After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.
To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.
How do review limits work?
CodeRabbit enforces per-developer PR review limits within each organization.
For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.
Reviewing files that changed from the base of the PR and between cf3f6c8 and df88b2d.
⛔ Files ignored due to path filters (1)
js/formidable.min.js is excluded by !**/*.min.js
📒 Files selected for processing (3)
js/formidable.js
tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js
tests/phpunit/fields/test_FrmFieldValidate.php
No actionable comments were generated in the recent review. 🎉
ℹ️ Recent review info⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a26a14d5-a247-4d76-b851-e35d56d60d64
📥 Commits
Reviewing files that changed from the base of the PR and between 3b3bf52 and cf3f6c8.
⛔ Files ignored due to path filters (1)
js/formidable.min.js is excluded by !**/*.min.js
📒 Files selected for processing (4)
classes/models/fields/FrmFieldUrl.php
js/formidable.js
tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js
tests/phpunit/fields/test_FrmFieldValidate.php
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.
📝 Walkthrough
Walkthrough
URL validation now accepts non-ASCII characters in hostnames in PHP and JavaScript. Tests cover internationalized domains, malformed URLs, raw Latin-1 host bytes, regex parity, and form-builder validation behavior.
Changes
Internationalized URL validation
Layer / File(s)
Summary
URL validation rules and unit coverage classes/models/fields/FrmFieldUrl.php, js/formidable.js, tests/phpunit/fields/test_FrmFieldValidate.php
PHP and JavaScript hostname patterns accept non-ASCII characters. PHPUnit coverage validates internationalized domains, malformed URLs, raw Latin-1 host bytes, and parity between JavaScript artifacts.
Form builder validation flow tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js
The Cypress test rejects a dotless internationalized hostname and accepts https://ernährung.ch with JavaScript validation enabled.
This localized change expands URL validation to support internationalized domain names while preserving existing sanitization and scheme checks; no actionable merge-blocking risk remains beyond normal checks and review.
We reviewed changes in 3b3bf52...df88b2d on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.
Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
$this->assertStringContainsString( '\u0080-\uFFFF', $contents, 'The JS host pattern is missing the code unit range in ' . $name );
$this->assertStringNotContainsString( '\x80-\xff', $contents, 'The PHP byte range was copied into ' . $name . '. JS matches UTF-16 code units, so that would reject the Cyrillic and CJK hosts the server accepts.' );
$this->assertStringContainsString( '\u0080-\uFFFF', $contents, 'The JS host pattern is missing the code unit range in ' . $name );
$this->assertStringNotContainsString( '\x80-\xff', $contents, 'The PHP byte range was copied into ' . $name . '. JS matches UTF-16 code units, so that would reject the Cyrillic and CJK hosts the server accepts.' );
$this->assertStringNotContainsString( '[\da-z\.-]', $contents, 'The old ASCII-only host class is still present in ' . $name );
$this->assertSame( 1, $matched, 'Could not find the url host class in js/formidable.js' );
$this->assertStringContainsString( $matches[0], file_get_contents( $minified ), 'js/formidable.min.js is stale. Rebuild it so it carries the same url host class as js/formidable.js.' );
PHPCS: two assertion messages in test_FrmFieldValidate.php exceeded the
180 character limit (SlevomatCodingStandard.Files.LineLength). Shortened
them; the detail they carried is already in the method docblocks.
DeepSource JS-0117 wanted the u flag on the JS host pattern, which uses
unicode escapes. Adding it required widening the class to a code point
range, since under the u flag the old code unit range would no longer
match astral characters that the PHP side accepts as bytes. Verified in
node: the u variant is identical to the previous one on all 15 sample
urls and across 640 generated ASCII cases, and an astral host still
matches, so PHP and JS stay in step.
DeepSource JS-R1004: four backtick strings in the new Cypress block had
no interpolation. Converted to plain strings.
The parity test needle and the explanatory comment were updated to match
the new JS form. The PHP pattern deliberately still has no u modifier,
because preg_match() returns false on malformed UTF-8 and the negated
result would report valid Latin-1 input as invalid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI status: 18 green, 2 red, and neither red one comes from this PR
Green, including the parts that matter here: PHPUnit on PHP 7.4/WP 6.9 and PHP 8/WP 6.9, PHPCS,
PHPStan, Psalm, Mago, ESLint, Oxlint, PHP CS Fixer, Rector, Stylelint, Spell Check, DeepSource:
JavaScript, DeepScan, Scrutinizer, CodeRabbit.
The new Cypress case passed:
✓ should accept an internationalized domain name in a Website/URL field (8398ms)
Spec Ran: Forms/fieldsInFormBuilder.cy.js (green)
That is the end-to-end proof that the committed js/formidable.min.js was actually rebuilt, since
wp-env activation regenerates js/frm.min.js from it.
Red 1 — Cypress admin-html-validation.cy.js:no-dup-id, Duplicate ID "frm_connect_with_oauth" at #frm_strp_settings_container > div:nth-child(2) > a, on the global
settings page.
That ID lives in stripe/views/settings/connect.php:30 and stripe/js/connect_settings.js:5, both
untouched here — this PR changes five files, none of them under stripe/.
Worth flagging separately: Cypress has never actually run on master. Every historical run shows skipped, because the gate added in #3254 reads github.event.pull_request.labels, which is null on push events, and the run e2e tests label
did not exist in this repo at all until I created it to label this PR. So there is no green baseline
to compare against, and this appears to be the first Cypress execution on any PR since the gate
landed. The duplicate ID looks like a genuine pre-existing HTML-validity and accessibility bug in
the Stripe Connect settings view (the anchor is rendered in two sibling containers) rather than
anything to do with URL fields. Happy to open a separate issue for it if useful — flagging rather
than silently folding it into this PR.
Red 2 — DeepSource: PHP. All of the reported findings are pre-existing lines that DeepSource
cannot resolve, not defects introduced here. Checked individually against origin/master:
Call to an undefined method assertEmpty()/assertNotEmpty()/assertArrayHasKey() at lines 58, 76,
78, 183, 198 — every one of those lines is unchanged master code. DeepSource is not resolving
PHPUnit's TestCase base class.
Access to an undefined property $factory at lines 16, 20, 145, 155, 170 — also unchanged master
code; $factory comes from the WP test-case base class.
Cannot access property $required on array|int|object at FrmFieldUrl.php:89 — this is the
pre-existing } elseif ( $this->field->required == '1' ... line, byte-identical to master's line
88. It only moved down one line because the fix adds a comment above it.
DeepSource: JavaScript went green after this PR's second commit, which addressed the two findings
that genuinely were mine (JS-0117 wanted the u flag; JS-R1004 flagged four backtick strings
with no interpolation).
Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
Housekeeping so the review queue reflects reality.
Resolved 5 DeepSource threads — all raised against the first commit (cf3f6c8) and all genuinely
fixed in df88b2d. GitHub already marked each of them outdated, and DeepSource: JavaScript is
now success on the head commit. For the record, rather than just taking the check's word for it:
Thread
Finding
Current state
js/formidable.js:546
JS-0117, use the u flag
Pattern is now …/iu with a code point range
fieldsInFormBuilder.cy.js:258
JS-R1004, needless template string
now 'li[id="text"] a[title="Text"]'
fieldsInFormBuilder.cy.js:259
JS-R1004
now 'li[id="url"] a[title="Website/URL"]'
fieldsInFormBuilder.cy.js:280
JS-R1004
now '[id^="frm_error_field_"]'
fieldsInFormBuilder.cy.js:290
JS-R1004
now '[id^="frm_error_field_"]'
Worth noting on JS-0117 specifically: adding u was not purely cosmetic. Under the u flag the
previous code unit range would have stopped matching astral-plane characters that the PHP side
accepts as bytes, which would have introduced a fresh server-accepts / client-rejects split — the
exact bug class this PR fixes. So the range was widened to a code point range at the same time,
and verified in node as identical to the previous behaviour across 640 generated ASCII cases with
astral hosts still matching.
Left the 16 DeepSource PHP threads open deliberately. They are false positives, but they are not
mine to dismiss and a reviewer should see them. The decisive evidence is that PHP 7.4 tests in WP 6.9 and PHP 8 tests in WP 6.9 both pass: a suite cannot pass while calling undefined methods,
so Call to an undefined method assertNotEmpty() cannot be true. DeepSource is not resolving
PHPUnit's TestCase, which is also why it reports the same error on line 198 — untouched master
code. Similarly FrmFieldUrl.php:89 is master's line 88 verbatim, moved down one line by the added
comment.
If the team wants those silenced repo-wide, excluding tests/** from the PHP analyzer in .deepsource.toml would do it, but that is a policy call rather than something to fold into this PR.
Filed Strategy11/formidable-pro#6568 for the pre-existing duplicate-ID failure in the Stripe Connect settings view that
this PR's Cypress run surfaced. It includes the root cause (the view is rendered once per mode with
hardcoded IDs) and a note that current behaviour is not broken, since the click handlers use
jQuery delegated events and derive mode from the [data-test-mode] ancestor.
Babysit pass — nothing pushed, and no re-litigating the two triage comments above. One addition
only, because it answers the question a reviewer actually has to decide: can this merge with DeepSource: PHP red?
The repo has already answered that twice, this month. Both of these merged on 2026-08-13 with DeepSource: PHP failing and inline DeepSource threads open on the phpunit files they added:
#3235 — Make field create handle invalid input better
fail
9, on tests/phpunit/fields/test_FrmField.php
Same finding class in both — Call to an undefined method assertX() and Access to an undefined property $factory. So this is not a new judgement call about this PR; it is
what happens to every core PR that adds a phpunit test.
I also found the mechanical cause, which makes it unfixable from inside this diff rather than merely
a false positive. .deepsource.toml excludes **/vendor/**, so PHPUnit\Framework\TestCase is
invisible to the PHP analyzer. stubs.php does declare WP_UnitTestCase extends WP_UnitTestCase_Base extends PHPUnit\Framework\TestCase, and phpstan.neon
and psalm.xml both load that file explicitly — which is exactly why PHPStan and Psalm are green
here while DeepSource is not. DeepSource has no equivalent stub setting, so every assert*() call in
all 78 phpunit test files is unresolvable to it. Nothing this PR can change; the fix is either a
stub path or a tests/** exclusion in .deepsource.toml, and that is a repo policy call.
Cypress is unchanged from the triage above: the sole failure is the pre-existing duplicate frm_connect_with_oauth ID, now tracked as Strategy11/formidable-pro#6568. I re-read the log on the current head to confirm
it is byte-for-byte the same failure #3242 hits on an unrelated diff, which is the cleanest proof
available that it is master-side and not either PR's doing.
Still waiting on a human review — the only reviews on record are deepsource-io[bot] and coderabbitai[bot], so this is not merging itself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The Website/URL field rejects valid internationalized domain names (IDNs) whose hostname contains
accented characters. Reported by an Elite customer in Help Scout ticket 257002 for a
.chdomainspelled with an umlaut.
.chexplicitly permits accented vowels, so the domain is legitimatelyregistrable.
The cause is a single host-matching regex, duplicated in PHP and JS. Its host character class
[\da-z\.-]is ASCII-only:classes/models/fields/FrmFieldUrl.phpinFrmFieldUrl::validate()js/formidable.jsincheckUrlField()js/formidable.min.js(the literal survives verbatim, sincenpm run minimizecompiles atWHITESPACElevel)Formidable Pro has zero copies of this pattern.
Why this is safe rather than a loosening
Three things make this a consistency fix rather than a new capability:
xn---encoded hosts have alwaysvalidated, and they resolve to the identical host. Accepting the Unicode spelling adds nothing
a submitter could not already do.
host, so the field already stored accented characters happily. Only the hostname was rejected.
esc_url_raw()already preserves the bytes. WordPress's URL sanitizer explicitly allows thehigh byte range, so sanitizing was never the thing rejecting these values.
Sanitizing, escaping, storage and the scheme allowlist are all untouched. Chrome's native
type="url"constraint already accepts every IDN form here, so the browser was not the blockereither.
Verification
byte/position combinations and are byte-for-byte identical. No previously accepted or rejected
ASCII url changes behaviour. This was brute-forced, not sampled.
javascript:,data:,vbscript:,<script>and CRLF-headerinputs all still fail exactly as before. The only behavioural delta across the whole matrix is
the IDN cases.
Cyrillic and CJK, and rejects 8 malformed hosts.
The trap worth knowing about in review
The two regexes deliberately do not use the same character range:
/umodifier), so the raw byte range covers all non-ASCII.\u0080-\uFFFF. The PHP byte range would cover anumlaut but not Cyrillic or CJK.
Copying one literal into both would accept those hosts server side while silently rejecting them in
the browser, which is the same class of split this ticket is about.
test_url_field_js_regex_parity()asserts the two JS files carry the code unit form and never the PHP one.
/uis also deliberately absent from the PHP pattern:preg_match()returnsfalseon invalidUTF-8, and because the result is negated that would report valid Latin-1 input as invalid.
test_url_non_utf8_host_byte()guards this, with a precondition so it cannot pass vacuously.One more subtlety, confirmed empirically: appending the range after the trailing hyphen
(
[\da-z\.-\x80-\xff]) is not a syntax error. PCRE reads it as the range0x2E-0x80, whichsilently admits
/ ? : @ < > [inside the hostname. The negative rowshttps://a/b.comandhttps://a?b.comexist to fail if anyone ever reorders the class that way.Tests
test_url_idn_validation()- 11 must-pass values (accented Latin, Cyrillic, CJK, uppercasenon-ASCII, punycode, ASCII baseline,
localhost, non-ASCII in path/query/fragment, and theno-scheme form) and 4 must-fail values.
test_url_non_utf8_host_byte()- the/uguard.test_url_field_js_regex_parity()- asserts the rule rather than pinning the literal, and checksthe minified artifact carries the same host class as its source. Mutation-proven: green on the
real files, red when
formidable.min.jsis reverted alone, red when the PHP range is copied intothe JS source.
expected_format_errors()table.it()with "Validate this form with javascript" enabled, so it exercises thecommitted minified artifact through a real browser rather than passing via the server-side path.
php -landnode --checkare clean. phpcs, phpstan, phpunit and eslint needcomposer installplus a WordPress test database, which is not set up on this machine, so they run here in CI under
the labels below. Reviewer note: core CI is label-gated, so this PR carries
run tests,run analysisandrun e2e tests. Without them onlytyposandpsalmwould run.Manual QA note
The front end serves the combined
js/frm.min.js, which is rebuilt fromjs/formidable.min.jsonly by
FrmAppHelper::save_combined_js()(called fromFrmMigrateandFrmAddon). When testingthis by hand, delete
js/frm.min.jsor setSCRIPT_DEBUG, otherwise a stale combined file willserve the old regex and the fix will look like it did nothing.
Deliberately not fixed here
These are pre-existing and adjacent, left out to keep the change narrow. Each is a separate
behaviour decision, and they are grouped into a follow-up issue rather than lost:
ftp:,mailto:,news:,feed:andtelnet:are rejected by the format check even though thescheme allowlist a few lines above explicitly admits them - a real internal contradiction.
https://-.-andhttps://....passes, so this check was never a strong qualitygate in the first place.
Help Scout ticket 257002.
Summary by CodeRabbit
Bug Fixes
Tests