Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ The format follows [Keep a Changelog] (https://keepachangelog.com/en/1.1.0/).
- **Certificate forms: no silent failure when the browser cannot run them** (#2030). The form posts with `method="post"`, so without JavaScript the name, e-mail and CPF/RF stay out of the URL and the logs, and a notice says JavaScript is off. A form whose scripts, PDF libraries or captcha and nonce refresh did not load in 10 s shows "The form did not load completely" with a Reload button instead of a form that fails on submit.
- **GPS forms no longer show "Location blocked" twice** (#2030). On a cached page, refreshing the captcha and security codes re-ran the checks while the browser was still answering the permission query, and both answers drew a banner. A stale answer is now dropped, and a banner replaces one with the same purpose.
- **"Location blocked" only when the browser really blocks it** (#2048). Opera for Android reports the location as blocked while it gives the position, so GPS forms showed a false block, the check-in a false error and the device test "Blocked". A reported block is now confirmed by one quick read; the position is discarded.
- **Scripts run again for visitors on sites that minify with LiteSpeed Cache** (#2056). Its minifier cut a regex literal ending in an escaped slash, which #2053 added, so the combined file stopped parsing and the device test stayed at "Checking…"; the three such literals are rewritten and a test refuses the shape.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion assets/js/ffc-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
|| /WhatsApp/i.test(ua)
|| /LinkedInApp/i.test(ua)
|| /BytedanceWebView|musical_ly/i.test(ua)
|| /Line\//i.test(ua);
|| /Line\/\d/i.test(ua);
},

/** @returns {boolean} Samsung Internet. */
Expand Down
2 changes: 1 addition & 1 deletion assets/js/ffc-core.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/js/ffc-device-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
['WhatsApp', /WhatsApp/i],
['TikTok', /BytedanceWebView|musical_ly/i],
['LinkedIn', /LinkedInApp/i],
['LINE', /Line\//]
['LINE', /Line\/\d/]
];

/**
Expand Down
2 changes: 1 addition & 1 deletion assets/js/ffc-device-check.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/js/ffc-preflight.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@
* they are, with the menu instructions still on screen.
*/
function openInChrome() {
var url = window.location.href.replace(/^https?:\/\//, '');
var url = window.location.href.replace(/^https?:\/{2}/, '');
try {
window.location.href = 'intent://' + url + '#Intent;scheme=https;package=com.android.chrome;end';
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion assets/js/ffc-preflight.min.js

Large diffs are not rendered by default.

117 changes: 117 additions & 0 deletions tests/Unit/MinifierSafeRegexTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php
/**
* Refuses a JavaScript regular expression literal that ends in an escaped
* slash, the shape a server-side minifier cuts in half.
*
* LiteSpeed Cache minifies and combines a site's scripts for visitors who
* are not logged in, through matthiasmullie/minify. That minifier reads
* `/Line\//` followed by `]` or a line break as a regex that closes at the
* escaped slash, then takes the real closing slash for the start of a
* comment, and drops the rest of the line. The combined file no longer
* parses, so every script in it stops: on the testing site the device test
* stayed at "Checking…" for every visitor, while a logged-in administrator,
* who gets the scripts uncombined, saw it work (#2056).
*
* The rule is wider than the measured trigger on purpose. Which characters
* may follow the literal safely is the minifier's business and changes
* with its version; a literal that does not end in `\/` cannot be cut that
* way at all, and `\/{2}` or `\/\d` always say the same thing.
*
* @package FreeFormCertificate\Tests\Unit
*/

declare(strict_types=1);

namespace FreeFormCertificate\Tests\Unit;

use PHPUnit\Framework\TestCase;

/**
* @coversNothing
*/
class MinifierSafeRegexTest extends TestCase {

/**
* The plugin's own readable scripts: what the minifier receives.
*
* @return array<int, string>
*/
private static function sources(): array {
$files = array();
foreach ( (array) glob( dirname( __DIR__, 2 ) . '/assets/js/*.js' ) as $file ) {
if ( ! str_ends_with( (string) $file, '.min.js' ) ) {
$files[] = (string) $file;
}
}
sort( $files );
return $files;
}

/**
* Lines of code that hold a regex literal ending in an escaped slash.
* Comments are skipped: the minifier drops them, so they cannot break.
*
* @param string $source JavaScript source.
* @return array<int, int> Line numbers, 1-based.
*/
private static function offending_lines( string $source ): array {
$found = array();
foreach ( explode( "\n", $source ) as $i => $line ) {
$code = ltrim( $line );
if ( str_starts_with( $code, '*' ) || str_starts_with( $code, '//' ) || str_starts_with( $code, '/*' ) ) {
continue;
}
// An escaped slash directly followed by the closing slash and
// optional flags, outside a comment opener (`\//` is never `//`).
if ( preg_match( '#\\\\/\/[dgimsuyv]*(?![\w/*])#', $line ) ) {
$found[] = $i + 1;
}
}
return $found;
}

public function test_no_regex_literal_ends_in_an_escaped_slash(): void {
$offenders = array();
foreach ( self::sources() as $file ) {
foreach ( self::offending_lines( (string) file_get_contents( $file ) ) as $line ) {
$offenders[] = basename( $file ) . ':' . $line;
}
}

$this->assertSame(
array(),
$offenders,
'A regex literal ends in an escaped slash, which the LiteSpeed minifier cuts in half (#2056). Write `\/{2}` or `\/\d` instead.'
);
}

/**
* The scan must reach every script: a recount by another traversal.
*/
public function test_the_scan_reads_every_readable_script(): void {
$dir = dirname( __DIR__, 2 ) . '/assets/js';
$recount = array();
foreach ( (array) scandir( $dir ) as $name ) {
if ( preg_match( '/^(?!.*\.min\.js$).+\.js$/', (string) $name ) ) {
$recount[] = $dir . '/' . $name;
}
}
sort( $recount );

$this->assertNotEmpty( $recount );
$this->assertSame( $recount, self::sources() );
}

/**
* Pins what the scan flags and what it leaves alone.
*/
public function test_canary(): void {
$this->assertSame( array( 1 ), self::offending_lines( "var a = [['LINE', /Line\\//]];" ) );
$this->assertSame( array( 1 ), self::offending_lines( "x = /a\\//i.test(u);" ) );
$this->assertSame( array( 1 ), self::offending_lines( "u.replace(/^https?:\\/\\//, '');" ) );
$this->assertSame( array(), self::offending_lines( "var a = [['LINE', /Line\\/\\d/]];" ) );
$this->assertSame( array(), self::offending_lines( "u.replace(/^https?:\\/{2}/, '');" ) );
$this->assertSame( array(), self::offending_lines( "var url = 'https://example.org/';" ) );
$this->assertSame( array(), self::offending_lines( " * `/Line\\//` is the old shape." ) );
}
}
Loading