diff --git a/src/helpers/package.json b/src/helpers/package.json index fe99fb2..092ffc9 100644 --- a/src/helpers/package.json +++ b/src/helpers/package.json @@ -1,9 +1,9 @@ { "title": "UAParser.js Helpers", "name": "@ua-parser-js/helpers", - "version": "2.0.0-alpha.3", + "version": "0.0.1", "author": "Faisal Salman ", - "description": "Helpers for UAParser.js", + "description": "A collection of utility methods for UAParser.js", "main": "ua-parser-helpers.js", "module": "ua-parser-helpers.mjs", "scripts" : { diff --git a/src/helpers/readme.md b/src/helpers/readme.md new file mode 100644 index 0000000..e23abc2 --- /dev/null +++ b/src/helpers/readme.md @@ -0,0 +1,65 @@ +# @ua-parser-js/helpers + +This package contains a collection of utility methods for [UAParser.js](https://github.com/faisalman/ua-parser-js) + +```sh +npm i @ua-parser-js/helpers +``` + +### * `isFrozenUA(ua:string):boolean` + +Check whether a user-agent string match with [frozen user-agent pattern](https://www.chromium.org/updates/ua-reduction/) + +### * `unfreezeUA():Promise` + +construct new unfreezed user-agent string using real data from client hints + +## Code Example + +```js +import { isFrozenUA } from '@ua-parser-js/helpers'; + +const regularMobileUA = "Mozilla/5.0 (Linux; Android 9; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36"; +const freezedMobileUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36"; + +console.log(isFrozenUA(regularMobileUA)); +// false + +console.log(isFrozenUA(freezedMobileUA)); +// true +``` + +```js +import { unfreezeUA } from '@ua-parser-js/helpers'; + +/* + Suppose we're in a browser having this client hints data: + + { + fullVersionList: [ + { + brand: 'New Browser', + version: '110.1.2.3' + }, + { + brand: 'Chromium', + version: '110.1.2.3' + }, + { + brand: 'Not(A:Brand', + version: '110' + } + ], + platform: 'Windows', + platformVersion: '13.0.0' + } + + And a freezed user-agent: + + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36' +*/ + +unfreezeUA(); + +// 'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36' +``` \ No newline at end of file diff --git a/src/helpers/ua-parser-helpers.d.ts b/src/helpers/ua-parser-helpers.d.ts new file mode 100644 index 0000000..61e041f --- /dev/null +++ b/src/helpers/ua-parser-helpers.d.ts @@ -0,0 +1,2 @@ +export function isFrozenUA(ua: string): boolean; +export function unfreezeUA(): Promise; \ No newline at end of file diff --git a/src/helpers/ua-parser-helpers.js b/src/helpers/ua-parser-helpers.js index eaa3171..45933eb 100644 --- a/src/helpers/ua-parser-helpers.js +++ b/src/helpers/ua-parser-helpers.js @@ -1,11 +1,11 @@ /////////////////////////////////////////////// -/* Helpers for UAParser.js +/* A collection of utility methods for UAParser.js https://github.com/faisalman/ua-parser-js Author: Faisal Salman MIT License */ ////////////////////////////////////////////// -/*jshint esversion: 6 */ +/*jshint esversion: 11 */ /* # Reference: @@ -33,8 +33,63 @@ - "" (empty string for Tablets & Desktop) */ -const isFrozenUA = ua => /Mozilla\/5\.0 \((Windows NT 10\.0; Win64; x64|Macintosh; Intel Mac OS X 10_15_7|X11; Linux x86_64|X11; CrOS x86_64 14541\.0\.0|Fuchsia|Linux; Android 10; K)\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0 (Mobile )?Safari\/537\.36/.test(ua); +const isFrozenUA = ua => /^Mozilla\/5\.0 \((Windows NT 10\.0; Win64; x64|Macintosh; Intel Mac OS X 10_15_7|X11; Linux x86_64|X11; CrOS x86_64 14541\.0\.0|Fuchsia|Linux; Android 10; K)\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0 (Mobile )?Safari\/537\.36$/.test(ua); + +const unfreezeUA = async () => { + if (!navigator) { + throw new Error('Currently only support browser environment'); + } else { + let ua = navigator.userAgent; + if (navigator.userAgentData && isFrozenUA(ua)) { + const ch = await navigator.userAgentData.getHighEntropyValues(['architecture', 'bitness', 'fullVersionList', 'model', 'platform', 'platformVersion', 'wow64']); + switch (ch.platform) { + case 'Windows': + let [major, minor] = ch.platformVersion?.split('.').map(i => parseInt(i, 10)); + let osReplacer = (major < 1) ? + `$ 6.${minor}` : + (major >= 13) ? + `$ 11.${minor}` : + `$ 10.${minor}`; + let archReplacer = (ch.architecture == 'arm') ? + '; ARM' : + (ch.wow64) ? + '; WOW64' : + (ch.architecture == 'x86' && ch.bitness == '64') ? + '; $' : ''; + ua = ua.replace(/(?Windows NT) 10\.0/, osReplacer) + .replace(/; (?Win64; x64)/, archReplacer); + break; + case 'Android': + ua = ua.replace(/(?Android) 10; K/, `$ ${ch.platformVersion}; ${ch.model}`); + break; + case 'Linux': + case 'Chrome OS': + archReplacer = (ch.architecture == 'arm') ? + ((ch.bitness == '64') ? 'arm64' : 'arm') : + (ch.architecture == 'x86' && ch.bitness == '64') ? + '$' : 'x86'; + + ua = ua.replace(/(?x86_64)/, archReplacer); + break; + case 'macOS': + ua = ua.replace(/(?Mac OS X) 10_15_7/, `$ ${ch.platformVersion.replace(/\./, '_')}`); + break; + } + let browserReplacer = ''; + ch.fullVersionList?.forEach((browser) => { + if (!/not.a.brand/i.test(browser.brand)) { + browserReplacer += `${browser.brand}/${browser.version} `; + } + }); + if (browserReplacer) { + ua = ua.replace(/Chrome\/\d+\.0\.0\.0 /, browserReplacer); + } + } + return ua; + } +}; module.exports = { - isFrozenUA + isFrozenUA, + unfreezeUA }; \ No newline at end of file diff --git a/test/mocha-test-helpers.js b/test/mocha-test-helpers.js index b954e73..c8417d6 100644 --- a/test/mocha-test-helpers.js +++ b/test/mocha-test-helpers.js @@ -1,4 +1,4 @@ -const { isFrozenUA } = require('ua-parser-js/helpers'); +const { isFrozenUA } = require('@ua-parser-js/helpers'); const assert = require('assert'); describe('isFrozenUA', () => { diff --git a/test/playwright-test-helpers.spec.mjs b/test/playwright-test-helpers.spec.mjs new file mode 100644 index 0000000..c5bc65e --- /dev/null +++ b/test/playwright-test-helpers.spec.mjs @@ -0,0 +1,47 @@ +// @ts-check +import { test, expect } from '@playwright/test'; +import { unfreezeUA } from '@ua-parser-js/helpers'; + +test('test for unfreezeUA() method', async ({ page }) => { + + + await page.addInitScript(() => { + Object.defineProperty(navigator, 'userAgentData', { + value: { + brands: [], + platform: 'Windows', + mobile: false, + getHighEntropyValues: () => { + return Promise.resolve({ + architecture: 'x86', + bitness: '64', + fullVersionList: [ + { + brand: 'New Browser', + version: '110.1.2.3' + }, + { + brand: 'Chromium', + version: '110.1.2.3' + }, + { + brand: 'Not(A:Brand', + version: '110' + } + ], + platform: 'Windows', + platformVersion: '0.3' + }); + } + } + }); + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36' + }); + }); + await page.goto('about:blank'); + await page.addScriptTag({ path: './src/helpers/ua-parser-helpers.js' }); + // @ts-ignore + const ua = await page.evaluate(async () => await unfreezeUA()); + expect(ua).toBe('Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36'); +}); \ No newline at end of file