UNPKG

node-device-detector

Version:

Nodejs device detector (port matomo-org/device-detector)

475 lines (428 loc) 11.7 kB
const CLIENT_TV_LIST = require('./const/clients-tv'); /** * restore original userAgent from clientHints object * @param {string} userAgent * @param {ResultClientHints} clientHints */ function restoreUserAgentFromClientHints(userAgent, clientHints) { if (!hasDeviceModelByClientHints(clientHints)) { return userAgent; } const deviceModel = clientHints.device.model; if (deviceModel === '') { return userAgent; } let newUserAgent = '' + userAgent; if (hasUserAgentClientHintsFragment(newUserAgent)) { const osHints = attr(clientHints, 'os', {}); const osVersion = attr(osHints, 'version', ''); newUserAgent = newUserAgent.replace(/(Android (?:10[.\d]*; K|1[1-5]))/, `Android ${osVersion !== '' ? osVersion : '10'}; ${deviceModel}` ); } if (!hasDesktopFragment(newUserAgent)) { return newUserAgent; } newUserAgent = newUserAgent.replace(/(X11; Linux x86_64)/, `X11; Linux x86_64; ${deviceModel}` ); return newUserAgent; } /** * match for base regex rule * @param str * @param userAgent * @returns {RegExpExecArray} */ function matchUserAgent(str, userAgent) { let regex = '(?:^|[^A-Z_-])(?:' + normalizeRegExp(str) + ')'; let match = new RegExp(regex, 'i'); return match.exec(userAgent); } function normalizeRegExp(regex) { return '' + regex.replace(new RegExp('/', 'g'), '\\/') .replace(new RegExp('\\+\\+', 'g'), '+'); } function getBaseRegExp(str) { const regex= '(?:^|[^A-Z0-9_-]|[^A-Z0-9-]_|sprd-|MZ-)(?:' + normalizeRegExp(str) + ')'; return new RegExp(regex, 'i'); } function matchReplace(template, matches) { let max = matches.length-1 || 1; if (template.indexOf('$') !== -1) { for (let nb = 1; nb <= max; nb++) { if (template.indexOf('$' + nb) === -1) { continue; } let replace = matches[nb] !== void 0 ? matches[nb] : ''; template = template.replace(new RegExp('\\$' + nb, 'g'), replace); } } return template; } /** * @param {string|null} val1 * @param {string|null} val2 * @return {boolean} */ function fuzzyCompare(val1, val2) { return val1 !== null && val2 !== null && val1.replace(/ /gi, '').toLowerCase() === val2.replace(/ /gi, '').toLowerCase(); } /** * @param {number|string} value1 * @param {number|string} value2 * @param {number} num * @return {boolean} */ function fuzzyCompareNumber(value1, value2, num = 3) { return parseFloat(value1).toFixed(num) === parseFloat(value2).toFixed(num); } /** * @param {number} value * @param {number} min * @param {number} max * @return {boolean} */ function fuzzyBetweenNumber(value, min, max) { return value >= min && value <= max; } /** * create hash from string * @param {string} str * @return {string} */ function createHash(str) { let hash = 0, i = 0, len = str.length; while (i < len) { hash = ((hash << 5) - hash + str.charCodeAt(i++)) << 0; } return hash.toString(16); } /** * Compare versions * @param {string} ver1 * @param {string} ver2 * @return {number} */ function versionCompare(ver1, ver2) { if (ver1 === ver2) { return 0; } let left = ver1.split('.'); let right = ver2.split('.'); let len = Math.min(left.length, right.length); for (let i = 0; i < len; i++) { if (left[i] === right[i]) { continue; } if (parseInt(left[i]) > parseInt(right[i])) { return 1; } if (parseInt(left[i]) < parseInt(right[i])) { return -1; } } if (left.length > right.length) { return 1; } if (left.length < right.length) { return -1; } return 0; } /** * @param {string} version * @param {*} maxMinorParts - how many version chars trim * @returns {string} */ function versionTruncate(version, maxMinorParts) { let versionParts = String(version).split('.'); if ( maxMinorParts !== null && maxMinorParts !== '' && versionParts.length > maxMinorParts ) { versionParts = versionParts.slice(0, 1 + maxMinorParts); } return versionParts.join('.'); } /** * @param {string} userAgent * @return {boolean} */ function hasAndroidTableFragment(userAgent) { return ( matchUserAgent('Android( [.0-9]+)?; Tablet;|Tablet(?! PC)|.*-tablet$', userAgent) !== null ); } /** * @param {string} userAgent * @return {boolean} */ function hasOperaTableFragment(userAgent) { return matchUserAgent('Opera Tablet', userAgent) !== null; } /** * Checks for the presence of a string in the UATouch * @param {string} userAgent * @returns {boolean} */ function hasTouchFragment(userAgent) { return matchUserAgent('Touch', userAgent) !== null; } function hasVRFragment(userAgent) { return matchUserAgent('Android( [.0-9]+)?; Mobile VR;| VR ', userAgent) !== null } /** * @param {string} userAgent * @returns {boolean} */ function hasAndroidMobileFragment(userAgent) { return matchUserAgent('Android( [.0-9]+)?; Mobile;|.*-mobile$', userAgent) !== null; } /** * All devices running Opera TV Store are assumed to be a tv * @param {string} userAgent * @return {boolean} */ function hasOperaTVStoreFragment(userAgent) { return matchUserAgent('Opera TV Store| OMI/', userAgent) !== null; } /** * All devices running Puffin Secure Browser that contain letter 'D' are assumed to be desktops * @param {string} userAgent * @return {boolean} */ function hasPuffinDesktopFragment(userAgent) { return matchUserAgent('Puffin/(?:\\d+[.\\d]+)[LMW]D', userAgent) !== null } /** * All devices running Puffin Web Browser that contain letter 'P' are assumed to be smartphones * @param {string} userAgent * @return {boolean} */ function hasPuffinSmartphoneFragment(userAgent) { return matchUserAgent('Puffin/(?:\\d+[.\\d]+)[AIFLW]P', userAgent) !== null } /** * All devices running Puffin Web Browser that contain letter 'T' are assumed to be tablets * @param {string} userAgent * @return {boolean} */ function hasPuffinTabletFragment(userAgent) { return matchUserAgent('Puffin/(?:\\d+[.\\d]+)[AILW]T', userAgent) !== null } /** * All devices containing TV fragment are assumed to be a tv * @param {string} userAgent * @return {boolean} */ function hasAndroidTVFragment(userAgent) { return matchUserAgent( 'Andr0id|(?:Android(?: UHD)?|Google) TV|\\(lite\\) TV|BRAVIA|Firebolt| TV$', userAgent ) !== null; } /** * All devices running Tizen TV or SmartTV are assumed to be a tv * @param {string} userAgent * @return {boolean} */ function hasTVFragment(userAgent) { return matchUserAgent('SmartTV|Tizen.+ TV .+$', userAgent) !== null; // |\\(TV; } /** * Check combinations in string that relate only to desktop UA * @param {string} userAgent * @returns {boolean} */ function hasDesktopFragment(userAgent) { const DESKTOP_PATTERN = '(?:Windows (?:NT|IoT)|X11; Linux x86_64)'; const DESKTOP_EXCLUDE_PATTERN = [ 'CE-HTML', ' Mozilla/|Andr[o0]id|Tablet|Mobile|iPhone|Windows Phone|ricoh|OculusBrowser', 'PicoBrowser|Lenovo|compatible; MSIE|Trident/|Tesla/|XBOX|FBMD/|ARM; ?([^)]+)', ].join('|'); return getBaseRegExp(DESKTOP_PATTERN).exec(userAgent) !== null && !(getBaseRegExp(DESKTOP_EXCLUDE_PATTERN).exec(userAgent) !== null); } function hasTVClient(name) { return CLIENT_TV_LIST.indexOf(name) !== -1 } /** * Check combinations is string that UserAgent ClientHints * @param {string} userAgent * @return {boolean} */ function hasUserAgentClientHintsFragment(userAgent) { return /Android (?:10[.\d]*; K(?: Build\/|[;)])|1[1-5]\)) AppleWebKit/i.test(userAgent); } /** * * @param {ResultClientHints|*} clientHints * @return {boolean} */ function hasDeviceModelByClientHints(clientHints) { return clientHints && clientHints.device && clientHints.device.model; } /** * @param {string} name * @return {boolean} */ function hasDeviceModelWrong (name) { return !!(name === '' || ['LeafOS on ARM64'].includes(name)); } /** * Get value by attribute for object or default value * @param {object} options * @param {string }propName * @param {*} defaultValue * @return {*|null} */ function attr(options, propName, defaultValue) { return options !== void 0 && options[propName] !== void 0 ? options[propName] : defaultValue !== void 0 ? defaultValue : null; } /** * Values become keys, and keys become values * @param {*} obj - * @returns {*} */ function revertObject(obj) { return Object.assign( {}, ...Object.entries(obj).map(([a, b]) => ({ [b]: a, })), {}, ); } /** * Remove chars for string * @param {string} str * @param {string} chars * @returns {any} */ function trimChars(str, chars) { let start = 0, end = str.length; while (start < end && str[start] === chars) ++start; while (end > start && str[end - 1] === chars) --end; return (start > 0 || end < str.length) ? str.substring(start, end) : str; } function getGroupForUserAgentTokens(tokens) { let groupIndex = 0; return tokens.reduce((group = {}, token) => { if (token === '') { return; } let data = token.match(/^\((.*)\)$/); if (data !== null) { groupIndex++; group['#' + groupIndex] = data[1].split(/[;,] /); return group; } let rowSlash = token.split('/'); if (rowSlash.length === 2) { group[rowSlash[0]] = rowSlash[1]; return group; } groupIndex++; group['#' + groupIndex] = token; return group; }, {}); } function getTokensForUserAgent(userAgent) { return userAgent.split(/ (?![^(]*\))/i); } /** * Split UserAgent to tokens and groups * * @param userAgent * @returns {{groups: *, userAgent: *, tokens: *}} */ function splitUserAgent(userAgent) { const tokens = getTokensForUserAgent(userAgent); const groups = getGroupForUserAgentTokens(tokens); const parts = []; for (let key in groups) { if (typeof groups[key] !== 'string' || !groups[key]) { continue; } if (key && String(key).charAt(0) === '#') { if ( !groups[key].match(/[/;]/i) && !groups[key].match(/^\s*[\d.]+/i) ) { parts.push(String(groups[key]).toLowerCase()); continue; } continue; } parts.push(String(key).toLowerCase()); } const hash = createHash(parts.join('.')).replace('-', ''); const path = parts.join('.'); return {tokens, groups, hash, path}; } const OS_WEIGHTS = [ { regex: 'cfnetwork|darwin|mac os|apple ?tv', wt: 'apple general' }, { regex: 'watch ?os', wt: 'apple watch os'}, { regex: 'android|linux; andr0id|/tclwebkit', wt: 'android general' }, { regex: 'windows', wt: 'windows' }, { regex: '\\(x11;', wt: 'linux general'}, { regex: 'linux; ?tizen', wt: 'tizen'}, { regex: '[ \\(]web[0o]s', wt: 'webos general'}, ]; function splitOsUserAgent(userAgent) { const parts = []; for(const record of OS_WEIGHTS) { const match = new RegExp(normalizeRegExp(record.regex), 'i') if (match.exec(userAgent)) { parts.push(record.wt); } } const hash = createHash(parts.join('.')).replace('-', ''); const path = parts.join('.'); return {hash, path}; } module.exports = { hasTVClient, matchUserAgent, fuzzyCompare, fuzzyCompareNumber, fuzzyBetweenNumber, versionCompare, versionTruncate, hasVRFragment, hasAndroidTableFragment, hasOperaTableFragment, hasOperaTVStoreFragment, hasAndroidMobileFragment, hasAndroidTVFragment, hasDesktopFragment, hasUserAgentClientHintsFragment, hasDeviceModelByClientHints, hasTVFragment, hasTouchFragment, attr, revertObject, trimChars, splitUserAgent, splitOsUserAgent, matchReplace, hasPuffinDesktopFragment, hasPuffinSmartphoneFragment, hasPuffinTabletFragment, hasDeviceModelWrong, getBaseRegExp, restoreUserAgentFromClientHints };