diff --git a/readme.md b/readme.md index 6b2146d73..c37673527 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ # UAParser.js -JavaScript library to detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data that can be used either in browser (client-side) or node.js (server-side). +JavaScript library to detect Browser, Engine, OS, CPU, and Device type/model from User-Agent & Client-Hints data that can be used either in browser (client-side) or node.js (server-side). * Author : Faisal Salman <> * Demo : https://faisalman.github.io/ua-parser-js @@ -56,25 +56,28 @@ console.log(parser); // {} let parserResults = parser.getResult(); console.log(parserResults); /** { - "ua": "", - "browser": {}, - "engine": {}, - "os": {}, - "device": {}, - "cpu": {} + "ua" : "", + "browser" : {}, + "engine" : {}, + "os" : {}, + "device" : {}, + "cpu" : {} + + // since@1.1 + ,"ua_ch" : {} } */ ``` When you call UAParser without the `new` keyword, it will automatically call `getResult()` function and return the parsed results. * `UAParser([user-agent:string][,extensions:object][,headers:object(since@1.1)])` - * returns result object `{ ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} }` + * returns result object `{ ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} /* added since@1.1: ua_ch: {} */ }` ## Methods #### Methods table The methods are self explanatory, here's a small overview on all the available methods: * `getResult()` - returns all function object calls, user-agent string, browser info, cpu, device, engine, os: -`{ ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} }`. +`{ ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} /* added since@1.1: ua_ch: {} */ }`. * `getBrowser()` - returns the browser name and version. * `getDevice()` - returns the device model, type, vendor. @@ -87,7 +90,7 @@ The methods are self explanatory, here's a small overview on all the available m --- * `getResult()` - * returns `{ ua: '', browser: UABrowser {}, cpu: UACPU {}, device: UADevice {}, engine: UAEngine {}, os: UAOS {} }` + * returns `{ ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} /* added since@1.1: ua_ch: {} */ }` * `getBrowser()` * returns `{ name: '', version: '' }` @@ -180,7 +183,7 @@ Ubuntu, Unix, VectorLinux, Viera, watchOS, WebOS, Windows [Phone/Mobile], Zenwal * set UA string to be parsed * returns current instance -#### * is() utility `since@1.1` +#### * `is()` utility `since@1.1` ```js // Is just a shorthand to check whether specified item has a property with equals value (case-insensitive) @@ -244,7 +247,7 @@ let engine = uap.getEngine(); engine.is("Blink"); // true ``` -#### * toString() utility `since@1.1` +#### * `toString()` utility `since@1.1` ```js // Retrieve full-name values as a string @@ -285,9 +288,33 @@ engine.version; // "28.0.1500.95" engine.toString(); // "Blink 28.0.1500.95" ``` +#### * `withClientHints()` `since@1.1` + +Unlike reading user-agent data, accessing client-hints data in browser-environment must be done in an asynchronous way. Worry not, you can chain the UAParser's `get*` method with `withClientHints()` to read the client-hints data as well that will return the updated data as a `Promise`. + +```js +(async function () { + let ua = new UAParser(); + + // get browser data from user-agent only : + let browser = ua.getBrowser(); + console.log('Using User-Agent: ', browser); + + // get browser data from client-hints (with user-agent as fallback) : + browser = await ua.getBrowser().withClientHints(); + console.log('Using Client-Hints: ', browser); + + // alternatively : + ua.getBrowser().withClientHints().then(function (browser) { + console.log('Using Client-Hints: ', browser); + }); +})(); +``` + + ## Extending Regex -If you want to detect something that's not currently provided by UAParser.js (eg: bots, specific apps, etc), you can pass a list of regexes to extend internal UAParser.js regexes with your own. +If you want to detect something that's not currently provided by UAParser.js (eg: `bots`, specific apps, etc), you can pass a list of regexes to extend internal UAParser.js regexes with your own. * `UAParser([uastring,] extensions [,headers:object(since@1.1)])` @@ -359,6 +386,18 @@ console.log(myParser2.setUA(myUA2).getDevice()); // {vendor: "MyTab", model: "1 cpu: { architecture: "" } + + // added since@1.1: + ,ua_ch: { + architecture: "", + brands: "", + bitness: "", + fullVersionList: "", + mobile: "", + model: "", + platform: "", + platformVersion: "" + } } */ // Default result depends on current window.navigator.userAgent value diff --git a/src/ua-parser.js b/src/ua-parser.js index d0a9d2be5..0695044d2 100755 --- a/src/ua-parser.js +++ b/src/ua-parser.js @@ -46,7 +46,8 @@ CH_HEADER_MOBILE = CH_HEADER + '-mobile', CH_HEADER_MODEL = CH_HEADER + '-model', CH_HEADER_PLATFORM = CH_HEADER + '-platform', - CH_HEADER_PLATFORM_VER = CH_HEADER_PLATFORM + '-version'; + CH_HEADER_PLATFORM_VER = CH_HEADER_PLATFORM + '-version', + CH_ALL_VALUES = ['brands', 'fullVersionList', MOBILE, MODEL, 'platform', 'platformVersion', ARCHITECTURE, 'bitness']; var AMAZON = 'Amazon', APPLE = 'Apple', @@ -73,6 +74,13 @@ CHROMIUM_OS = 'Chromium OS', MAC_OS = 'Mac OS', WINDOWS = 'Windows'; + + var NAVIGATOR = (typeof window !== UNDEF_TYPE && window.navigator) ? + window.navigator : + undefined, + NAVIGATOR_UADATA = (NAVIGATOR && NAVIGATOR.userAgentData) ? + NAVIGATOR.userAgentData : + undefined; /////////// // Helper @@ -813,19 +821,23 @@ // Constructor //////////////// - function UACHData (uach, isBrowser) { + function UAParserDataCH (uach, isHTTP_UACH) { uach = uach || {}; - if (!isBrowser) { - initialize.call(this, ['brands', MOBILE, MODEL, 'platform', 'platformVersion', ARCHITECTURE, 'bitness']); - if ((uach[CH_HEADER_FULL_VER_LIST] || uach[CH_HEADER])) { - this.brands = []; - var tokens = strip(/\\?\"/g, (uach[CH_HEADER_FULL_VER_LIST] || uach[CH_HEADER])).split(', '); + initialize.call(this, CH_ALL_VALUES); + 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='); - this.brands[i] = { brand : token[0], version : token[1] }; + arr[i] = { brand : token[0], version : token[1] }; } - } - this.mobile = uach[CH_HEADER_MOBILE] == '?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]); var setHeader = function (header) { return header ? strip(/\"/g, header) : undefined; }; @@ -834,25 +846,32 @@ this.platformVersion = setHeader(uach[CH_HEADER_PLATFORM_VER]); this.architecture = setHeader(uach[CH_HEADER_ARCH]); this.bitness = setHeader(uach[CH_HEADER_BITNESS]); + } else { + for (var prop in uach) { + this[prop] = uach[prop]; + } } return this; } - function UAItem (data) { + function UAParserItem (data) { if (!data) return; this.ua = data[0]; - this.rgxMap = data[1]; - this.uaCH = data[2]; + this.uaCH = data[1]; + this.rgxMap = data[3]; this.data = (function (data) { - var init_props = data[3], - is_ignoreProps = data[4], - is_ignoreRgx = data[5], - toString_props = data[6]; - - function UAData () { + var ua = data[0], + itemType = data[2], + rgxMap = data[3], + init_props = data[4], + is_ignoreProps = data[5], + is_ignoreRgx = data[6], + toString_props = data[7]; + + function UAParserData () { initialize.call(this, init_props); } - UAData.prototype.is = function (strToCheck) { + UAParserData.prototype.is = function (strToCheck) { var is = false; for (var i in this) { if (this.hasOwnProperty(i) && !is_ignoreProps[i] && lowerize(this[i], is_ignoreRgx) === lowerize(strToCheck, is_ignoreRgx)) { @@ -865,7 +884,7 @@ } return is; }; - UAData.prototype.toString = function () { + UAParserData.prototype.toString = function () { var str = EMPTY; for (var i in toString_props) { if (typeof(this[toString_props[i]]) !== UNDEF_TYPE) { @@ -874,36 +893,83 @@ } return str ? str : UNDEF_TYPE; }; - return new UAData(); + UAParserData.prototype.withClientHints = function () { + if (!NAVIGATOR_UADATA) return; + 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, rgxMap, JS_UACH).get(), + device = new UAParserDevice(ua, rgxMap, JS_UACH).get(), + engine = new UAParserEngine(ua, rgxMap).get(), + os = new UAParserOS(ua, rgxMap, JS_UACH).get(); + + switch (itemType) { + case 'browser': + return browser; + case 'cpu': + return cpu; + case 'device': + return device; + case 'engine': + return engine; + case 'os': + return os; + default : + return { + 'ua' : ua, + 'ua_ch' : JS_UACH, + 'browser' : browser, + 'cpu' : cpu, + 'device' : device, + 'engine' : engine, + 'os' : os + }; + } + }); + }; + return new UAParserData(); })(data); } - UAItem.prototype.get = function (prop) { + UAParserItem.prototype.get = function (prop) { if (!prop) return this.data; return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined; }; - UAItem.prototype.parse = function () { + UAParserItem.prototype.parse = function () { rgxMapper.call(this.data, this.ua, this.rgxMap); return this; }; - UAItem.prototype.set = function (prop, val) { + UAParserItem.prototype.set = function (prop, val) { this.data[prop] = val; return this; }; - function UABrowser (ua, browserMap, uach) { - UAItem.call(this, [ + function UAParserBrowser (ua, browserMap, uach) { + UAParserItem.call(this, [ ua, - browserMap, uach, + 'browser', + browserMap, [NAME, VERSION, MAJOR], [VERSION, MAJOR], ' ?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.set(MAJOR, majorize(this.get(VERSION))); } - UABrowser.prototype = new UAItem(); - UABrowser.prototype.parseCH = function () { - var brands = this.uaCH.brands; + UAParserBrowser.prototype = new UAParserItem(); + UAParserBrowser.prototype.parseCH = function () { + var brands = this.uaCH.fullVersionList || this.uaCH.brands; if (brands) { for (var i in brands) { var brandName = brands[i].brand, @@ -918,38 +984,58 @@ return this; }; - function UACPU (ua, cpuMap, uach) { - UAItem.call(this, [ + function UAParserCPU (ua, cpuMap, uach) { + UAParserItem.call(this, [ ua, - cpuMap, uach, + 'cpu', + cpuMap, [ARCHITECTURE], [], null, [ARCHITECTURE] ]); + this.parseCH(); + if (!this.get(ARCHITECTURE)) { + this.parse(); + } } - UACPU.prototype = new UAItem(); - UACPU.prototype.parseCH = function () { + UAParserCPU.prototype = new UAParserItem(); + UAParserCPU.prototype.parseCH = function () { var archName = this.uaCH.architecture; - archName += (archName && this.uaCH.bitness == '64') ? '64' : EMPTY; - rgxMapper.call(this.data, archName, this.rgxMap); + if (archName) { + archName += (archName && this.uaCH.bitness == '64') ? '64' : EMPTY; + rgxMapper.call(this.data, archName, this.rgxMap); + } return this; }; - function UADevice (ua, deviceMap, uach) { - UAItem.call(this, [ + function UAParserDevice (ua, deviceMap, uach) { + UAParserItem.call(this, [ ua, - deviceMap, uach, + 'device', + deviceMap, [TYPE, MODEL, VENDOR], [], 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); + } + } } - UADevice.prototype = new UAItem(); - UADevice.prototype.parseCH = function () { + UAParserDevice.prototype = new UAParserItem(); + UAParserDevice.prototype.parseCH = function () { if (this.uaCH.mobile) { this.set(TYPE, MOBILE); } @@ -959,41 +1045,59 @@ return this; }; - function UAEngine (ua, engineMap) { - UAItem.call(this, [ + function UAParserEngine (ua, engineMap) { + UAParserItem.call(this, [ ua, - engineMap, null, + 'engine', + engineMap, [NAME, VERSION], [VERSION], null, [NAME, VERSION] ]); + this.parse(); } - UAEngine.prototype = new UAItem(); + UAParserEngine.prototype = new UAParserItem(); - function UAOS (ua, osMap, uach) { - UAItem.call(this, [ + function UAParserOS (ua, osMap, uach) { + UAParserItem.call(this, [ ua, - osMap, uach, + 'os', + osMap, [NAME, VERSION], [VERSION], ' ?os$', [NAME, VERSION] ]); + this.parseCH(); + if (!this.get(NAME)) { + this.parse(); + if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA.platform != 'Unknown') { + this.set(NAME, NAVIGATOR_UADATA.platform + .replace(/chrome os/i, CHROMIUM_OS) + .replace(/macos/i, MAC_OS)); // backward compatibility + } + } } - UAOS.prototype = new UAItem(); - UAOS.prototype.parseCH = function (uach) { + UAParserOS.prototype = new UAParserItem(); + UAParserOS.prototype.parseCH = function () { var osName = this.uaCH.platform; - var osVersion = this.uaCH.platformVersion; - osVersion = (osName == WINDOWS) ? (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10') : osVersion; - this.set(NAME, osName) - .set(VERSION, osVersion); + if(osName) { + var osVersion = this.uaCH.platformVersion; + osVersion = (osName == WINDOWS) ? (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10') : osVersion; + this.set(NAME, osName) + .set(VERSION, osVersion); + } return this; }; - + function UAParserResult (ua, uach) { + UAParserItem.call(this, [ua, uach]); + } + UAParserResult.prototype = new UAParserItem(); + function UAParser (ua, extensions, headers) { if (typeof ua === OBJ_TYPE) { @@ -1016,22 +1120,14 @@ return new UAParser(ua, extensions, headers).getResult(); } - var navigator = (typeof window !== UNDEF_TYPE && window.navigator) ? - window.navigator : - undefined, - - userAgent = ua || - ((navigator && navigator.userAgent) ? - navigator.userAgent : + var userAgent = ua || + ((NAVIGATOR && NAVIGATOR.userAgent) ? + NAVIGATOR.userAgent : (headers && headers[USER_AGENT] ? headers[USER_AGENT] : EMPTY)), - clientHints = new UACHData(headers, false), - - userAgentData = (navigator && navigator.userAgentData) ? - navigator.userAgentData : - undefined, + HTTP_UACH = new UAParserDataCH(headers, true), regexMap = extensions ? extend(regexes, extensions) : @@ -1039,85 +1135,35 @@ // public methods this.getBrowser = function () { - var browser = new UABrowser(userAgent, regexMap.browser, clientHints); - if (headers && (headers[CH_HEADER_FULL_VER_LIST] || headers[CH_HEADER])) { - browser.parseCH(); - } - if (!browser.get(NAME)) { - browser.parse(); - // Brave-specific detection - if (navigator && navigator.brave && typeof navigator.brave.isBrave == FUNC_TYPE) { - browser.set(NAME, 'Brave'); - } - } - return browser - .set(MAJOR, majorize(browser.get(VERSION))) - .get(); + return new UAParserBrowser(userAgent, regexMap.browser, HTTP_UACH).get(); }; this.getCPU = function () { - var cpu = new UACPU(userAgent, regexMap.cpu, clientHints); - if (headers && headers[CH_HEADER_ARCH]) { - cpu.parseCH(); - } - if (!cpu.get(ARCHITECTURE)) { - cpu.parse(); - } - return cpu.get(); + return new UAParserCPU(userAgent, regexMap.cpu, HTTP_UACH).get(); }; this.getDevice = function () { - var device = new UADevice(userAgent, regexMap.device, clientHints); - if (headers && (headers[CH_HEADER_MOBILE] || headers[CH_HEADER_MODEL])) { - device.parseCH(); - } - if (!device.get(TYPE) || !device.get(MODEL)) { - device.parse(); - if (!device.get(TYPE) && userAgentData && userAgentData.mobile) { - device.set(TYPE, MOBILE); - } - // iPadOS-specific detection: identified as Mac, but has some iOS-only properties - if (device.get(NAME) == 'Macintosh' && navigator && typeof navigator.standalone !== UNDEF_TYPE && navigator.maxTouchPoints && navigator.maxTouchPoints > 2) { - device - .set(MODEL, 'iPad') - .set(TYPE, TABLET); - } - } - return device.get(); + return new UAParserDevice(userAgent, regexMap.device, HTTP_UACH).get(); }; this.getEngine = function () { - return new UAEngine(userAgent, regexMap.engine) - .parse() - .get(); + return new UAParserEngine(userAgent, regexMap.engine).get(); }; this.getOS = function () { - var os = new UAOS(userAgent, regexMap.os, clientHints); - if (headers && headers[CH_HEADER_PLATFORM]) { - os.parseCH(); - } - if (!os.get(NAME)) { - os.parse(); - if (!os.get(NAME) && userAgentData && userAgentData.platform != 'Unknown') { - os.set(NAME, userAgentData.platform - .replace(/chrome os/i, CHROMIUM_OS) - .replace(/macos/i, MAC_OS)); // backward compatibility - } - } - return os.get(); + return new UAParserOS(userAgent, regexMap.os, HTTP_UACH).get(); }; this.getResult = function () { - return { - 'ua' : userAgent, - 'ua_ch' : clientHints, - 'browser' : this.getBrowser(), - 'cpu' : this.getCPU(), - 'device' : this.getDevice(), - 'engine' : this.getEngine(), - 'os' : this.getOS() - }; + return new UAParserResult(userAgent, HTTP_UACH) + .set('ua', userAgent) + .set('ua_ch', HTTP_UACH) + .set('browser', this.getBrowser()) + .set('cpu', this.getCPU()) + .set('device', this.getDevice()) + .set('engine', this.getEngine()) + .set('os', this.getOS()) + .get(); }; this.getUA = function () { diff --git a/test/test.js b/test/test.js index 677e02b90..30f26d108 100644 --- a/test/test.js +++ b/test/test.js @@ -82,7 +82,7 @@ describe('Returns', function () { assert.deepEqual(new UAParser('').getResult(), { ua : '', - ua_ch : { architecture: undefined, bitness: undefined, brands: undefined, mobile: false, model: undefined, platform: undefined, platformVersion: undefined }, + ua_ch : { architecture: undefined, bitness: undefined, brands: undefined, fullVersionList: undefined, mobile: false, model: undefined, platform: undefined, platformVersion: undefined }, browser: { name: undefined, version: undefined, major: undefined }, cpu: { architecture: undefined }, device: { vendor: undefined, model: undefined, type: undefined }, @@ -400,4 +400,12 @@ describe('Map UA-CH headers', function () { assert.strictEqual(os.name, "Linux"); assert.strictEqual(os.version, "x86_64"); }); +}); + +describe('Map UA-CH JS', function () { + it('Can read client hints from browser', async function () { + let ua = new UAParser(); + let browser = await ua.getBrowser().withClientHints(); + // TODO : create tests + }); }); \ No newline at end of file