diff --git a/src/ua-parser.js b/src/ua-parser.js index acc6fed..73e26f8 100755 --- a/src/ua-parser.js +++ b/src/ua-parser.js @@ -39,6 +39,11 @@ EMBEDDED = 'embedded', USER_AGENT = 'user-agent', UA_MAX_LENGTH = 350, + BRANDS = 'brands', + FULLVERLIST = 'fullVersionList', + PLATFORM = 'platform', + PLATFORMVER = 'platformVersion', + BITNESS = 'bitness', CH_HEADER = 'sec-ch-ua', CH_HEADER_FULL_VER_LIST = CH_HEADER + '-full-version-list', CH_HEADER_ARCH = CH_HEADER + '-arch', @@ -127,6 +132,16 @@ return /^(browser|cpu|device|engine|os)$/.test(prop); } }, + itemListToArray = function (header) { + if (!header) return undefined; + var arr = []; + var tokens = strip(/\\?\"/g, header).split(', '); + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i].split(';v='); + arr[i] = { brand : token[0], version : token[1] }; + } + return arr; + }, lowerize = function (str, rgx) { return typeof(str) === STR_TYPE ? strip((rgx ? new RegExp(rgx, 'i') : EMPTY), str.toLowerCase()) : str; }, @@ -136,6 +151,9 @@ strip = function (pattern, str) { return str.replace(pattern, EMPTY); }, + stripQuotes = function (val) { + return typeof val === STR_TYPE ? strip(/\"/g, val) : val; + }, trim = function (str, len) { if (typeof(str) === STR_TYPE) { str = strip(/^\s\s*/, str); @@ -830,31 +848,21 @@ function UAParserDataCH (uach, isHTTP_UACH) { uach = uach || {}; initialize.call(this, CH_ALL_VALUES); - var setVal = function (val) { - return typeof val === STR_TYPE ? strip(/\"/g, val) : val || undefined; - }; if (isHTTP_UACH) { - var toArray = function (header) { - if (!header) return undefined; - var arr = []; - var tokens = strip(/\\?\"/g, header).split(', '); - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i].split(';v='); - arr[i] = { brand : token[0], version : token[1] }; - } - return arr; - }; - this.brands = toArray(uach[CH_HEADER]); - this.fullVersionList = toArray(uach[CH_HEADER_FULL_VER_LIST]); - this.mobile = /\?1/.test(uach[CH_HEADER_MOBILE]); - this.model = setVal(uach[CH_HEADER_MODEL]); - this.platform = setVal(uach[CH_HEADER_PLATFORM]); - this.platformVersion = setVal(uach[CH_HEADER_PLATFORM_VER]); - this.architecture = setVal(uach[CH_HEADER_ARCH]); - this.bitness = setVal(uach[CH_HEADER_BITNESS]); + initialize.call(this, [ + [BRANDS, itemListToArray(uach[CH_HEADER])], + [FULLVERLIST, itemListToArray(uach[CH_HEADER_FULL_VER_LIST])], + [BRANDS, itemListToArray(uach[CH_HEADER])], + [MOBILE, /\?1/.test(uach[CH_HEADER_MOBILE])], + [MODEL, stripQuotes(uach[CH_HEADER_MODEL])], + [PLATFORM, stripQuotes(uach[CH_HEADER_PLATFORM])], + [PLATFORMVER, stripQuotes(uach[CH_HEADER_PLATFORM_VER])], + [ARCHITECTURE, stripQuotes(uach[CH_HEADER_ARCH])], + [BITNESS, stripQuotes(uach[CH_HEADER_BITNESS])] + ]); } else { for (var prop in uach) { - if(this.hasOwnProperty(prop) && uach[prop]) this[prop] = setVal(uach[prop]); + if(this.hasOwnProperty(prop) && uach[prop]) this[prop] = stripQuotes(uach[prop]); } } return this; @@ -867,6 +875,7 @@ this.rgxMap = data[3]; this.data = (function (data) { var ua = data[0], + uaCH = data[1], itemType = data[2], rgxMap = data[3], init_props = data[4], @@ -900,17 +909,41 @@ return str ? str : UNDEF_TYPE; }; UAParserData.prototype.withClientHints = function () { - if (!NAVIGATOR_UADATA) return this; + if (!NAVIGATOR_UADATA) { + var HTTP_UACH = uaCH; + switch (itemType) { + case UA_BROWSER: + return new UAParserBrowser(ua, rgxMap, HTTP_UACH).parseCH().get(); + case UA_CPU: + return new UAParserCPU(ua, rgxMap, HTTP_UACH).parseCH().get(); + case UA_DEVICE: + return new UAParserDevice(ua, rgxMap, HTTP_UACH).parseCH().get(); + case UA_ENGINE: + return new UAParserEngine(ua, rgxMap).get(); + case UA_OS: + return new UAParserOS(ua, rgxMap, HTTP_UACH).parseCH().get(); + default : + return { + 'ua' : ua, + 'ua_ch' : uaCH, + 'browser' : new UAParserBrowser(ua, rgxMap[UA_BROWSER], HTTP_UACH).parseCH().get(), + 'cpu' : new UAParserCPU(ua, rgxMap[UA_CPU], HTTP_UACH).parseCH().get(), + 'device' : new UAParserDevice(ua, rgxMap[UA_DEVICE], HTTP_UACH).parseCH().get(), + 'engine' : new UAParserEngine(ua, rgxMap[UA_ENGINE]).get(), + 'os' : new UAParserOS(ua, rgxMap[UA_OS], HTTP_UACH).parseCH().get() + }; + } + } return NAVIGATOR_UADATA .getHighEntropyValues(CH_ALL_VALUES) .then(function (res) { var JS_UACH = new UAParserDataCH(res, false), - browser = new UAParserBrowser(ua, rgxMap, JS_UACH).get(), - cpu = new UAParserCPU(ua, ((itemType == UA_RESULT) ? rgxMap.cpu : rgxMap), JS_UACH).get(), - device = new UAParserDevice(ua, rgxMap, JS_UACH).get(), + browser = new UAParserBrowser(ua, rgxMap, JS_UACH).parseCH().get(), + cpu = new UAParserCPU(ua, ((itemType == UA_RESULT) ? rgxMap[UA_CPU] : rgxMap), JS_UACH).parseCH().get(), + device = new UAParserDevice(ua, rgxMap, JS_UACH).parseCH().get(), engine = new UAParserEngine(ua, rgxMap).get(), - os = new UAParserOS(ua, rgxMap, JS_UACH).get(); + os = new UAParserOS(ua, rgxMap, JS_UACH).parseCH().get(); switch (itemType) { case UA_BROWSER: @@ -963,24 +996,21 @@ ' ?browser$', [NAME, VERSION] ]); - this.parseCH(); - if (!this.get(NAME)) { - this.parse(); - // Brave-specific detection - if (NAVIGATOR && NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) { - this.set(NAME, 'Brave'); - } + this.parse(); + // Brave-specific detection + if (NAVIGATOR && NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) { + this.set(NAME, 'Brave'); } this.set(MAJOR, majorize(this.get(VERSION))); } UAParserBrowser.prototype = new UAParserItem(); UAParserBrowser.prototype.parseCH = function () { - var brands = this.uaCH.fullVersionList || this.uaCH.brands; + var brands = this.uaCH[FULLVERLIST] || this.uaCH[BRANDS]; if (brands) { for (var i in brands) { var brandName = brands[i].brand, brandVersion = brands[i].version; - if (!/not.a.brand/i.test(brandName) && (!this.get(NAME) || /chromi/i.test(this.get(NAME)))) { + if (!/not.a.brand/i.test(brandName) || /chromi/i.test(this.get(NAME))) { this.set(NAME, strip(GOOGLE+' ', brandName)) .set(VERSION, brandVersion) .set(MAJOR, majorize(brandVersion)); @@ -1001,16 +1031,13 @@ null, [ARCHITECTURE] ]); - this.parseCH(); - if (!this.get(ARCHITECTURE)) { - this.parse(); - } + this.parse(); } UAParserCPU.prototype = new UAParserItem(); UAParserCPU.prototype.parseCH = function () { - var archName = this.uaCH.architecture; + var archName = this.uaCH[ARCHITECTURE]; if (archName) { - archName += (archName && this.uaCH.bitness == '64') ? '64' : EMPTY; + archName += (archName && this.uaCH[BITNESS] == '64') ? '64' : EMPTY; rgxMapper.call(this.data, archName, this.rgxMap); } return this; @@ -1027,26 +1054,23 @@ null, [VENDOR, MODEL] ]); - this.parseCH(); - if (!this.get(TYPE) || !this.get(MODEL)) { - this.parse(); - if (!this.get(TYPE) && NAVIGATOR_UADATA && NAVIGATOR_UADATA.mobile) { - this.set(TYPE, MOBILE); - } - // iPadOS-specific detection: identified as Mac, but has some iOS-only properties - if (this.get(NAME) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { - this.set(MODEL, 'iPad') - .set(TYPE, TABLET); - } + this.parse(); + if (!this.get(TYPE) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[MOBILE]) { + this.set(TYPE, MOBILE); + } + // iPadOS-specific detection: identified as Mac, but has some iOS-only properties + if (this.get(NAME) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { + this.set(MODEL, 'iPad') + .set(TYPE, TABLET); } } UAParserDevice.prototype = new UAParserItem(); UAParserDevice.prototype.parseCH = function () { - if (this.uaCH.mobile) { + if (this.uaCH[MOBILE]) { this.set(TYPE, MOBILE); } - if (this.uaCH.model) { - this.set(MODEL, strip(/\"/g, this.uaCH.model)); + if (this.uaCH[MODEL]) { + this.set(MODEL, this.uaCH[MODEL]); } return this; }; @@ -1077,21 +1101,18 @@ ' ?os$', [NAME, VERSION] ]); - this.parseCH(); - if (!this.get(NAME)) { - this.parse(); - if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA.platform && NAVIGATOR_UADATA.platform != 'Unknown') { - this.set(NAME, NAVIGATOR_UADATA.platform - .replace(/chrome os/i, CHROMIUM_OS) - .replace(/macos/i, MAC_OS)); // backward compatibility - } + this.parse(); + if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[PLATFORM] && NAVIGATOR_UADATA[PLATFORM] != 'Unknown') { + this.set(NAME, NAVIGATOR_UADATA[PLATFORM] + .replace(/chrome os/i, CHROMIUM_OS) + .replace(/macos/i, MAC_OS)); // backward compatibility } } UAParserOS.prototype = new UAParserItem(); UAParserOS.prototype.parseCH = function () { - var osName = this.uaCH.platform; + var osName = this.uaCH[PLATFORM]; if(osName) { - var osVersion = this.uaCH.platformVersion; + var osVersion = this.uaCH[PLATFORMVER]; osVersion = (osName == WINDOWS) ? (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10') : osVersion; this.set(NAME, osName) .set(VERSION, osVersion); @@ -1141,23 +1162,23 @@ // public methods this.getBrowser = function () { - return new UAParserBrowser(userAgent, regexMap.browser, HTTP_UACH).get(); + return new UAParserBrowser(userAgent, regexMap[UA_BROWSER], HTTP_UACH).get(); }; this.getCPU = function () { - return new UAParserCPU(userAgent, regexMap.cpu, HTTP_UACH).get(); + return new UAParserCPU(userAgent, regexMap[UA_CPU], HTTP_UACH).get(); }; this.getDevice = function () { - return new UAParserDevice(userAgent, regexMap.device, HTTP_UACH).get(); + return new UAParserDevice(userAgent, regexMap[UA_DEVICE], HTTP_UACH).get(); }; this.getEngine = function () { - return new UAParserEngine(userAgent, regexMap.engine).get(); + return new UAParserEngine(userAgent, regexMap[UA_ENGINE]).get(); }; this.getOS = function () { - return new UAParserOS(userAgent, regexMap.os, HTTP_UACH).get(); + return new UAParserOS(userAgent, regexMap[UA_OS], HTTP_UACH).get(); }; this.getResult = function () { diff --git a/test/test.js b/test/test.js index 3291497..aab97ad 100644 --- a/test/test.js +++ b/test/test.js @@ -351,54 +351,134 @@ describe('Map UA-CH headers', function () { 'sec-ch-ua-platform-version' : '13', 'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' }; + + let uap = UAParser(headers).withClientHints(); + let browser = new UAParser(headers).getBrowser().withClientHints(); + let cpu = new UAParser(headers).getCPU().withClientHints(); + let device = new UAParser(headers).getDevice().withClientHints(); + let engine = new UAParser(headers).getEngine().withClientHints(); + let os = new UAParser(headers).getOS().withClientHints(); - const headers2 = { - 'sec-ch-ua-mobile' : '?1', - 'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' + let ua_ch = { + "architecture": "ARM", + "bitness": "64", + "brands": [ + { + "brand": "Chromium", + "version": "93" + }, + { + "brand": "Google Chrome", + "version": "93" + }, + { + "brand": " Not;A Brand", + "version": "99" + } + ], + "fullVersionList": [ + { + "brand": "Chromium", + "version": "93.0.1.2" + }, + { + "brand": "Google Chrome", + "version": "93.0.1.2" + }, + { + "brand": " Not;A Brand", + "version": "99.0.1.2" + } + ], + "mobile": true, + "model": "Pixel 99", + "platform": "Windows", + "platformVersion": "13" }; - let uap = UAParser(headers); - let browser = uap.browser; - let cpu = uap.cpu; - let device = uap.device; - let engine = uap.engine; - let os = uap.os; - - it('Can read from client-hints headers', function () { + it('Can read from client-hints headers using `withClientHints()`', function () { + assert.deepEqual(uap.ua_ch, ua_ch); + assert.strictEqual(uap.ua, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"); + assert.strictEqual(uap.browser.name, "Chrome"); + assert.strictEqual(uap.browser.version, "93.0.1.2"); + assert.strictEqual(uap.browser.major, "93"); assert.strictEqual(browser.name, "Chrome"); assert.strictEqual(browser.version, "93.0.1.2"); assert.strictEqual(browser.major, "93"); + assert.strictEqual(uap.cpu.architecture, "arm64"); assert.strictEqual(cpu.architecture, "arm64"); + assert.strictEqual(uap.device.type, "mobile"); + assert.strictEqual(uap.device.model, "Pixel 99"); + assert.strictEqual(uap.device.vendor, undefined); assert.strictEqual(device.type, "mobile"); assert.strictEqual(device.model, "Pixel 99"); assert.strictEqual(device.vendor, undefined); + assert.strictEqual(uap.engine.name, 'Blink'); + assert.strictEqual(uap.engine.version, '110.0.0.0'); assert.strictEqual(engine.name, 'Blink'); assert.strictEqual(engine.version, '110.0.0.0'); + assert.strictEqual(uap.os.name, "Windows"); + assert.strictEqual(uap.os.version, "11"); assert.strictEqual(os.name, "Windows"); assert.strictEqual(os.version, "11"); }); - it('Can read from user-agent header', function () { + it('Only read from user-agent header when called without `withClientHints()`', function () { - uap = UAParser(headers2); - browser = uap.browser; - cpu = uap.cpu; - device = uap.device; - engine = uap.engine; - os = uap.os; + uap = UAParser(headers); + browser = new UAParser(headers).getBrowser(); + cpu = new UAParser(headers).getCPU(); + device = new UAParser(headers).getDevice(); + engine = new UAParser(headers).getEngine(); + os = new UAParser(headers).getOS(); - assert.strictEqual(browser.name, "Chrome"); - assert.strictEqual(browser.version, "110.0.0.0"); - assert.strictEqual(browser.major, "110"); - assert.strictEqual(cpu.architecture, "amd64"); - assert.strictEqual(device.type, "mobile"); - assert.strictEqual(device.model, undefined); - assert.strictEqual(device.vendor, undefined); - assert.strictEqual(engine.name, 'Blink'); - assert.strictEqual(engine.version, '110.0.0.0'); - assert.strictEqual(os.name, "Linux"); - assert.strictEqual(os.version, "x86_64"); + assert.deepEqual(uap.ua_ch, ua_ch); + assert.strictEqual(uap.browser.name, "Chrome"); + assert.strictEqual(uap.browser.version, "110.0.0.0"); + assert.strictEqual(uap.browser.major, "110"); + assert.strictEqual(uap.cpu.architecture, "amd64"); + assert.strictEqual(uap.device.type, undefined); + assert.strictEqual(uap.device.model, undefined); + assert.strictEqual(uap.device.vendor, undefined); + assert.strictEqual(uap.engine.name, 'Blink'); + assert.strictEqual(uap.engine.version, '110.0.0.0'); + assert.strictEqual(uap.os.name, "Linux"); + assert.strictEqual(uap.os.version, "x86_64"); + }); + + it('Fallback to user-agent header when using `withClientHints()` but found no client hints-related headers', function () { + + const headers2 = { + 'sec-ch-ua-mobile' : '?1', + 'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' + }; + + uap = UAParser(headers2).withClientHints(); + + ua_ch = { + "architecture": undefined, + "bitness": undefined, + "brands": undefined, + "fullVersionList": undefined, + "mobile": true, + "model": undefined, + "platform": undefined, + "platformVersion": undefined + }; + + assert.deepEqual(uap.ua_ch, ua_ch); + assert.strictEqual(uap.browser.name, "Chrome"); + assert.strictEqual(uap.browser.version, "110.0.0.0"); + assert.strictEqual(uap.browser.major, "110"); + assert.strictEqual(uap.cpu.architecture, "amd64"); + assert.strictEqual(uap.device.type, "mobile"); + assert.strictEqual(uap.device.model, undefined); + assert.strictEqual(uap.device.vendor, undefined); + assert.strictEqual(uap.engine.name, 'Blink'); + assert.strictEqual(uap.engine.version, '110.0.0.0'); + assert.strictEqual(uap.os.name, "Linux"); + assert.strictEqual(uap.os.version, "x86_64"); }); }); @@ -451,6 +531,12 @@ describe('Map UA-CH JS', () => { assert.strictEqual(result.cpu.architecture, 'amd64'); assert.strictEqual(result.os.name, 'Android'); + await uap.getDevice().withClientHints().then((device) => { + assert.strictEqual(device.type, 'mobile'); + assert.strictEqual(device.vendor, undefined); + assert.strictEqual(device.model, 'Galaxy S3'); + }); + let result_without_ch = uap.getResult(); assert.strictEqual(result_without_ch.browser.name, undefined);