From e01663b48fa68680a795606dbbff1e2141ec68ab Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Sat, 8 Apr 2023 04:40:59 +0700 Subject: [PATCH] Rearrange internal class & remove old Safari map --- src/ua-parser.js | 359 +++++++++++++++++++++-------------------- test/browser-test.json | 4 +- 2 files changed, 189 insertions(+), 174 deletions(-) diff --git a/src/ua-parser.js b/src/ua-parser.js index 8e6c891..ffa73ea 100755 --- a/src/ua-parser.js +++ b/src/ua-parser.js @@ -95,18 +95,7 @@ // Helper ////////// - var assignFromEntries = function (arr) { - for (var i in arr) { - var propName = arr[i]; - if (typeof propName == OBJ_TYPE && propName.length == 2) { - this[propName[0]] = propName[1]; - } else { - this[propName] = undefined; - } - } - return this; - }, - extend = function (regexes, extensions) { + var extend = function (regexes, extensions) { var mergedRegexes = {}; for (var i in regexes) { mergedRegexes[i] = extensions[i] && extensions[i].length % 2 === 0 ? extensions[i].concat(regexes[i]) : regexes[i]; @@ -150,6 +139,17 @@ majorize = function (version) { return typeof(version) === STR_TYPE ? strip(/[^\d\.]/g, version).split('.')[0] : undefined; }, + setProps = function (arr) { + for (var i in arr) { + var propName = arr[i]; + if (typeof propName == OBJ_TYPE && propName.length == 2) { + this[propName[0]] = propName[1]; + } else { + this[propName] = undefined; + } + } + return this; + }, strip = function (pattern, str) { return str.replace(pattern, EMPTY); }, @@ -243,18 +243,7 @@ // String map ////////////// - // Safari < 3.0 - var oldSafariMap = { - '1.0' : '/8', - '1.2' : '/1', - '1.3' : '/3', - '2.0' : '/412', - '2.0.2' : '/416', - '2.0.3' : '/417', - '2.0.4' : '/419', - '?' : '/' - }, - windowsVersionMap = { + var windowsVersionMap = { 'ME' : '4.90', 'NT 3.11' : 'NT3.51', 'NT 4.0' : 'NT4.0', @@ -386,7 +375,7 @@ /version\/([\w\.\,]+) .*(safari)/i // Safari ], [VERSION, NAME], [ /webkit.+?(mobile ?safari|safari)(\/[\w\.]+)/i // Safari < 3.0 - ], [NAME, [VERSION, strMapper, oldSafariMap]], [ + ], [NAME, [VERSION, '1']], [ /(webkit|khtml)\/([\w\.]+)/i ], [NAME, VERSION], [ @@ -427,7 +416,7 @@ /((?:i[346]|x)86)[;\)]/i // IA32 (x86) ], [[ARCHITECTURE, 'ia32']], [ - /\b(aarch64|arm(v?8e?l?|_?64))\b/i // ARM64 + /\b(aarch64|arm(v?8e?l?|_?64))\b/i // ARM64 ], [[ARCHITECTURE, 'arm64']], [ /\b(arm(?:v[67])?ht?n?[fl]p?)\b/i // ARMHF @@ -811,23 +800,23 @@ var defaultProps = (function () { var props = { init : {}, isIgnore : {}, isIgnoreRgx : {}, toString : {}}; - assignFromEntries.call(props.init, [ + setProps.call(props.init, [ [UA_BROWSER, [NAME, VERSION, MAJOR]], [UA_CPU, [ARCHITECTURE]], [UA_DEVICE, [TYPE, MODEL, VENDOR]], [UA_ENGINE, [NAME, VERSION]], [UA_OS, [NAME, VERSION]] ]); - assignFromEntries.call(props.isIgnore, [ + setProps.call(props.isIgnore, [ [UA_BROWSER, [VERSION, MAJOR]], [UA_ENGINE, [VERSION]], [UA_OS, [VERSION]] ]); - assignFromEntries.call(props.isIgnoreRgx, [ + setProps.call(props.isIgnoreRgx, [ [UA_BROWSER, / ?browser$/i], [UA_OS, / ?os$/i] ]); - assignFromEntries.call(props.toString, [ + setProps.call(props.toString, [ [UA_BROWSER, [NAME, VERSION]], [UA_CPU, [ARCHITECTURE]], [UA_DEVICE, [VENDOR, MODEL]], @@ -837,7 +826,7 @@ return props; })(); - var createUAParserData = function (itemType, ua, rgxMap, uaCH) { + var createUAParserData = function (item, itemType) { var init_props = defaultProps.init[itemType], is_ignoreProps = defaultProps.isIgnore[itemType] || 0, @@ -845,27 +834,30 @@ toString_props = defaultProps.toString[itemType] || 0; function UAParserData () { - assignFromEntries.call(this, init_props); + setProps.call(this, init_props); } + + UAParserData.prototype.getItem = function () { + return item; + }; + UAParserData.prototype.withClientHints = function () { - var prevData = this; - // nodejs / non-client-hints browsers if (!NAVIGATOR_UADATA) { - var item = new UAParserItem(itemType, ua, rgxMap, uaCH); - item.data = prevData; - return item.parseCH().get(); + return item + .parseCH() + .get(); } // browsers based on chromium 85+ return NAVIGATOR_UADATA .getHighEntropyValues(CH_ALL_VALUES) .then(function (res) { - var JS_UACH = new UAParserDataCH(res, false); - var item = new UAParserItem(itemType, ua, rgxMap, JS_UACH); - item.data = prevData; - return item.parseCH().get(); + return item + .setCH(new UAParserDataCH(res, false)) + .parseCH() + .get(); }); }; @@ -923,9 +915,9 @@ function UAParserDataCH (uach, isHTTP_UACH) { uach = uach || {}; - assignFromEntries.call(this, CH_ALL_VALUES); + setProps.call(this, CH_ALL_VALUES); if (isHTTP_UACH) { - assignFromEntries.call(this, [ + setProps.call(this, [ [BRANDS, itemListToArray(uach[CH_HEADER])], [FULLVERLIST, itemListToArray(uach[CH_HEADER_FULL_VER_LIST])], [BRANDS, itemListToArray(uach[CH_HEADER])], @@ -941,133 +933,138 @@ if(this.hasOwnProperty(prop) && typeof uach[prop] !== UNDEF_TYPE) this[prop] = uach[prop]; } } - return this; } function UAParserItem (itemType, ua, rgxMap, uaCH) { - assignFromEntries.call(this, [ + + this.get = function (prop) { + if (!prop) return this.data; + return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined; + }; + + this.set = function (prop, val) { + this.data[prop] = val; + return this; + }; + + this.setCH = function (ch) { + this.uaCH = ch; + return this; + }; + + this.detectFeature = function () { + if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) { + switch (this.itemType) { + case UA_BROWSER: + // Brave-specific detection + if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) { + this.set(NAME, 'Brave'); + } + break; + case UA_DEVICE: + // Chrome-specific detection: check for 'mobile' value of navigator.userAgentData + 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(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { + this.set(MODEL, 'iPad') + .set(TYPE, TABLET); + } + break; + case UA_OS: + // Chrome-specific detection: check for 'platform' value of navigator.userAgentData + if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[PLATFORM]) { + this.set(NAME, NAVIGATOR_UADATA[PLATFORM]); + } + } + } + return this; + }; + + this.parseUA = function () { + if (this.itemType != UA_RESULT) { + rgxMapper.call(this.data, this.ua, this.rgxMap); + } + if (this.itemType == UA_BROWSER) { + this.set(MAJOR, majorize(this.get(VERSION))); + } + return this; + }; + + this.parseCH = function () { + var ua = this.ua, + uaCH = this.uaCH, + rgxMap = this.rgxMap; + + switch (this.itemType) { + case UA_BROWSER: + var brands = uaCH[FULLVERLIST] || 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) && (i < 1 || /chromi/i.test(this.get(NAME)))) { + this.set(NAME, strip(GOOGLE+' ', brandName)) + .set(VERSION, brandVersion) + .set(MAJOR, majorize(brandVersion)); + } + } + } + break; + case UA_CPU: + var archName = uaCH[ARCHITECTURE]; + if (archName) { + if (archName && uaCH[BITNESS] == '64') archName += '64'; + rgxMapper.call(this.data, archName + ';', rgxMap); + } + break; + case UA_DEVICE: + if (uaCH[MOBILE]) { + this.set(TYPE, MOBILE); + } + if (uaCH[MODEL]) { + this.set(MODEL, uaCH[MODEL]); + } + break; + case UA_OS: + var osName = uaCH[PLATFORM]; + if(osName) { + var osVersion = uaCH[PLATFORMVER]; + if (osName == WINDOWS) osVersion = (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10'); + this.set(NAME, osName) + .set(VERSION, osVersion); + } + break; + case UA_RESULT: + var data = this.data; + var parse = function (itemType) { + return data[itemType] + .getItem() + .setCH(uaCH) + .parseCH() + .get(); + }; + this.set('ua', ua) + .set(UA_BROWSER, parse(UA_BROWSER)) + .set(UA_CPU, parse(UA_CPU)) + .set(UA_DEVICE, parse(UA_DEVICE)) + .set(UA_ENGINE, parse(UA_ENGINE)) + .set(UA_OS, parse(UA_OS)); + } + return this; + }; + + setProps.call(this, [ ['itemType', itemType], ['ua', ua], ['uaCH', uaCH], ['rgxMap', rgxMap], - ['data', createUAParserData(itemType, ua, rgxMap, uaCH)] + ['data', createUAParserData(this, itemType)] ]); - if(this.itemType == UA_RESULT) { - var createUAParserItem = function (itemType) { - return new UAParserItem(itemType, ua, rgxMap[itemType], uaCH).parseUA().get(); - }; - this.set('ua', ua) - .set(UA_BROWSER, createUAParserItem(UA_BROWSER)) - .set(UA_CPU, createUAParserItem(UA_CPU)) - .set(UA_DEVICE, createUAParserItem(UA_DEVICE)) - .set(UA_ENGINE, createUAParserItem(UA_ENGINE)) - .set(UA_OS, createUAParserItem(UA_OS)) - .get(); - } + return this; } - UAParserItem.prototype.detectFeature = function () { - var isSelfNav = NAVIGATOR && NAVIGATOR.userAgent == this.ua; - switch(this.itemType) { - case UA_BROWSER: - // Brave-specific detection - if (isSelfNav && NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) { - this.set(NAME, 'Brave'); - } - this.set(MAJOR, majorize(this.get(VERSION))); - break; - case UA_DEVICE: - if (isSelfNav && !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 (isSelfNav && this.get(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { - this.set(MODEL, 'iPad') - .set(TYPE, TABLET); - } - break; - case UA_OS: - if (isSelfNav && !this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[PLATFORM]) { - this.set(NAME, NAVIGATOR_UADATA[PLATFORM]); - } - } - return this; - }; - UAParserItem.prototype.get = function (prop) { - if (!prop) return this.data; - return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined; - }; - UAParserItem.prototype.parseUA = function () { - if (this.itemType != UA_RESULT) { - rgxMapper.call(this.data, this.ua, this.rgxMap); - } - this.detectFeature(); - return this; - }; - UAParserItem.prototype.parseCH = function () { - var ua = this.ua, - uaCH = this.uaCH, - rgxMap = this.rgxMap; - - switch (this.itemType) { - case UA_BROWSER: - var brands = uaCH[FULLVERLIST] || 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) && (i < 1 || /chromi/i.test(this.get(NAME)))) { - this.set(NAME, strip(GOOGLE+' ', brandName)) - .set(VERSION, brandVersion) - .set(MAJOR, majorize(brandVersion)); - } - } - } - break; - case UA_CPU: - var archName = uaCH[ARCHITECTURE]; - if (archName) { - if (archName && uaCH[BITNESS] == '64') archName += '64'; - rgxMapper.call(this.data, archName + ';', rgxMap); - } - break; - case UA_DEVICE: - if (uaCH[MOBILE]) { - this.set(TYPE, MOBILE); - } - if (uaCH[MODEL]) { - this.set(MODEL, uaCH[MODEL]); - } - break; - case UA_OS: - var osName = uaCH[PLATFORM]; - if(osName) { - var osVersion = uaCH[PLATFORMVER]; - if (osName == WINDOWS) osVersion = (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10'); - this.set(NAME, osName) - .set(VERSION, osVersion); - } - break; - case UA_RESULT: - var prevData = this.data; - var createUAParserItemWithCH = function (itemType) { - var item = new UAParserItem(itemType, ua, rgxMap[itemType], uaCH); - item.data = prevData[itemType]; - return item.parseCH().get(); - }; - this.set('ua', ua) - .set(UA_BROWSER, createUAParserItemWithCH(UA_BROWSER)) - .set(UA_CPU, createUAParserItemWithCH(UA_CPU)) - .set(UA_DEVICE, createUAParserItemWithCH(UA_DEVICE)) - .set(UA_ENGINE, createUAParserItemWithCH(UA_ENGINE)) - .set(UA_OS, createUAParserItemWithCH(UA_OS)); - } - return this; - }; - UAParserItem.prototype.set = function (prop, val) { - this.data[prop] = val; - return this; - }; function UAParser (ua, extensions, headers) { @@ -1104,26 +1101,44 @@ extend(defaultRegexes, extensions) : defaultRegexes, - createUAParserItemFunc = function (itemType) { - return function () { - return new UAParserItem(itemType, userAgent, itemType == UA_RESULT ? regexMap : regexMap[itemType], HTTP_UACH).parseUA().get(); - }; + createItemFunc = function (itemType) { + if (itemType == UA_RESULT) { + return function () { + return new UAParserItem(itemType, userAgent, regexMap, HTTP_UACH) + .set('ua', userAgent) + .set(UA_BROWSER, this.getBrowser()) + .set(UA_CPU, this.getCPU()) + .set(UA_DEVICE, this.getDevice()) + .set(UA_ENGINE, this.getEngine()) + .set(UA_OS, this.getOS()) + .get(); + }; + } else { + return function () { + return new UAParserItem(itemType, userAgent, regexMap[itemType], HTTP_UACH) + .parseUA() + .detectFeature() + .get(); + }; + } }; // public methods - assignFromEntries.call(this, [ - ['getBrowser', createUAParserItemFunc(UA_BROWSER)], - ['getCPU', createUAParserItemFunc(UA_CPU)], - ['getDevice', createUAParserItemFunc(UA_DEVICE)], - ['getEngine', createUAParserItemFunc(UA_ENGINE)], - ['getOS', createUAParserItemFunc(UA_OS)], - ['getResult', createUAParserItemFunc(UA_RESULT)], + setProps.call(this, [ + ['getBrowser', createItemFunc(UA_BROWSER)], + ['getCPU', createItemFunc(UA_CPU)], + ['getDevice', createItemFunc(UA_DEVICE)], + ['getEngine', createItemFunc(UA_ENGINE)], + ['getOS', createItemFunc(UA_OS)], + ['getResult', createItemFunc(UA_RESULT)], ['getUA', function () { return userAgent; }], ['setUA', function (ua) { userAgent = (typeof ua === STR_TYPE && ua.length > UA_MAX_LENGTH) ? trim(ua, UA_MAX_LENGTH) : ua; return this; }] - ]).setUA(userAgent); + ]) + .setUA(userAgent); + return this; } diff --git a/test/browser-test.json b/test/browser-test.json index b8f7a6e..9ca190c 100644 --- a/test/browser-test.json +++ b/test/browser-test.json @@ -1084,8 +1084,8 @@ "expect" : { "name" : "Safari", - "version" : "2.0.4", - "major" : "2" + "version" : "1", + "major" : "1" } }, {