Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.

$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.

$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