diff --git a/readme.md b/readme.md index 73bfa27..6b2146d 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 with relatively small footprint (~17KB minified, ~6KB gzipped) 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 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 @@ -26,24 +26,20 @@ JavaScript library to detect Browser, Engine, OS, CPU, and Device type/model fro -51degrees +51degrees ↗ @51degrees/ua-parser-js

UAParser.js has been upgraded to detect comprehensive device data based on the User-Agent and User-Agent Client Hints.

This package supports all device types including Apple and Android devices and can be used either in a browser (client-side) or Node.js environment (server-side).

Visit ↗ 51Degrees UAParser to get started.

- - -

On 6 March, we’ll be hosting a demonstration of the 51Degrees UAParser. Register your place for the webinar ↗ here.

- --- # Documentation -### UAParser([user-agent:string][,extensions:object]) +### UAParser([user-agent:string][,extensions:object][,headers:object(since@1.1)]) In The Browser environment you dont need to pass the user-agent string to the function, you can just call the funtion and it should automatically get the string from the `window.navigator.userAgent`, but that is not the case in nodejs. The user-agent string must be passed in nodejs for the function to work. Usually you can find the user agent in: @@ -55,7 +51,7 @@ When you call `UAParser` with the `new` keyword `UAParser` will return a new ins Like so: * `new UAParser([user-agent:string][,extensions:object][,headers:object(since@1.1)])` ```js -let parser = new UAParser("user-agent"); // you need to pass the user-agent for nodejs +let parser = new UAParser("your user-agent here"); // you need to pass the user-agent for nodejs console.log(parser); // {} let parserResults = parser.getResult(); console.log(parserResults); @@ -297,24 +293,30 @@ If you want to detect something that's not currently provided by UAParser.js (eg ```js // Example: -let myOwnListOfBrowsers = [ +const myOwnListOfBrowsers = [ [/(mybrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION, ['type', 'bot']] ]; + +const myUA = 'Mozilla/5.0 MyBrowser/1.3'; + let myParser = new UAParser({ browser: myOwnListOfBrowsers }); -let myUA = 'Mozilla/5.0 MyBrowser/1.3'; + console.log(myParser.setUA(myUA).getBrowser()); // {name: "MyBrowser", version: "1.3", major: "1", type : "bot"} console.log(myParser.getBrowser().is('bot')); // true // Another example: -let myOwnListOfDevices = [ +const myOwnListOfDevices = [ [/(mytab) ([\w ]+)/i], [UAParser.DEVICE.VENDOR, UAParser.DEVICE.MODEL, [UAParser.DEVICE.TYPE, UAParser.DEVICE.TABLET]], [/(myphone)/i], [UAParser.DEVICE.VENDOR, [UAParser.DEVICE.TYPE, UAParser.DEVICE.MOBILE]] ]; + +const myUA2 = 'Mozilla/5.0 MyTab 14 Pro Max'; + let myParser2 = new UAParser({ browser: myOwnListOfBrowsers, device: myOwnListOfDevices }); -let myUA2 = 'Mozilla/5.0 MyTab 14 Pro Max'; + console.log(myParser2.setUA(myUA2).getDevice()); // {vendor: "MyTab", model: "14 Pro Max", type: "tablet"} ``` @@ -408,6 +410,17 @@ var uap = require('ua-parser-js'); http.createServer(function (req, res) { // get user-agent header var ua = uap(req.headers['user-agent']); + + /* // BEGIN since@1.1 - you can also pass client-hints data to UAParser + + var getHighEntropyValues = 'Sec-CH-UA-Full-Version-List, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch, Sec-CH-UA-Bitness'; + res.setHeader('Accept-CH', getHighEntropyValues); + res.setHeader('Critical-CH', getHighEntropyValues); + + var ua = uap(req.headers); + + // END since@1.1 */ + // write the result as response res.end(JSON.stringify(ua, null, ' ')); }) diff --git a/src/ua-parser.js b/src/ua-parser.js index 7a3dc7a..f9b4edc 100755 --- a/src/ua-parser.js +++ b/src/ua-parser.js @@ -38,7 +38,15 @@ WEARABLE = 'wearable', EMBEDDED = 'embedded', USER_AGENT = 'user-agent', - UA_MAX_LENGTH = 350; + UA_MAX_LENGTH = 350, + CH_HEADER = 'sec-ch-ua', + CH_HEADER_FULL_VER_LIST = CH_HEADER + '-full-version-list', + CH_HEADER_ARCH = CH_HEADER + '-arch', + CH_HEADER_BITNESS = CH_HEADER + '-bitness', + 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'; var AMAZON = 'Amazon', APPLE = 'Apple', @@ -63,7 +71,8 @@ ZTE = 'ZTE', FACEBOOK = 'Facebook', CHROMIUM_OS = 'Chromium OS', - MAC_OS = 'Mac OS'; + MAC_OS = 'Mac OS', + WINDOWS = 'Windows'; /////////// // Helper @@ -110,6 +119,8 @@ var rgxMapper = function (ua, arrays) { + if(!ua || !arrays) return; + var i = 0, j, k, p, q, matches, match; // loop through all regexes maps @@ -718,7 +729,7 @@ /(windows)[\/ ]?([ntce\d\. ]+\w)(?!.+xbox)/i ], [NAME, [VERSION, strMapper, windowsVersionMap]], [ /(win(?=3|9|n)|win 9x )([nt\d\.]+)/i - ], [[NAME, 'Windows'], [VERSION, strMapper, windowsVersionMap]], [ + ], [[NAME, WINDOWS], [VERSION, strMapper, windowsVersionMap]], [ // iOS/macOS /ip[honead]{2,4}\b(?:.*os ([\w]+) like mac|; opera)/i, // iOS @@ -788,115 +799,174 @@ // Constructor //////////////// - function UAItem () {} + function UAItem (data) { + if (!data) return; + this.ua = data[0]; + this.rgxMap = data[1]; + this.data = (function (data) { + var is_ignoreProps = data[3], + is_ignoreRgx = data[4], + toString_props = data[5]; + + var UAData = function () { + for (var init_props in data[2]) { + this[data[2][init_props]] = undefined; + } + }; + UAData.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)) { + is = true; + if (strToCheck != UNDEF_TYPE) break; + } else if (strToCheck == UNDEF_TYPE && is) { + is = !is; + break; + } + } + return is; + }; + UAData.prototype.toString = function () { + var str = EMPTY; + for (var i in toString_props) { + if (typeof(this[toString_props[i]]) !== UNDEF_TYPE) { + str += (str ? ' ' : EMPTY) + this[toString_props[i]]; + } + } + return str ? str : UNDEF_TYPE; + }; + return new UAData(); + })(data); + } UAItem.prototype.get = function (prop) { - if (!prop) { - return this.data; - } + if (!prop) return this.data; return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined; }; - UAItem.prototype.parse = function (ua, rgxmap) { - rgxMapper.call(this.data, ua, rgxmap); + UAItem.prototype.parse = function () { + rgxMapper.call(this.data, this.ua, this.rgxMap); + return this; }; UAItem.prototype.set = function (prop, val) { this.data[prop] = val; - }; - UAItem.prototype.then = function (callback) { - return callback(this.data); - }; - UAItem.createUAData = function (data) { - var is_ignoreProps = data.is_ignoreProps, - is_ignoreRgx = data.is_ignoreRgx, - toString_props = data.toString_props; - - var UAData = function () { - for (var i in data.init_props) { - this[data.init_props[i]] = undefined; - } - }; - UAData.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)) { - is = true; - if (strToCheck != UNDEF_TYPE) break; - } else if (strToCheck == UNDEF_TYPE && is) { - is = !is; - break; - } - } - return is; - }; - UAData.prototype.toString = function () { - var str = EMPTY; - for (var i in toString_props) { - if (typeof(this[toString_props[i]]) !== UNDEF_TYPE) { - str += (str ? ' ' : EMPTY) + this[toString_props[i]]; - } - } - return str ? str : UNDEF_TYPE; - }; - return new UAData(); + return this; }; - function UABrowser () { - this.data = UAItem.createUAData({ - init_props : [NAME, VERSION, MAJOR], - is_ignoreProps : [VERSION, MAJOR], - is_ignoreRgx : ' ?browser$', - toString_props : [NAME, VERSION] - }); + function UABrowser (ua, browserMap) { + UAItem.call(this, [ + ua, + browserMap, + [NAME, VERSION, MAJOR], + [VERSION, MAJOR], + ' ?browser$', + [NAME, VERSION] + ]); } UABrowser.prototype = new UAItem(); + UABrowser.prototype.parse = function (uach) { + if (uach) { + var brands = uach[CH_HEADER_FULL_VER_LIST] || uach[CH_HEADER]; + if (brands) { + brands = brands.replace(/\\?\"/g, EMPTY).split(', '); + for (var i in brands) { + var brand = brands[i].split(';v='), + brandName = brand[0], + brandVersion = brand[1]; + if (!/not.a.brand/i.test(brandName) && (!this.get(NAME) || /chromi/i.test(this.get(NAME)))) { + this.set(NAME, brandName.replace(GOOGLE+' ', EMPTY)) + .set(VERSION, brandVersion) + .set(MAJOR, majorize(brandVersion)); + } + } + } + } + if (!this.get(NAME)) UAItem.prototype.parse.call(this); + return this; + }; - function UACPU () { - this.data = UAItem.createUAData({ - init_props : [ARCHITECTURE], - is_ignoreProps : [], - toString_props : [ARCHITECTURE] - }); + function UACPU (ua, cpuMap) { + UAItem.call(this, [ + ua, + cpuMap, + [ARCHITECTURE], + [], + null, + [ARCHITECTURE] + ]); } UACPU.prototype = new UAItem(); + UACPU.prototype.parse = function (uach) { + if (uach) { + var archName = uach[CH_HEADER_ARCH]; + archName += (archName && uach[CH_HEADER_BITNESS] == '64') ? '64' : EMPTY; + rgxMapper.call(this.data, archName, this.rgxMap); + } + if (!this.get(ARCHITECTURE)) UAItem.prototype.parse.call(this); + return this; + }; - function UADevice () { - this.data = UAItem.createUAData({ - init_props : [TYPE, MODEL, VENDOR], - is_ignoreProps : [], - toString_props : [VENDOR, MODEL] - }); + function UADevice (ua, deviceMap) { + UAItem.call(this, [ + ua, + deviceMap, + [TYPE, MODEL, VENDOR], + [], + null, + [VENDOR, MODEL] + ]); } UADevice.prototype = new UAItem(); + UADevice.prototype.parse = function (uach) { + if (uach) { + this.set(TYPE, uach[CH_HEADER_MOBILE] == '?1' ? + MOBILE : + this.get(TYPE)) + .set(MODEL, uach[CH_HEADER_MODEL] ? + uach[CH_HEADER_MODEL].replace(/\"/g, EMPTY) : + this.get(MODEL)); + } + if (!this.get(TYPE) && !this.get(MODEL)) UAItem.prototype.parse.call(this); + return this; + }; - function UAEngine () { - this.data = UAItem.createUAData({ - init_props : [NAME, VERSION], - is_ignoreProps : [VERSION], - toString_props : [NAME, VERSION] - }); + function UAEngine (ua, engineMap) { + UAItem.call(this, [ + ua, + engineMap, + [NAME, VERSION], + [VERSION], + null, + [NAME, VERSION] + ]); } UAEngine.prototype = new UAItem(); - function UAOS () { - this.data = UAItem.createUAData({ - init_props : [NAME, VERSION], - is_ignoreProps : [VERSION], - is_ignoreRgx : ' ?os$', - toString_props : [NAME, VERSION] - }); + function UAOS (ua, osMap) { + UAItem.call(this, [ + ua, + osMap, + [NAME, VERSION], + [VERSION], + ' ?os$', + [NAME, VERSION] + ]); } UAOS.prototype = new UAItem(); - - function UAResult () { - this.data = { - ua : '', - browser : undefined, - cpu : undefined, - device : undefined, - engine : undefined, - os : undefined - }; - } - UAResult.prototype = new UAItem(); + UAOS.prototype.parse = function (uach) { + if (uach) { + var osName = uach[CH_HEADER_PLATFORM] ? uach[CH_HEADER_PLATFORM].replace(/\"/g, EMPTY) : undefined; + var osVersion = uach[CH_HEADER_PLATFORM_VER] ? uach[CH_HEADER_PLATFORM_VER].replace(/\"/g, EMPTY) : undefined; + osVersion = (osName == WINDOWS) ? (majorize(osVersion) >= 13 ? '11' : '10') : osVersion; + this.set(NAME, osName) + .set(VERSION, osVersion); + } + if (!this.get(NAME)) UAItem.prototype.parse.call(this); + if (/(chrome |mac)os/.test(this.get(NAME))) { + this.set(NAME, this.get(NAME) + .replace(/chrome os/i, CHROMIUM_OS) + .replace(/macos/i, MAC_OS)); + } + return this; + }; function UAParser (ua, extensions, headers) { @@ -908,6 +978,7 @@ extensions = ua; // case UAParser(extensions) } else { headers = ua; // case UAParser(headers) + extensions = undefined; } ua = undefined; } else if (typeof ua === STR_TYPE && !isExtensions(extensions)) { @@ -918,75 +989,114 @@ if (!(this instanceof UAParser)) { return new UAParser(ua, extensions, headers).getResult(); } - var _navigator = (typeof window !== UNDEF_TYPE && window.navigator) ? window.navigator : undefined; - // _ua = user-supplied string || window.navigator.userAgent || user-agent header || empty - var _ua = ua || ((_navigator && _navigator.userAgent) ? _navigator.userAgent : (!ua && headers && headers[USER_AGENT] ? headers[USER_AGENT] : EMPTY)); - var _uach = (_navigator && _navigator.userAgentData) ? _navigator.userAgentData : undefined; - var _rgxmap = extensions ? extend(regexes, extensions) : regexes; + var navigator = (typeof window !== UNDEF_TYPE && window.navigator) ? + window.navigator : + undefined, + + userAgent = ua || + ((navigator && navigator.userAgent) ? + navigator.userAgent : + (headers && headers[USER_AGENT] ? + headers[USER_AGENT] : + EMPTY)), + + clientHints = (navigator && navigator.userAgentData) ? + navigator.userAgentData : + undefined, + + regexMap = extensions ? + extend(regexes, extensions) : + regexes; // public methods this.getBrowser = function () { - var _browser = new UABrowser(); - _browser.parse(_ua, _rgxmap.browser); - _browser.set(MAJOR, majorize(_browser.get(VERSION))); - // Brave-specific detection - if (_navigator && _navigator.brave && typeof _navigator.brave.isBrave == FUNC_TYPE) { - _browser.set(NAME, 'Brave'); + var browser = new UABrowser(userAgent, regexMap.browser); + if (headers && (headers[CH_HEADER_FULL_VER_LIST] || headers[CH_HEADER])) { + browser.parse(headers); + } else { + browser.parse(); + // Brave-specific detection + if (navigator && navigator.brave && typeof navigator.brave.isBrave == FUNC_TYPE) { + browser.set(NAME, 'Brave'); + } } - return _browser.get(); + return browser + .set(MAJOR, majorize(browser.get(VERSION))) + .get(); }; + this.getCPU = function () { - var _cpu = new UACPU(); - _cpu.parse(_ua, _rgxmap.cpu); - return _cpu.get(); + var cpu = new UACPU(userAgent, regexMap.cpu); + if (headers && headers[CH_HEADER_ARCH]) { + cpu.parse(headers); + } else { + cpu.parse(); + } + return cpu.get(); }; + this.getDevice = function () { - var _device = new UADevice(); - _device.parse(_ua, _rgxmap.device); - if (!_device.get(TYPE) && _uach && _uach.mobile) { - _device.set(TYPE, MOBILE); + var device = new UADevice(userAgent, regexMap.device); + if (headers && (headers[CH_HEADER_MOBILE] || headers[CH_HEADER_MODEL])) { + device.parse(headers); + } else { + device.parse(); + if (!device.get(TYPE) && clientHints && clientHints.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); + } } - // 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'); - _device.set(TYPE, TABLET); - } - return _device.get(); + return device.get(); }; + this.getEngine = function () { - var _engine = new UAEngine(); - _engine.parse(_ua, _rgxmap.engine); - return _engine.get(); + return new UAEngine(userAgent, regexMap.engine) + .parse() + .get(); }; + this.getOS = function () { - var _os = new UAOS(); - _os.parse(_ua, _rgxmap.os); - if (!_os.get(NAME) && _uach && _uach.platform != 'Unknown') { - _os.set(NAME, _uach.platform - .replace(/chrome os/i, CHROMIUM_OS) - .replace(/macos/i, MAC_OS)); // backward compatibility + var os = new UAOS(userAgent, regexMap.os); + if (headers && headers[CH_HEADER_PLATFORM]) { + os.parse(headers); + } else { + os.parse(); + if (!os.get(NAME) && clientHints && clientHints.platform != 'Unknown') { + os.set(NAME, clientHints.platform + .replace(/chrome os/i, CHROMIUM_OS) + .replace(/macos/i, MAC_OS)); // backward compatibility + } } - return _os.get(); + return os.get(); }; + this.getResult = function () { - var _result = new UAResult(); - _result.set('ua', _ua); - _result.set('browser', this.getBrowser()); - _result.set('cpu', this.getCPU()); - _result.set('device', this.getDevice()); - _result.set('engine', this.getEngine()); - _result.set('os', this.getOS()); - return _result.get(); + return { + 'ua' : userAgent, + 'browser' : this.getBrowser(), + 'cpu' : this.getCPU(), + 'device' : this.getDevice(), + 'engine' : this.getEngine(), + 'os' : this.getOS() + }; }; + this.getUA = function () { - return _ua; + return userAgent; }; + this.setUA = function (ua) { - _ua = (typeof ua === STR_TYPE && ua.length > UA_MAX_LENGTH) ? trim(ua, UA_MAX_LENGTH) : ua; + userAgent = (typeof ua === STR_TYPE && ua.length > UA_MAX_LENGTH) ? trim(ua, UA_MAX_LENGTH) : ua; return this; }; - this.setUA(_ua); + + this.setUA(userAgent); return this; } diff --git a/test/test.js b/test/test.js index 339f3c3..88c2522 100644 --- a/test/test.js +++ b/test/test.js @@ -335,4 +335,68 @@ describe('Read user-agent data from req.headers', function () { let engine = UAParser(req.headers).engine; assert.strictEqual(engine.name, "EdgeHTML"); }); +}); + +describe('Map UA-CH headers', function () { + + const headers = { + 'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"', + 'sec-ch-ua-full-version-list' : '"Chromium";v="93.0.1.2", "Google Chrome";v="93.0.1.2", " Not;A Brand";v="99.0.1.2"', + 'sec-ch-ua-arch' : 'ARM', + 'sec-ch-ua-bitness' : '64', + 'sec-ch-ua-mobile' : '?1', + 'sec-ch-ua-model' : 'Pixel 99', + 'sec-ch-ua-platform' : 'Windows', + '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' + }; + + 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 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 () { + + assert.strictEqual(browser.name, "Chrome"); + assert.strictEqual(browser.version, "93.0.1.2"); + assert.strictEqual(browser.major, "93"); + assert.strictEqual(cpu.architecture, "arm64"); + assert.strictEqual(device.type, "mobile"); + assert.strictEqual(device.model, "Pixel 99"); + assert.strictEqual(device.vendor, undefined); + assert.strictEqual(engine.name, 'Blink'); + assert.strictEqual(engine.version, '110.0.0.0'); + assert.strictEqual(os.name, "Windows"); + assert.strictEqual(os.version, "11"); + }); + + it('Can read from user-agent header', function () { + + uap = UAParser(headers2); + browser = uap.browser; + cpu = uap.cpu; + device = uap.device; + engine = uap.engine; + os = uap.os; + + 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"); + }); }); \ No newline at end of file