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 @@ -37,6 +37,7 @@ The format follows [Keep a Changelog] (https://keepachangelog.com/en/1.1.0/).
- **Certificate forms check the visitor's browser first** (#2030). Every form runs the pre-flight: an app's own browser is asked to switch (with "Continue anyway"), one missing what the form needs is stopped, cookies are checked on every form and ALTCHA-only needs HTTPS. Settings → Certificates → Browser check turns it off.
- **The public CSV download checks the browser too** (#2030). An app's own browser is asked to open the page in the phone's browser, with "Continue anyway", as on a certificate form, under the same Settings → Certificates → Browser check; the in-app banner it had is replaced.
- **The verification page can test the visitor's device** (#2040). `[ffc_verification]` with `?ffc_device_check=1` shows, in the site's theme, each check a form runs (secure connection, browser, cookies, location permission and precision, PDF) and copies the results for support; the position is never shown. The page links there from its footer.
- **The device test names the browser and its version** (#2052). "Ordinary browser" becomes, for instance, "Chrome (v. 129.0.6668.81)", "Brave / Chromium (v. …)" or "Opera 103 / Chromium (v. 117.…)", read from the brands a Chromium browser declares and then from the user agent; an app's own browser names the app, and the PDF row says what it checked.

### Fixed

Expand Down
196 changes: 191 additions & 5 deletions assets/js/ffc-device-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,194 @@
return os === 'android' ? text('deviceAndroid', 'Android') : text('deviceDesktop', 'computer');
}

/** Apps whose own browser marks the user agent, by the name shown. */
var APPS = [
['Instagram', /Instagram/i],
['Facebook', /FBAN|FBAV|FBIOS/i],
['WhatsApp', /WhatsApp/i],
['TikTok', /BytedanceWebView|musical_ly/i],
['LinkedIn', /LinkedInApp/i],
['LINE', /Line\//]
];

/**
* Chromium brands by the name shown, in the order they win: a derivative
* lists "Chromium" too, and Chrome lists only "Google Chrome" and
* "Chromium" (#2052).
*/
var BRANDS = [
['Brave', 'Brave'],
['Microsoft Edge', 'Edge'],
['Opera', 'Opera'],
['Samsung Internet', 'Samsung Internet'],
['Google Chrome', 'Chrome']
];

function uaVersion(ua, re) {
var m = re.exec(ua);
return m ? m[1] : '';
}

/**
* A version as the browser gave it, without the zeros a reduced user
* agent pads it with ("129.0.0.0" says only "129").
*
* @param {string} version
* @returns {string}
*/
function trimVersion(version) {
return String(version || '').replace(/(\.0)+$/, '');
}

function labelled(name, version) {
return version ? name + ' (v. ' + trimVersion(version) + ')' : name;
}

/**
* The name and version of a Chromium-based browser, from the brands it
* declares. Its own number goes before the engine's where it has one
* (Opera, Samsung Internet), read from the user agent.
*
* @param {Array<{brand: string, version: string}>} brands
* @param {string} ua
* @returns {string} '' when no brand names the browser.
*/
function fromBrands(brands, ua) {
var versionOf = function (brand) {
for (var i = 0; i < brands.length; i++) {
if (brands[i].brand === brand) {
return brands[i].version;
}
}
return null;
};
var engine = versionOf('Chromium');
for (var i = 0; i < BRANDS.length; i++) {
var own = versionOf(BRANDS[i][0]);
if (own === null) {
continue;
}
var name = BRANDS[i][1];
if ('Chrome' === name) {
return labelled('Chrome', own);
}
if ('Opera' === name) {
name += ' ' + (uaVersion(ua, /(?:OPR|Opera)\/(\d+)/) || trimVersion(own).split('.')[0]);
} else if ('Samsung Internet' === name) {
name += ' ' + (uaVersion(ua, /SamsungBrowser\/(\d+)/) || trimVersion(own).split('.')[0]);
}
return labelled(name + ' / Chromium', engine || '');
}
return engine !== null ? labelled('Chromium', engine) : '';
}

/**
* The name and version from the user agent alone: Firefox, Safari, every
* browser on iOS (all of them WebKit), and a Chromium browser that does
* not declare its brands.
*
* @param {string} ua
* @param {boolean} ios
* @returns {string} '' for a browser this does not know.
*/
function fromUserAgent(ua, ios) {
var v;
if (ios) {
if ((v = uaVersion(ua, /CriOS\/([\d.]+)/))) {
return labelled('Chrome / WebKit', v);
}
if ((v = uaVersion(ua, /FxiOS\/([\d.]+)/))) {
return labelled('Firefox / WebKit', v);
}
if ((v = uaVersion(ua, /EdgiOS\/([\d.]+)/))) {
return labelled('Edge / WebKit', v);
}
v = uaVersion(ua, /Version\/([\d.]+).*Safari/);
return v ? labelled('Safari', v) : '';
}
var chromium = uaVersion(ua, /(?:Chrome|Chromium)\/([\d.]+)/);
if ((v = uaVersion(ua, /Edg(?:A)?\/([\d.]+)/))) {
return labelled('Edge / Chromium', chromium || v);
}
if ((v = uaVersion(ua, /(?:OPR|Opera)\/(\d+)/))) {
return labelled('Opera ' + v + ' / Chromium', chromium);
}
if ((v = uaVersion(ua, /SamsungBrowser\/(\d+)/))) {
return labelled('Samsung Internet ' + v + ' / Chromium', chromium);
}
if ((v = uaVersion(ua, /Firefox\/([\d.]+)/))) {
return labelled('Firefox', v);
}
if ((v = uaVersion(ua, /Chromium\/([\d.]+)/))) {
return labelled('Chromium', v);
}
if (chromium) {
return labelled('Chrome', chromium);
}
v = uaVersion(ua, /Version\/([\d.]+).*Safari/);
return v ? labelled('Safari', v) : '';
}

/**
* The browser's name and version (#2052), for the person reading the
* results: no decision reads it, which is why it lives here and not in
* FFC.Platform. The brands a Chromium browser declares come first, with
* the full version when it answers; the user agent is the fallback.
*
* @param {object} platform FFC.Platform.
* @returns {Promise<string>} '' for a browser this does not know.
*/
function browserName(platform) {
var ua = platform.ua();
var fallback = function () {
return fromUserAgent(ua, platform.ios());
};
var data = navigator.userAgentData;
if (!data || !Array.isArray(data.brands) || !data.brands.length) {
return Promise.resolve(fallback());
}
var brandsOnly = function () {
return fromBrands(data.brands, ua) || fallback();
};
if (typeof data.getHighEntropyValues !== 'function') {
return Promise.resolve(brandsOnly());
}
try {
return Promise.resolve(data.getHighEntropyValues(['fullVersionList'])).then(function (values) {
var list = values && values.fullVersionList;
return (Array.isArray(list) && list.length && fromBrands(list, ua)) || brandsOnly();
}, brandsOnly);
} catch (e) {
return Promise.resolve(brandsOnly());
}
}

/** The app whose own browser this is, or ''. */
function appName(ua) {
for (var i = 0; i < APPS.length; i++) {
if (APPS[i][1].test(ua)) {
return APPS[i][0];
}
}
return '';
}

function checkBrowser(platform) {
var where = device(platform);
if (platform.inApp()) {
var app = appName(platform.ua());
set('browser', 'warn', text('browserInApp', 'An app\'s own browser (%s): the location and downloads often fail. Open the page in the phone\'s browser.')
.replace('%s', app ? app + ', ' + where : where));
return;
}
set('browser', 'ok', text('browserOk', 'Ordinary browser (%s)').replace('%s', where));
browserName(platform).then(function (name) {
if (name) {
set('browser', 'ok', text('browserNamed', '%1$s — %2$s').replace('%1$s', name).replace('%2$s', where));
}
});
}

function checkLocationPermission() {
if (!navigator.geolocation) {
set('location', 'fail', text('noGeolocation', 'This browser cannot give a location.'));
Expand Down Expand Up @@ -150,9 +338,7 @@
? text('secureYes', 'Yes')
: text('secureNo', 'No: the browser gives no location here, and forms that ask for it will stop.'));

set('browser', platform.inApp() ? 'warn' : 'ok', (platform.inApp()
? text('browserInApp', 'An app\'s own browser (%s): the location and downloads often fail. Open the page in the phone\'s browser.')
: text('browserOk', 'Ordinary browser (%s)')).replace('%s', device(platform)));
checkBrowser(platform);

set('cookies', preflight.cookiesWork() ? 'ok' : 'fail', preflight.cookiesWork()
? text('cookiesYes', 'Allowed')
Expand All @@ -161,7 +347,7 @@
var missing = preflight.missingFeatures();
set('pdf', missing.length ? 'fail' : 'ok', missing.length
? text('pdfNo', 'Not supported: missing %s.').replace('%s', missing.join(', '))
: text('pdfYes', 'Supported'));
: text('pdfYes', 'Supported: this browser can build and save the PDF.'));

checkLocationPermission();
set('accuracy', 'info', text('accuracyIdle', 'Press "Test my location".'));
Expand All @@ -184,5 +370,5 @@
init();
}

window.FFCDeviceCheck = { summary: summary };
window.FFCDeviceCheck = { summary: summary, browserName: browserName };
})();
2 changes: 1 addition & 1 deletion assets/js/ffc-device-check.min.js

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

Loading
Loading