node-device-detector
Version:
Nodejs device detector (port matomo-org/device-detector)
493 lines (436 loc) • 13.8 kB
JavaScript
const ClientAbstractParser = require('./../client-abstract-parser');
const CLIENT_TYPE = require('./../const/client-type');
const BROWSER_FAMILIES = require('./browser-families');
const ArrayPath = require('./../../lib/array-path');
const helper = require('./../helper');
const BrowserHints = require('./hints/browser-hints');
const BROWSER_SHORT = helper.revertObject(require('./browser-short'));
const browserHints = new BrowserHints;
const CLIENTHINT_MAPPING = {
'Chrome': ['Google Chrome'],
'Chrome Webview': ['Android WebView'],
'DuckDuckGo Privacy Browser': ['DuckDuckGo'],
'Edge WebView': ['Microsoft Edge WebView2'],
'Mi Browser': ['Miui Browser', 'XiaoMiBrowser'],
'Microsoft Edge': ['Edge'],
'Norton Private Browser': ['Norton Secure Browser'],
'Vewd Browser': ['Vewd Core'],
};
const BROWSERHINT_SKIP_VERSION = ['A0', 'AL', 'HP', 'JR', 'MU', 'OM', 'OP', 'VR'];
const compareBrandForClientHints = (brand) => {
for (let brandName in CLIENTHINT_MAPPING) {
for (let mapBrand of CLIENTHINT_MAPPING[brandName]) {
if (brand.toLowerCase() === mapBrand.toLowerCase()) {
return brandName;
}
}
}
return brand;
};
class Browser extends ClientAbstractParser {
constructor() {
super();
this.engine_collection = require('../../regexes/client/browser_engine');
this.collection = require('../../regexes/client/browsers');
this.type = CLIENT_TYPE.BROWSER;
this.collectionLength = this.collection.length;
}
getCollectionBrowsers() {
return BROWSER_SHORT;
}
getAvailableBrowsers() {
return Object.keys(this.getCollectionBrowsers());
}
/**
* Generates the result for the parse method
*
* @param {string} userAgent
* @param {{}} data
* @param {{}} hint
* @param {{}} hash
* @returns {null|ResultClient}
*/
prepareParseResult(
userAgent,
data,
hint,
hash
) {
let type = CLIENT_TYPE.BROWSER;
let name = '';
let version = '';
let engine = '';
let engineVersion = '';
let short = '';
let family = '';
// use client hints in favor of user agent data if possible
if (hint && hint.name && hint.version) {
name = hint.name;
version = hint.version;
short = hint.short_name;
family = this.buildFamily(short);
// If the version reported from the client hints is YYYY or YYYY.MM (e.g., 2022 or 2022.04),
// then it is the Iridium browser
// https://iridiumbrowser.de/news/
if (/^202[0-4]/.test(version)) {
name = 'Iridium';
short = 'I1';
}
if (data) {
// https://bbs.360.cn/thread-16096544-1-1.html
if (/^15/.test(version) && /^114/.test(data.version)) {
name = '360 Secure Browser';
short = '3B';
engine = data.engine;
engineVersion = data.engine_version;
}
// If client hints report the following browsers, we use the version from useragent
if (data.version && BROWSERHINT_SKIP_VERSION.indexOf(short) !== -1) {
version = data.version;
}
if ('Vewd Browser' === name) {
engine = data.engine;
engineVersion = data.engine_version;
}
// If client hints report Chromium, but user agent detects a Chromium based browser, we favor this instead
const hasChromeBased = ('Chromium' === name || 'Chrome Webview' === name) && data.name
&& ['CR', 'CV', 'AN'].indexOf(data.short_name) === -1;
if (hasChromeBased) {
name = data.name;
short = data.short_name;
version = data.version;
family = this.buildFamily(short);
}
// Fix mobile browser names e.g. Chrome => Chrome Mobile
if (name + ' Mobile' === data.name) {
name = data.name;
short = data.short_name;
}
// If user agent detects another browser, but the family matches, we use the detected engine from user agent
if (name !== data.name && family === this.buildFamily(data.short_name)) {
engine = data.engine;
engineVersion = data.engine_version;
}
// If the browser name matches the client hints then browser engine overwrite
if (name === data.name) {
engine = data.engine;
engineVersion = data.engine_version;
}
// In case the user agent reports a more detailed version, we try to use this instead
if (data.version && data.version.indexOf(version) === 0 && helper.versionCompare(version, data.version) < 0) {
version = data.version;
}
}
// If DDG Private browser then set version empty string
if ('DuckDuckGo Privacy Browser' === name) {
version = '';
}
// In case client hints report a more detailed engine version, we try to use this instead
if ('Blink' === engine && 'Iridium' !== name && helper.versionCompare(engineVersion, hint.version) < 0) {
engineVersion = hint.version;
}
} else if (data !== null) {
name = data.name;
version = data.version;
short = data.short_name;
engine = data.engine;
engineVersion = data.engine_version;
}
family = this.buildFamily(short);
// is app id
if (hash !== null && name !== hash.name) {
name = hash.name;
version = '';
short = this.buildShortName(name);
if (/Chrome\/.+ Safari\/537.36/.test(userAgent)) {
engine = 'Blink';
family = this.buildFamily(short) || 'Chrome';
engineVersion = this.buildEngineVersion(userAgent, engine);
}
}
// exclude Blink engine version for browsers
if ('Blink' === engine && 'Flow Browser' === name) {
engineVersion = '';
}
// the browser simulate ua for Android OS
if ('Every Browser' === name) {
family = 'Chrome';
engine = 'Blink';
engineVersion = '';
}
// This browser simulates user-agent of Firefox
if ('TV-Browser Internet' === name && 'Gecko' === engine) {
family = 'Chrome';
engine = 'Blink';
engineVersion = '';
}
if ('Wolvic' === name) {
if ('Blink' === engine) {
family = 'Chrome';
}
if ('Gecko' === engine) {
family = 'Firefox';
}
}
if (name === '') {
return null;
}
return {
type: String(type),
name: String(name),
short_name: String(short),
version: String(version),
engine: String(engine),
engine_version: String(engineVersion),
family: String(family)
};
}
/**
* General method for parsing user-agent and client-hints
*
* @param {string} userAgent
* @param {} clientHints
* @returns {ResultClient|null}
*/
parse(userAgent, clientHints) {
userAgent = this.prepareUserAgent(userAgent);
const hash = this.parseFromHashHintsApp(clientHints);
const hint = this.parseFromClientHints(clientHints);
const data = this.parseFromUserAgent(userAgent, clientHints);
return this.prepareParseResult(userAgent, data, hint, hash);
}
/**
* Parses client-hints for getting the browser name from the application ID (clientHints.app)
*
* @param {ResultClientHints} clientHints
* @return {{name: string}}
*/
parseFromHashHintsApp(clientHints) {
return browserHints.parse(clientHints);
}
/**
* Parses client-hints for getting browser name, version, short name
*
* @param {ResultClientHints} clientHints
* @return {{name: string, short_name: string, version: string}}
*/
parseFromClientHints(clientHints) {
let name = '';
let short = '';
let version = '';
if (clientHints && clientHints.client) {
const brands = ArrayPath.get(clientHints, 'client.brands', []);
for (let brandItem of brands) {
let brand = compareBrandForClientHints(brandItem.brand);
for (let browserName in this.getCollectionBrowsers()) {
let shortName = this.getCollectionBrowsers()[browserName];
let found = helper.fuzzyCompare(`${brand}`, browserName)
|| helper.fuzzyCompare(`${brand} Browser`, browserName)
|| helper.fuzzyCompare(`${brand}`, browserName + ' Browser');
if (found) {
name = String(browserName);
short = String(shortName);
version = String(brandItem.version);
break;
}
}
// If we detected a brand, that is not chromium,
// we will use it, otherwise we will look further
if ('' !== name && 'Chromium' !== name && 'Microsoft Edge' !== name) {
break;
}
}
if (clientHints.client.version) {
version = String(clientHints.client.version);
}
}
return {
name: name,
short_name: short,
version: this.buildVersion(version, [])
};
}
/**
* Inline parse userAgent by position collection
*
* @param {string} userAgent
* @param {number} position
* @return {{engine: *, name: string, short_name: *, engine_version: *, family: string, version: string}|null}
*/
parseUserAgentByPosition(userAgent, position = 0) {
let item = this.collection[position];
if (item === void 0) {
return null;
}
let regex = this.getBaseRegExp(item.regex);
let match = regex.exec(userAgent);
if (match !== null) {
let name = this.buildByMatch(item.name, match);
name = this.buildName(name);
let version = this.buildVersion(item.version, match);
let short = this.buildShortName(name);
let engine = this.buildEngine(
item.engine !== void 0 ? item.engine : {},
version
);
if (engine === '') {
engine = this.parseEngine(userAgent);
}
let engineVersion = this.buildEngineVersion(userAgent, engine);
let family = this.buildFamily(short);
return {
name: name,
short_name: String(short),
version: version,
engine: String(engine),
engine_version: String(engineVersion),
family: family
};
}
return null;
}
parseFromUserAgent(userAgent) {
if (!userAgent) {
return null;
}
return super.parse(userAgent, {});
}
/**
* normalisation browser name from any case
*
* @param {string} name
* @returns {string}
*/
buildName(name) {
let result = name;
let normalName = this.getCollectionBrowsers()[name];
if (normalName === void 0) {
let lname = name.toLowerCase();
let browsers = this.getAvailableBrowsers();
for (let i = 0, l = browsers.length; i < l; i++) {
if (lname === browsers[i].toLowerCase()) {
result = browsers[i];
break;
}
}
}
return result;
}
/**
* Get short code browser for full name browser
*
* @param {string} name
* @return {string}
*/
buildShortName(name) {
const UNKNOWN = 'UNK';
let result = this.getCollectionBrowsers()[name];
if (result !== void 0) {
return result;
}
return UNKNOWN;
}
/**
* Get browser family for short name browser
*
* @param {string} shortName
* @returns {string}
*/
buildFamily(shortName) {
for (let browserFamily in BROWSER_FAMILIES) {
if (
browserFamily &&
BROWSER_FAMILIES[browserFamily] &&
BROWSER_FAMILIES[browserFamily].indexOf(shortName) !== -1
) {
return browserFamily;
}
}
return '';
}
/**
* Get browser engine for engine name and versions
*
* @param {string} engine
* @param {string} browserVersion
* @return {string}
*/
buildEngine(engine, browserVersion) {
let result = '';
if (engine.hasOwnProperty('default') && engine.default !== '') {
result = engine.default;
}
if (engine.hasOwnProperty('versions')) {
let versions = Object.keys(engine.versions).sort(helper.versionCompare);
for (let i = 0, l = versions.length; i < l; i++) {
if (
browserVersion !== '' &&
helper.versionCompare(browserVersion, versions[i]) >= 0
) {
result = engine.versions[versions[i]];
}
}
}
return result;
}
/**
* Get engine name for parser engines
*
* @param {string} userAgent
* @returns {string}
*/
parseEngine(userAgent) {
let result = '';
for (let i = 0, l = this.engine_collection.length; i < l; i++) {
let item = this.engine_collection[i];
let regex = this.getBaseRegExp(item.regex);
let match = regex.exec(userAgent);
if (match !== null) {
result = item.name;
break;
}
}
return result;
}
/**
* Get engine version for useragent and engine name
*
* @param {string} userAgent
* @param {string} engine
* @return {string}
*/
buildEngineVersion(userAgent, engine) {
if (engine === '') {
return '';
}
if (engine === 'Gecko' || engine === 'Clecko') {
let pattern = '[ ](?:rv[: ])([0-9.]+)';
let regexp = new RegExp(pattern, 'i');
let match = regexp.exec(userAgent);
if (match !== null && /(?:g|cl)ecko\/[0-9]{8,10}/i.test(userAgent)) {
return match[1];
}
}
let engineToken = '' + engine;
if ('Blink' === engine) {
engineToken = 'Chr[o0]me|Chromium|Cronet';
}
if ('LibWeb' === engine) {
engineToken = 'LibWeb\\+LibJs\\/';
}
if ('Arachne' === engine) {
engineToken = 'Arachne\\/5\\.';
}
let regexp = new RegExp(
'(?:' + engineToken + ')' +
'\\s*[/_]?\\s*(((?=\\d+\\.\\d)\\d+[.\\d]*|\\d{1,7}(?=(?:\\D|$))))',
'i'
);
let match = regexp.exec(userAgent);
if (match !== null) {
return match[1]
}
return '';
}
}
module.exports = Browser;