Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions classes/models/fields/FrmFieldUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ public function validate( $args ) {

$errors = array();

// Validate the url format
if ( $value && ! preg_match( '/^http(s)?:\/\/(?:localhost|(?:[\da-z\.-]+\.[\da-z\.-]+))/i', $value ) ) {
// Validate the url format. The host class allows \x80-\xff so internationalized domain names pass.
// Byte range by design, and no /u modifier: with /u, preg_match() returns false on invalid UTF-8.
if ( $value && ! preg_match( '/^http(s)?:\/\/(?:localhost|(?:[\da-z\x80-\xff\.-]+\.[\da-z\x80-\xff\.-]+))/i', $value ) ) {
$errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $this->field, 'invalid' );
} elseif ( $this->field->required == '1' && ! $value ) { // phpcs:ignore Universal.Operators.StrictComparisons

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot access property $required on array|int|object


The property you are trying to access is not defined and will cause unexpected behavior when used.

$errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $this->field, 'blank' );
Expand Down
5 changes: 4 additions & 1 deletion js/formidable.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,10 @@ function frmFrontFormJS() {
let fieldID;
const url = field.value;

if ( url !== '' && ! /^http(s)?:\/\/(?:localhost|(?:[\da-z\.-]+\.[\da-z\.-]+))/i.test( url ) ) {
// Keep in sync with FrmFieldUrl::validate(), but the ranges differ by design: JS matches UTF-16
// code units, so it uses the u flag and a code point range where the PHP side matches raw
// UTF-8 bytes. PHP must NOT gain /u: preg_match() returns false on malformed UTF-8.
if ( url !== '' && ! /^http(s)?:\/\/(?:localhost|(?:[\da-z\u0080-\u{10FFFF}\.-]+\.[\da-z\u0080-\u{10FFFF}\.-]+))/iu.test( url ) ) {
fieldID = getFieldId( field, true );
if ( ! ( fieldID in errors ) ) {
errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
Expand Down
2 changes: 1 addition & 1 deletion js/formidable.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,48 @@ describe( 'Fields in the form builder', () => {
cy.go( 'back' );
} );

it( 'should accept an internationalized domain name in a Website/URL field', () => {
cy.openForm();

cy.log( 'Create a text field and a Website/URL field' );
cy.get( 'li[id="text"] a[title="Text"]' ).click( { force: true } );
cy.get( 'li[id="url"] a[title="Website/URL"]' ).click( { force: true } );

cy.log( 'Update form' );
cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click( { force: true } );

cy.log( "Enabling the 'Validate this form with javascript' setting" );
cy.xpath( "//ul[@class='frm_form_nav']//a[contains(text(),'Settings')]" ).should( 'contain', 'Settings' ).click();
cy.get( '#js_validate' ).click( { force: true } );
cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click( { force: true } );

cy.log( 'Click on Preview - Blank Page' );
cy.get( '#frm-previewDrop', { timeout: 5000 } ).should( 'contain', 'Preview' ).click();
cy.get( '.preview > .frm-dropdown-menu > :nth-child(1) > a' ).should( 'contain', 'On Blank Page' ).invoke( 'removeAttr', 'target' ).click();

/**
* A host with no dot must still be rejected. This proves the javascript validator really is
* running on this field, so the assertion further down cannot pass for the wrong reason.
*/
cy.log( 'A host with no dot is still rejected' );
cy.get( '[id^="field_"]' ).filter( 'input' ).eq( 1 ).type( 'münchen' );
cy.get( '[id^="field_"]' ).filter( 'input' ).eq( 0 ).click();
cy.get( '[id^="frm_error_field_"]' ).should( 'exist' );

/**
* An accented host must be accepted. The regex runs out of the committed js/formidable.min.js,
* which is rebuilt into js/frm.min.js when the plugin is activated, so a stale minified
* artifact fails right here.
*/
cy.log( 'An internationalized domain name is accepted' );
cy.get( '[id^="field_"]' ).filter( 'input' ).eq( 1 ).clear().type( 'https://ernährung.ch' );
cy.get( '[id^="field_"]' ).filter( 'input' ).eq( 0 ).click();
cy.get( '[id^="frm_error_field_"]' ).should( 'not.exist' );

cy.log( 'Navigate back to the formidable form page' );
cy.go( 'back' );
} );

afterEach( () => {
cy.log( 'Teardown - Save the form and delete it' );
cy.get( "a[aria-label='Close']", { timeout: 10000 } ).click( { force: true } );
Expand Down
117 changes: 117 additions & 0 deletions tests/phpunit/fields/test_FrmFieldValidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ protected function expected_format_errors() {
'value' => 'http://',
'invalid' => false,
),
array(
'type' => 'url',
'value' => 'https://ernährung.ch',
'invalid' => false,
),
array(
'type' => 'url',
'value' => 'https://пример.рф',
'invalid' => false,
),
array(
'type' => 'url',
'value' => 'https://a/b.com',
'invalid' => true,
),
);
}

Expand Down Expand Up @@ -183,6 +198,108 @@ public function test_url_value() {
$this->assertArrayHasKey( 'field' . $field->id, $errors, 'http:// passed required validation ' . print_r( $errors, 1 ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertArrayHasKey()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* Internationalized domain names must pass validation.
*
* The host pattern in FrmFieldUrl::validate() matches UTF-8 bytes, so non-ASCII hosts are
* accepted. These are real registrable domains - .ch permits accented vowels - and the punycode
* spelling of the same domain has always passed, so accepting these adds no new capability.
*
* @covers FrmFieldUrl::validate
*/
public function test_url_idn_validation() {
$field = $this->factory->field->get_object_by_id( $this->get_field_key( 'url' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access to an undefined property test_FrmFieldValidate::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access to an undefined property WP_UnitTest_Factory::$field


The property you are trying to access is not defined and will cause unexpected behavior when used.

$this->assertNotEmpty( $field );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertNotEmpty()


The method you are trying to call is not defined, which can result in a fatal error.


$should_pass = array(
'https://ernährung.ch',
'https://münchen.de',
'https://café.fr',
'https://пример.рф',
'https://例え.jp',
'https://ÄPFEL.DE',
'https://xn--ernhrung-2za.ch',
'https://example.com',
'http://localhost',
'https://ernährung.ch/über-uns?q=grüße#süß',
'ernährung.ch',
);

foreach ( $should_pass as $url ) {
$errors = $this->check_single_value( array( $field->id => $url ) );
$this->assertArrayNotHasKey( 'field' . $field->id, $errors, 'A valid url failed validation: ' . $url );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertArrayNotHasKey()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* The last two must fail even though the class now allows non-ASCII: the pattern still
* requires a dotted host, and a hyphen placed after the byte range would turn it into the
* range 0x2E-0x80 and let path and query characters through.
*/
$should_fail = array(
'münchen',
'https://ä',
'https://a/b.com',
'https://a?b.com',
);

foreach ( $should_fail as $url ) {
$errors = $this->check_single_value( array( $field->id => $url ) );
$this->assertArrayHasKey( 'field' . $field->id, $errors, 'An invalid url passed validation: ' . $url );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertArrayHasKey()


The method you are trying to call is not defined, which can result in a fatal error.

}
}

/**
* A raw Latin-1 host byte must still validate.
*
* This guards against adding the /u modifier to the host pattern. With /u, preg_match() returns
* false on invalid UTF-8, and because the result is negated the value would be reported invalid.
*
* @covers FrmFieldUrl::validate
*/
public function test_url_non_utf8_host_byte() {
$field = $this->factory->field->get_object_by_id( $this->get_field_key( 'url' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access to an undefined property test_FrmFieldValidate::$factory


The property you are trying to access is not defined and will cause unexpected behavior when used.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access to an undefined property WP_UnitTest_Factory::$field


The property you are trying to access is not defined and will cause unexpected behavior when used.

$this->assertNotEmpty( $field );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertNotEmpty()


The method you are trying to call is not defined, which can result in a fatal error.


$url = "https://ex\xE4mple.com";

// Without this the assertion below would pass vacuously if the byte were stripped first.
$this->assertNotEmpty( esc_url_raw( $url ), 'The Latin-1 host byte did not survive sanitizing, so this test proves nothing.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertNotEmpty()


The method you are trying to call is not defined, which can result in a fatal error.


$errors = $this->check_single_value( array( $field->id => $url ) );
$this->assertArrayNotHasKey( 'field' . $field->id, $errors, 'A Latin-1 host byte failed validation, which suggests the /u modifier was added to the host pattern.' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertArrayNotHasKey()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* The JS copy of the host pattern must stay in step with the PHP one, and the minified artifact
* must be rebuilt whenever the source changes.
*
* There is no JS engine in this suite, so this asserts the rules on the source rather than
* running the regex: the code unit range is present, the PHP byte range was not copied across by
* mistake, the old ASCII-only class is gone, and the minified file carries the same class.
*
* @covers FrmFieldUrl::validate
*/
public function test_url_field_js_regex_parity() {
$source = FrmAppHelper::plugin_path() . '/js/formidable.js';
$minified = FrmAppHelper::plugin_path() . '/js/formidable.min.js';

foreach ( array( $source, $minified ) as $file ) {
$this->assertFileExists( $file );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertFileExists()


The method you are trying to call is not defined, which can result in a fatal error.


$contents = file_get_contents( $file );
$name = basename( $file );

$this->assertStringContainsString( '\u0080-\u{10FFFF}', $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 . '; that rejects Cyrillic and CJK hosts.' );
$this->assertStringNotContainsString( '[\da-z\.-]', $contents, 'The old ASCII-only host class is still present in ' . $name );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertStringNotContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

}

// The host class in the source must appear verbatim in the minified artifact.
$matched = preg_match( '/\[\\\\da-z[^\]]*\]/', file_get_contents( $source ), $matches );
$this->assertSame( 1, $matched, 'Could not find the url host class in js/formidable.js' );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmFieldValidate::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertStringContainsString( $matches[0], file_get_contents( $minified ), 'js/formidable.min.js is stale. Rebuild it.' );
}

/**
* @covers FrmFieldEmail::validate
*/
Expand Down
Loading