lib/detect/browser.js
'use strict';
/**
* @module Browser
* @desc Browser Detection - Gets Data Pertaining to User's Browser and OS
* @typicalname browser
* @example
* ```
* var browser = require("adlibs/lib/detect/browser")
* ```
*/
/**
* @type Image
* @property {String} srcset
*/
/**
* @type performance
* @property {Object} timing
* @property {Function} mark
* @ignore
*/
/**
* @type document
* @property {Number} uniqueID
* @property {String} hidden
* @property {Object} all
* @property {String} documentMode
* @property {String} visibilityState
* @property {Document} documentElement
* @property {Function} webkitRequestFullscreen
* @ignore
*/
/**
* @type navigator
* @property {String} platform
* @property {Object} mozGetUserMedia
* @property {Boolean} standalone
* @property {Object} geolocation
* @property {Object} mozBattery
* @property {Object} webkitGetUserMedia
* @property {String} userAgent
* @property {Function} vibrate
* @ignore
*/
/**
* @type browser
* @property {String} name
* @property {Boolean} trustworthy
* @property {Boolean} desktop
* @property {Boolean} mobile
* @property {Boolean} tablet
* @property {Boolean} console
* @property {String} version
* @property {UA} ua
* @property {Feature} feature
* @property {Engine} engine
* @property {OS} os
* @property {Function} setVersionAgainstUaVersion
*/
/**
* @type ua
* @property {String} version
*/
/**
* @type feature
* @property {String} version
*/
/**
* @type engine
* @property {String} name
* @property {String} version
*/
/**
* @type os
* @property {String} name
* @property {String} version
*/
/**
* @type style
* @property {Object} KhtmlUserInput
* @ignore
*/
/**
* @type URL
* @property {Function} createObjectURL
* @ignore
*/
/**
* @type history
* @property {Object} replaceState
* @ignore
*/
/* This is a separator to keep the JSDOC from going insane */
var can = require('../canHas').can,
has = require('../canHas').has,
kindleBrowser = /Kindle|Silk|KFTT|KFOT|KFJWA|KFJWI|KFSOWI|KFTHWA|KFTHWI|KFAPWA|KFAPWI/i,
results = [],
map = {},
DEFAULT_VERSION = -1,
MAX = {
EXCEED: 'ex',
OK: 'ok'
},
MAX_BROWSER_VER = 1000,
TYPE = {
MICROSOFT: 1,
FIREFOX: 2,
CHROME: 3,
OPERA: 4,
SAFARI: 5,
ANDROID: 6,
SAFARI_MOBILE: 7,
OPERA_MINI: 8,
OPERA_ANDROID: 9,
CHROME_MOBILE: 10,
MICROSOFT_MOBILE: 11,
FIREFOX_MOBILE: 12,
BLACKBERRY: 13,
KINDLE: 14,
WEBVIEW: 15,
UNKNOWN: 16,
UNKNOWN_MOBILE: 17
};
/**
* @private
* @returns {UA}
* @constructor
*/
var Ua = function () {
this.version = DEFAULT_VERSION;
};
/**
* @private
* @returns {Feature}
* @constructor
*/
var Feature = function () {
this.version = DEFAULT_VERSION;
};
/**
* @private
* @returns {Engine}
* @constructor
*/
var Engine = function () {
this.name = '';
this.version = DEFAULT_VERSION;
};
/**
* @private
* @returns {OS}
* @constructor
*/
var Os = function () {
this.name = '';
this.version = DEFAULT_VERSION + '';
};
/**
* @private
* @returns {Browser} Returns the Browser instance
* @constructor
*/
var Browser = function () {
var self = this; // here for minification purposes
self.name = ''; // verified name
self.trustworthy = true; // does the ua jive with the feature detection
self.desktop = false; // is this a desktop browser
self.mobile = false; // is this a mobile phone browser
self.tablet = false; // is this a mobile tablet browser
self.console = false; // is this a video game or other console browser
self.max = MAX.OK; // by default the ua version matches what is available
self.version = DEFAULT_VERSION; // full version
self.ua = new Ua();
self.feature = new Feature();
self.engine = new Engine();
self.os = new Os();
};
/**
* Check for MathML support in browsers to help detect certain browser version numbers where this is the only difference.
* @param {Document} d
* @static
* @returns {Boolean} returns true if browser has mathml support
*/
var mathMLSupport = function (d) {
var hasMathML = false;
if (d.createElementNS) {
var NAMESPACE = 'http://www.w3.org/1998/Math/MathML',
div = d.createElement('div'),
mfrac;
div.style.position = 'absolute';
div.style.top = div.style.left = 0;
div.style.visibility = 'hidden';
div.style.width = div.style.height = 'auto';
div.style.fontFamily = 'serif';
div.style.lineheight = 'normal';
mfrac = div.appendChild(d.createElementNS(NAMESPACE,'math'))
.appendChild(d.createElementNS(NAMESPACE,'mfrac'));
mfrac.appendChild(d.createElementNS(NAMESPACE,'mi'))
.appendChild(d.createTextNode('xx'));
mfrac.appendChild(d.createElementNS(NAMESPACE,'mi'))
.appendChild(d.createTextNode('yy'));
d.body.appendChild(div);
hasMathML = div.offsetHeight > div.offsetWidth;
}
return hasMathML;
};
/**
* Performs a simple test to see if we're on mobile or not.
* @param {Window=} win
* @static
* @returns {Boolean} returns true if mobile
*/
var isMobile = function (win) {
win = win || window;
try {
win.document.createEvent('TouchEvent');
// Surface tablets have touch events, so we use the Pointer Lock API to detect them
return !can(win.document, 'exitPointerLock') || !can(win.document, 'mozExitPointerLock');
} catch (e) {
// Opera Mini and IE10M don't support touch events
// execCommand is only on desktop browsers
return !can(win.document, 'execCommand');
}
};
/**
* Saves a property to the results array and returns its index.
* @param {*} result
* @returns {Number}
*/
var save = function (result) {
if (typeof result === 'boolean') {
results.push(result === true ? '1' : '0');
} else if (typeof result === 'number') {
results.push(result + '');
} else {
results.push(result);
}
return results.length - 1;
};
/**
* Uses the min and max versions of a browser to determine its version.
* @param {Number} uaVersion
* @param {Number} minVersion
* @param {Number=} maxVersion
* @static
* @returns {Number} returns version number
*/
var getVersion = function (uaVersion, minVersion, maxVersion) {
var actualVersion = minVersion;
if (uaVersion >= minVersion) {
if (!maxVersion || uaVersion <= maxVersion) {
actualVersion = uaVersion;
} else if (maxVersion && uaVersion > maxVersion) {
actualVersion = maxVersion;
}
}
return actualVersion;
};
/**
* Searches for a match between the regex and specified string.
* @param {RegExp} regex
* @param {String} ua
* @static
* @returns {*|Boolean} returns true if match found
*/
var looksLike = function (regex, ua) {
return regex.test(ua);
};
/**
* Parses the result of the RegExp match if it exists.
* Gracefully falls back to the default version if not.
* @param {String} ua
* @param {RegExp} regex
* @param {Number=} radix
* @static
* @returns {Number} returns the regex match or default version
*/
var parseIntIfMatch = function (ua, regex, radix) {
return ua.match(regex) !== null ? parseInt(ua.match(regex)[1], radix || 10) : DEFAULT_VERSION;
};
/**
* Parses the floating point value of the RegExp match if found.
* Gracefully falls back to the default if not.
* @param {String} ua
* @param {RegExp} regex
* @static
* @returns returns the regex match or the default version
*/
var parseFloatIfMatch = function (ua, regex) {
return ua.match(regex) !== null ? parseFloat(ua.match(regex)[1]) : DEFAULT_VERSION;
};
/**
* Determines the version of Android being used.
* @param {Window} win
* @param {Number} uaVersion
* @static
* @returns {Number} returns the Android version
*/
var getAndroidVersion = function (win, uaVersion) {
var nav = win.navigator,
androidVersion = DEFAULT_VERSION;
if (can(nav, 'sendBeacon')) {
androidVersion = getVersion(uaVersion, 5.0, Infinity);
} else if (can(has('performance', win), 'now')) {
androidVersion = getVersion(uaVersion, 4.4);
} else if (has('FileList', win)) {
androidVersion = getVersion(uaVersion, 4.0, 4.3);
} else {
androidVersion = getVersion(uaVersion, 2.1, 4.0);
}
return androidVersion;
};
/**
* Determines the version of Chrome being used.
* @param {Window} win
* @param {Number} uaVersion
* @static
* @returns {Number} returns the Chrome version
*/
var getChromiumVersion = function (win, uaVersion) {
var chromiumVersion = DEFAULT_VERSION;
// no session history management - version 4 - api
// geolocation - version 5 - api
// web notifications(prefixed) - version 5 - api
// server-sent DOM events - version 6 - api
// FileReader - version 6 - API
// inline svg - version 7 - html5
// typed arrays - version 7 - api
// classList DOMTokenList - version 8 - API
// defer attribute for external scripts - version 8 - html5
// progress meter - version 8 - html5
// matchMedia - version 9 - API
// webP image format, partial support - version 9 - other
// form validation - version 10 - html5
// getComputedStyle - version 11 - html5
// IndexedDB (prefixed)- version 11 - API
// Details and summary elements - version 12 - html5
// Navigation timing API - version 13 - API
// File API, full support - version 13 - API
// Web sockets, full support - version 14 - API
// Full screen API (prefixed)- version 15 - API
// WebGL, 3D Canvas Graphics - version 18 - API/HTML5 -- unreliable indicator
// WebVVT - version 18 - other
// Blob constructing - version 20 - API
// high resolution time api (prefixed) - version 20 - API
// Color input type - version 20 - HTML5
// getUserMedia API - version 21 - API
// web notifications - version 22 - api
// touch events - version 22 - API/HTML5
// Blob URLs - version 23 - API
// WebRTC Peer Connection (prefixed)- version 23 - API
// webP image format, full support - version 23 - other
// requestAnimationFrame - version 24 - API
// high resolution time api - version 24 - API
// User timing API - version 25 - API
// web speach api(prefixed) - version 25 - api
// shadow dom, prefixed - version 25 - html5
// HTML templates - version 26 - html5
// mutation observer - version 27 - API
// Canvas blend - version 30 - API
// vibration API - version 30 - API
// Promises - version 32 - api
// page visibility - version 33 - api
// Opus codec - version 33 - other
// matches() DOM method - version 34 - API
// srcset attribute - version 34 - api
// shadow dom - version 35 - api/html5
// toolbar/context menu - version 36 - html5
// scoped css - version 36 - html5
if (has('Proxy', win)) {
chromiumVersion = getVersion(uaVersion, 49, MAX_BROWSER_VER);
} else if (has('PushManager', win)) {
chromiumVersion = getVersion(uaVersion, 44, 48);
} else if (can(win.navigator, 'permissions')) {
chromiumVersion = getVersion(uaVersion, 43);
} else if (can(win.navigator, 'sendBeacon')) {
chromiumVersion = getVersion(uaVersion, 39, 42);
} else if (can(win.navigator, 'getBattery')) {
chromiumVersion = getVersion(uaVersion, 38);
} else if (can(has('crypto', win), 'subtle')) {
chromiumVersion = getVersion(uaVersion, 37);
} else if (can(new Image(), 'srcset')) { // Chrome 34+
chromiumVersion = getVersion(uaVersion, 34, 36);
} else if (can(win.document, 'visibilityState')) { // Chrome 33+
chromiumVersion = getVersion(uaVersion, 33);
} else if (has('Promise', win)) { // Chrome 32+
chromiumVersion = getVersion(uaVersion, 32);
} else if (can(win.navigator, 'vibrate')) { // Chrome 30+
chromiumVersion = getVersion(uaVersion, 30, 31);
} else if (has('MutationObserver', win)) { // Chrome 27+
chromiumVersion = getVersion(uaVersion, 27, 29);
} else if (can(win.document.createElement('template'), 'content')) { // Chrome 26+
chromiumVersion = getVersion(uaVersion, 26);
} else if (can(has('performance', win), 'mark')) { // Chrome 25+
chromiumVersion = getVersion(uaVersion, 25);
} else if (has('requestAnimationFrame', win)) { // Chrome 24+
chromiumVersion = getVersion(uaVersion, 24);
} else if (can(has('URL', win), 'createObjectURL')) { // Chrome 23+
chromiumVersion = getVersion(uaVersion, 23);
} else if (has('Notification', win)) { // Chrome 22+
chromiumVersion = getVersion(uaVersion, 22);
} else if (can(win.navigator, 'webkitGetUserMedia')) { // Chrome 21+
chromiumVersion = getVersion(uaVersion, 21);
} else if (has('Blob', win)) { // Chrome 20+
chromiumVersion = getVersion(uaVersion, 20);
} else if (can(win.document, 'webkitRequestFullscreen')) { // Chrome 15+
chromiumVersion = getVersion(uaVersion, 15, 19);
} else if (can(has('performance', win), 'timing')) { // Chrome 13+
chromiumVersion = getVersion(uaVersion, 13, 14);
} else if (can(win.document.createElement('details'), 'open')) { // Chrome 12+
chromiumVersion = getVersion(uaVersion, 12);
} else if (has('webkitIndexedDB', win)) { // Chrome 11+
chromiumVersion = getVersion(uaVersion, 11);
} else if (can(win.document.createElement('input'), 'checkValidity')) { // Chrome 10+
chromiumVersion = getVersion(uaVersion, 10);
} else if (has('matchMedia', win)) { // Chrome 9+
chromiumVersion = getVersion(uaVersion, 9);
} else if (can(win.document.createElement('_'), 'classList')) { // Chrome 8+
chromiumVersion = getVersion(uaVersion, 8);
} else if (has('Uint32Array', win)) { // Chrome 7+
chromiumVersion = getVersion(uaVersion, 7);
} else if (has('FileReader', win)) { // Chrome 6+
chromiumVersion = getVersion(uaVersion, 6);
} else if (has('webkitNotification', win)) { // Chrome 5+
chromiumVersion = getVersion(uaVersion, 5);
} else if (can(has('history', win), 'replaceState')) { // Chrome 4+
chromiumVersion = getVersion(uaVersion, 4);
} else {
chromiumVersion = getVersion(uaVersion, 0, 3);
}
return chromiumVersion;
};
/**
* @private
* @param {Window} win
* @param {Number} uaVersion
* @returns {Number}
*/
var getGeckoVersion = function (win, uaVersion) {
var geckoVersion = DEFAULT_VERSION,
d = win.document,
nav = win.navigator;
// Detect all versions of browsers who are using gecko as their rendering engine, prioritizing Firefox
// all versions:
// optimal version coverage:
// versions accounted for:
// versions covered by feature testing:
if (has('PushManager', win)) {
geckoVersion = getVersion(uaVersion, 44, MAX_BROWSER_VER);
} else if (has('MessageChannel', win)) {
geckoVersion = getVersion(uaVersion, 41, 43);
} else if (has('fetch', win)) {
geckoVersion = getVersion(uaVersion, 39, 40);
} else if (can(has('performance', win), 'mark')) {
geckoVersion = getVersion(uaVersion, 38);
} else if (can(has('crypto', win), 'subtle')) {
geckoVersion = getVersion(uaVersion, 34, 37);
} else if (can(win.navigator, 'sendBeacon')) {
geckoVersion = getVersion(uaVersion, 31, 33);
} else if (has('SharedWorker', win)) { // FF 29+
geckoVersion = getVersion(uaVersion, 29, 30);
} else if (has('AudioContext', win)) { // FF 25+
geckoVersion = getVersion(uaVersion, 25, 28);
} else if (has('requestAnimationFrame', win)) { // FF 23+
geckoVersion = getVersion(uaVersion, 23, 24);
} else if (has('Notification', win)) { // FF 22+
geckoVersion = getVersion(uaVersion, 22);
} else if (can(d, 'hidden')) { // FF 18+
geckoVersion = getVersion(uaVersion, 18, 21);
} else if (can(nav, 'mozGetUserMedia')) { // FF 17+
geckoVersion = getVersion(uaVersion, 17);
} else if (has('indexedDB', win)) { // FF 16+
geckoVersion = getVersion(uaVersion, 16);
} else if (can(has('performance', win), 'now')) { // FF 15+
geckoVersion = getVersion(uaVersion, 15);
} else if (has('MutationObserver', win)) { // FF 14+
geckoVersion = getVersion(uaVersion, 14);
} else if (has('Blob', win)) { // FF 13+
geckoVersion = getVersion(uaVersion, 13);
} else if (has('WebSocket', win)) { // FF 11+
geckoVersion = getVersion(uaVersion, 11, 12);
} else if (can(nav, 'mozBattery')) { // FF 10+
geckoVersion = getVersion(uaVersion, 10);
} else if (can(has('performance', win), 'timing')) { // FF 7+
geckoVersion = getVersion(uaVersion, 7, 9);
} else if (has('matchMedia', win)) { // FF 6+
geckoVersion = getVersion(uaVersion, 6);
} else if (has('Uint32Array', win)) { // FF 4+
geckoVersion = getVersion(uaVersion, 4, 5);
} else if (has('FileReader', win)) { // FF 3.6
geckoVersion = getVersion(uaVersion, 3.6);
} else if (has('JSON', win)) { // FF 3.5+
geckoVersion = getVersion(uaVersion, 3.5);
} else if (has('postMessage', win)) { // FF 3+
geckoVersion = getVersion(uaVersion, 3);
} else {
geckoVersion = getVersion(uaVersion, 0, 2.9);
}
return geckoVersion;
};
/**
* @private
* @param {Window} win
* @param {Number} uaVersion
* @returns {Number}
*/
var getTridentVersion = function (win, uaVersion) {
var tridentVersion = DEFAULT_VERSION,
d = win.document;
// Detect all IE versions possible by feature detection
// all versions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.2, 5.5, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0
// optimal version coverage: 6.0, 7.0, 8.0, 9.0, 10.0, 11.0
// versions accounted for: 3-11
// versions covered by feature testing: 3.0, 4.0, 5.0, 5.5, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0
if (can(d, 'pointerLockElement')) {
tridentVersion = getVersion(uaVersion, 13, MAX_BROWSER_VER);
} else if (has('Proxy', win)) {
tridentVersion = getVersion(uaVersion, 12);
} else if (has('MutationObserver', win)) { // IE 11+
tridentVersion = getVersion(uaVersion, 11);
} else if (has('atob', win)) { // IE 10+
tridentVersion = getVersion(uaVersion, 10);
} else if (has('addEventListener', win)) { // IE 9+
tridentVersion = getVersion(uaVersion, 9);
} else if (has('localStorage', win)) { // IE 8+
tridentVersion = getVersion(uaVersion, 8);
} else if (can(d, 'all') && has('XMLHttpRequest', win) && !has('XDomainRequest', win) && !has('opera', win)) { // IE 7
tridentVersion = getVersion(uaVersion, 7);
} else if (can(d, 'all') && !has('XMLHttpRequest', win)) { // IE 6
tridentVersion = getVersion(uaVersion, 6);
} else { // IE 3 - 5.5
tridentVersion = DEFAULT_VERSION;
}
return tridentVersion;
};
/**
* See https://en.wikipedia.org/wiki/Trident_(layout_engine)
* @private
* @param {Number} ver
* @returns {Number}
*/
var getTridentEngineVersion = function (ver) {
var engineVersion = DEFAULT_VERSION;
if (ver >= 11) {
engineVersion = 7;
} else if (ver === 10) {
engineVersion = 6;
} else if (ver === 9) {
engineVersion = 5;
} else if (ver === 8) {
engineVersion = 4;
} else if (ver <= 7) {
engineVersion = 3;
}
return engineVersion;
};
/**
* Returns the version of the Safari browser.
* @param {Window} win
* @param {Number} uaVersion
* @static
* @returns {Number} returns the version of Safari
*/
var getSafariVersion = function (win, uaVersion) {
var safariVersion = DEFAULT_VERSION,
d = win.document,
nav = win.navigator;
if (has('WebAssembly', win)) {
safariVersion = getVersion(uaVersion, 11.0, Infinity);
} else if (has ('Intl', win)) {
// 10.10 chosen to account for any possible patch releases in the future
safariVersion = getVersion(uaVersion, 10.0, 10.10);
} else if (can(has('CSS', win), 'supports')) {
safariVersion = getVersion(uaVersion, 9.0, 9.3);
} else if (has('indexedDB', win)) {
safariVersion = getVersion(uaVersion, 8.0, 8.4);
} else if (has('execCommand', d)) {
safariVersion = getVersion(uaVersion, 7.0, 7.1);
} else if (has('requestAnimationFrame', win)) {
safariVersion = getVersion(uaVersion, 6.0, 6.1);
} else if (has('Uint32Array', win)) {
// Safari 6533.18.5 - iOS 4.3.5
safariVersion = getVersion(uaVersion, 5.1);
} else if (can(nav, 'geolocation')) {
safariVersion = getVersion(uaVersion, 5.0);
} else if (can(nav, 'onLine')) {
safariVersion = getVersion(uaVersion, 4.2, 4.3);
} else if (has('JSON', win)) {
// Safari 6531.22.7 - iOS 4.0.2
safariVersion = getVersion(uaVersion, 4.0, 4.1);
} else if (has('postMessage', win)) {
// webkit 531.21.10 - iOS 3.2.2
// webkit 528.16 - iOS 3.1.3
safariVersion = getVersion(uaVersion, 3.2);
} else {
safariVersion = getVersion(uaVersion, 0, 3.1);
}
return safariVersion;
};
/**
* Creates a Browser instance with its attributed Kindle values.
* @param {Window} win
* @param {Number} uaVersion
* @static
* @returns {Number}
*/
var getKindleVersion = function (win, uaVersion) {
var kindleVersion = DEFAULT_VERSION,
d = win.document;
if (can(d, 'pointerLockElement')) {
kindleVersion = getVersion(uaVersion, 3.0, Infinity);
} else if (has('PerformanceTiming', win)) {
kindleVersion = getVersion(uaVersion, 2.0);
} else {
kindleVersion = getVersion(uaVersion, 1.0);
}
return kindleVersion;
};
/**
* Creates a Browser instance with its attributed OS and device type values.
* @param {Window} win
* @param {String} ua
* @static
* @returns {Browser} returns the browser instance
*/
var getOtherOS = function (win, ua) {
var otherBrowser = new Browser();
if (has('wiiu', win)) {
otherBrowser.os.name = 'Wii';
otherBrowser.os.version = 'U';
otherBrowser.name = 'NetFront';
otherBrowser.console = true;
} else if (looksLike(/Wii/i, ua)) {
otherBrowser.os.name = 'Wii';
otherBrowser.name = 'NetFront';
otherBrowser.console = true;
} else if (looksLike(/PlayStation.4/i, ua)) {
otherBrowser.os.name = 'PlayStation';
otherBrowser.os.version = '4';
otherBrowser.name = 'NetFront';
otherBrowser.console = true;
} else if (looksLike(/PlayStation/i, ua)) {
otherBrowser.os.name = 'PlayStation';
otherBrowser.os.version = '3';
otherBrowser.console = true;
} else if (looksLike(/NgetOtherOSokiaN/i, ua)) {
otherBrowser.os.name = 'Symbian';
otherBrowser.mobile = true;
} else if (looksLike(/blackberry|RIM/i, ua)) {
otherBrowser.os.name = 'Blackberry';
otherBrowser.mobile = true;
} else if (win.navigator && win.navigator.platform === 'X11' || looksLike(/Linux/i, ua)) {
otherBrowser.os.name = 'Linux';
otherBrowser.desktop = true;
} else {
otherBrowser.os.name = 'Unknown';
}
return otherBrowser;
};
/**
* Creates a Browser instance with its attributed Apple values.
* @param {Window} win
* @param {String} ua
* @static
* @returns {Browser} returns the Browser instance
*/
var getAppleOS = function (win, ua) {
var mac = /Mac/i,
iOS = /iPhone|iPad|iPod/i,
appleBrowser = new Browser(),
iOSVersion = DEFAULT_VERSION,
macVersion = DEFAULT_VERSION;
// Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53
// webviews will not have Safari in their user-agent string
if ((iOS.test(ua) || iOS.test(win.navigator.platform)) || !looksLike(/Safari|Firefox|Chrome/i, ua)) {
if (!looksLike(/Version\/\d\.\d/i, ua)) {
appleBrowser.ua.version = '2.0';
} else {
if (looksLike(/.OS.(\d.\d+)/i, ua)) {
iOSVersion = (ua.match(/.OS.(\d.\d+)/i)[1].replace('_','.'));
} else if (looksLike(/.Version\/(\d\.\d+)/i, ua)) {
iOSVersion = (ua.match(/.Version\/(\d\.\d+)/i)[1]);
}
}
appleBrowser.ua.version = appleBrowser.os.version = iOSVersion;
appleBrowser.os.name = 'iOS';
appleBrowser.tablet = /ipad/i.test(win.navigator.platform);
appleBrowser.mobile = /iphone|ipod/i.test(win.navigator.platform);
} else if (mac.test(ua) || mac.test(win.navigator.platform)) {
macVersion = parseIntIfMatch(ua, /Mac.OS.X.10.(\d+)/i, 10); // this format was introduced at 3.0+
if (macVersion > 0) {
appleBrowser.os.name = 'Mac';
appleBrowser.os.version = '10.' + macVersion;
}
appleBrowser.desktop = true;
}
return appleBrowser;
};
/**
* Creates a Browser instance with its attributed Windows values.
* @param {Window} win
* @param {String} ua
* @static
* @returns {Browser} returns the Browser instance
*/
var getMicrosoftOS = function (win, ua) {
var microsoftBrowser = new Browser();
if (looksLike(/XBox One/i, ua)) {
microsoftBrowser.os.name = 'Xbox';
microsoftBrowser.os.version = 'One';
microsoftBrowser.name = 'Internet Explorer';
microsoftBrowser.version = '10.0';
microsoftBrowser.console = true;
} else if (looksLike(/Xbox/i, ua)) {
microsoftBrowser.os.name = 'Xbox';
microsoftBrowser.os.version = '360';
microsoftBrowser.name = 'Internet Explorer';
microsoftBrowser.version = '7.0';
microsoftBrowser.console = true;
} else {
microsoftBrowser.os.name = 'Windows';
if (looksLike(/IEMobile/i, ua)) {
microsoftBrowser.mobile = true;
microsoftBrowser.os.name = 'Windows Phone';
if (looksLike(/Windows.Phone.(?:os)?\s?(\d\d?\.?\d?\d?)/i, ua)) {
microsoftBrowser.os.version = ua.match(/Windows.Phone.(?:os)?\s?(\d\d?\.?\d?\d?)/i)[1];
} else if (looksLike(/WP(\d\d?\.?\d?\d?)/i, ua)) {
microsoftBrowser.os.version = ua.match(/WP(\d\d?\.?\d?\d?)/i)[1];
}
} else if (looksLike(/Windows.NT./i, ua)) {
microsoftBrowser.desktop = true;
var pcVersion = parseFloatIfMatch(ua, /Windows.NT.(\d\d?\.?\d?\d?)/i); // this format was introduced at 3.0+
// List pulled from http://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx
switch (pcVersion) {
case 10:
microsoftBrowser.os.version = '10.0';
break;
case 6.3:
microsoftBrowser.os.version = '8.1';
break;
case 6.2:
microsoftBrowser.os.version = '8';
break;
case 6.1:
microsoftBrowser.os.version = '7';
break;
case 6:
microsoftBrowser.os.version = 'Vista';
break;
case 5.2:
microsoftBrowser.os.version = '2003';
break;
case 5.1:
microsoftBrowser.os.version = 'XP';
break;
case 5.01:
microsoftBrowser.os.version = '2000 SP1';
break;
case 5:
microsoftBrowser.os.version = '2000';
break;
case 4:
microsoftBrowser.os.version = 'NT';
break;
default:
microsoftBrowser.os.version = DEFAULT_VERSION;
}
} else if (looksLike(/Windows.9(\d)/i, ua)) {
microsoftBrowser.os.version = '9x'; // 95, 98, or Me, which all must have a market share of nothingness by now so we dont care which it is
microsoftBrowser.desktop = true;
} else if (looksLike(/Windows.CE/i, ua)) {
microsoftBrowser.os.version = 'CE'; // only matters that its mobile
microsoftBrowser.mobile = true;
} else {
microsoftBrowser.os.version = DEFAULT_VERSION + '';
microsoftBrowser.desktop = true;
}
}
// special detection for MS Surfaces specifically
if(looksLike(/Touch/i, ua) && !looksLike(/IEMobile/i, ua)) {
microsoftBrowser.os.name = 'Window RT';
}
return microsoftBrowser;
};
/**
* Creates a Browser instance with its attributed Android values.
* @param {Window} win
* @param {String} ua
* @static
* @returns {Browser} returns the Browser instance
*/
var getAndroidOS = function (win, ua) {
var androidBrowser = new Browser();
androidBrowser.ua.version = parseFloatIfMatch(ua, /Android\s(\d+\.\d+)/i);
androidBrowser.os.name = 'Android';
androidBrowser.mobile = true;
if (looksLike(/Chrome/i, ua)) {
// modern Android browsers use the chrome engine
androidBrowser.engine.name = 'chrome';
androidBrowser.engine.version = parseIntIfMatch(ua, /Chrome\/(\d+)/i, 10);
} else if (looksLike(/AppleWebKit/i, ua)) {
// old Android browsers uses webkit
androidBrowser.engine.name = 'webkit';
androidBrowser.engine.version = parseIntIfMatch(ua, /AppleWebKit\/(\d+)/i, 10);
} else {
androidBrowser.engine.name = 'unknown';
}
// for now, we use the same logic for Android's browser and os detection
androidBrowser.os.version = getAndroidVersion(win, androidBrowser.ua.version) + '';
return androidBrowser;
};
/**
* Returns the Kindle's OS.
* @param {Window} win
* @param {String} ua
* @static
* @returns {Browser} returns the Browser instance
*/
var getKindleOS = function (win, ua) {
var kindleBrowser = new Browser();
if (looksLike(/Silk/i, ua)) {
kindleBrowser.engine.name = 'silk';
kindleBrowser.engine.version = parseIntIfMatch(ua, /Silk\/(\d+)/i, 10);
// set the detected version equal to silk by default
kindleBrowser.ua.verison = kindleBrowser.engine.version;
} else if (looksLike(/AppleWebKit/i, ua)) {
kindleBrowser.engine.name = 'webkit';
kindleBrowser.engine.version = parseIntIfMatch(ua, /AppleWebKit\/(\d+)/i, 10);
// if the kindle doesn't have silk in the userAgent, something is wonky
kindleBrowser.ua.version = 1;
}
if (looksLike(/Version/i, ua)) {
// some kindles have a Version property in their userAgent
kindleBrowser.ua.version = parseFloatIfMatch(ua, /Version\/(\d+\.\d+)/i);
}
kindleBrowser.version = getKindleVersion(win, kindleBrowser.ua.version);
kindleBrowser.tablet = true;
return kindleBrowser;
};
/**
* Reads the user agent string to determine OS.
* @param {Window} win
* @param {String} ua
* @static
* @returns {Browser} returns the Browser instance
*/
var getOsFromUa = function (win, ua) {
var unknownOS = new Browser();
if (looksLike(/Win|IEMobile/i, ua)) {
unknownOS = getMicrosoftOS(win, ua);
} else if (looksLike(/Mac|iPhone|iPad|iPod/i, ua)) {
unknownOS = getAppleOS(win, ua);
} else if (looksLike(/Android/i, ua)) {
unknownOS = getAndroidOS(win, ua);
} else if (looksLike(kindleBrowser, ua)) {
unknownOS = getKindleOS(win, ua);
} else {
unknownOS = getOtherOS(win, ua);
}
return unknownOS;
};
/**
* Returns an object containing browser details (e.g. name, os, version, etc.).
* @param {Window=} win
* @param {String=} userAgent
* @static
* @returns {Browser} returns the Browser instance
* @example
* ```js
* var os = browser.detect().os.name;
*
* console.log(os); // outputs OS name (e.g. Windows, Mac, Android, etc.)
* ```
*/
var detect = function (win, userAgent) {
var detectedBrowser = new Browser(),
browserType = '',
w = win || window,
d = w.document,
ua = userAgent || w.navigator.userAgent,
nav = w.navigator,
style = w.document.documentElement.style;
// reset the results array
results = [];
// see if this is a mobile browser
detectedBrowser.mobile = isMobile(w);
// run thru mobile detection first if applicable
if (detectedBrowser.mobile) {
// MS Surfaces pass the mobile test, so we account for them here
// IE Mobile sometimes contain touch in the UA
if (looksLike(/Win/i, ua) && looksLike(/Touch/i, ua) && !looksLike(/IEMobile/i, ua)) {
browserType = TYPE.MICROSOFT;
// Kindle feature support varies greatly, and they retain low market-share, so we trust the user agent for now
} else if (looksLike(kindleBrowser, ua)) {
browserType = TYPE.KINDLE;
} else if (can(nav, 'permissions')) {
// Chrome is the only mobile platform with permissions
browserType = TYPE.CHROME_MOBILE;
} else if (has('ondevicelight', w)) {
// Only FF Mobile has the ambient light API
browserType = TYPE.FIREFOX_MOBILE;
} else if (has('setImmediate', w)) {
// IE is the only mobile with setImmediate
browserType = TYPE.MICROSOFT_MOBILE;
} else if (!has('matchMedia', w)) {
// only Opera Mini lacks matchMedia, thanks for making this easy Opera!
browserType = TYPE.OPERA_MINI;
} else if (has('speechSynthesis', w)) {
// iOS began supporting internationalization api in iOS 10
// iOS Safari has speech synth support that goes way back to older versions too
browserType = TYPE.SAFARI_MOBILE;
} else if (has('isFinite', w) || can(has('connection', nav), 'type')) {
// Android is the only remaining one with isFinite
// Very old Android supports nav.connection.type
if (mathMLSupport(d)) {
// Detect mathML to find webviews reporting themselves as Android
browserType = TYPE.WEBVIEW;
} else {
// Android has never supported mathML as of 4.4.4
browserType = TYPE.ANDROID;
}
} else if (!has('Intl', w)) {
// blackBerry is the only one left without Internationalization
browserType = TYPE.BLACKBERRY;
} else if (has('webkitRequestFileSystem', w)) {
// Opera is the only one remaining with a File System API
browserType = TYPE.OPERA_ANDROID;
} else {
browserType = TYPE.UNKNOWN_MOBILE;
}
} else if (!has('EventSource', w) && can(nav, 'onLine')) {
browserType = TYPE.MICROSOFT;
} else if (has('InstallTrigger', w)) {
browserType = TYPE.FIREFOX;
} else if (has('chrome', w) && !has('opera', w) && !looksLike(/\sOPR\/\d+/i, ua)) {
browserType = TYPE.CHROME;
} else if (has('opera', w) || looksLike(/\sOPR\/\d+/i, ua)) {
browserType = TYPE.OPERA;
} else if (!has('webkitRequestFileSystem', w)) {
browserType = TYPE.SAFARI;
} else {
browserType = TYPE.UNKNOWN;
}
// now that we know the environment, we run feature detection specific to it
if (browserType === TYPE.MICROSOFT) {
detectedBrowser = getMicrosoftOS(w, ua);
detectedBrowser.engine.name = 'trident';
detectedBrowser.ua.version = parseIntIfMatch(ua, /Edge\/(\d+)/i, 10);
if (detectedBrowser.ua.version === DEFAULT_VERSION) {
detectedBrowser.ua.version = parseIntIfMatch(ua, /MSIE\/(\d+)/i, 10);
}
detectedBrowser.version = getTridentVersion(w, detectedBrowser.ua.version);
detectedBrowser.engine.version = getTridentEngineVersion(detectedBrowser.version);
if (detectedBrowser.version >= 12) {
detectedBrowser.name = 'Edge';
} else {
detectedBrowser.name = 'Internet Explorer';
}
if (detectedBrowser.name === 'Edge' && !looksLike(/Edge/i, ua)) {
detectedBrowser.trustworthy = false;
} else if (detectedBrowser.name === 'Internet Explorer' && (!looksLike(/MSIE/i, ua) && !looksLike(/Trident/i, ua))) {
detectedBrowser.trustworthy = false;
}
} else if (browserType === TYPE.FIREFOX) {
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.name = 'Firefox';
detectedBrowser.engine.name = 'gecko';
detectedBrowser.engine.version = parseIntIfMatch(ua, /rv:(\d+)/i, 10);
detectedBrowser.ua.version = parseIntIfMatch(ua, /Firefox\/(\d+)/i, 10);
detectedBrowser.version = getGeckoVersion(w, detectedBrowser.ua.version);
if (!looksLike(/Firefox/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.CHROME) {
// Detect all chrome versions possible by feature detection
// all versions: 1-38
// optimal version coverage: 21, 35, 36, 37, 38
// versions accounted for: 4-38
// versions covered by feature testing: 4-13, 15, 20-27, 30, 32-34
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.engine.name = has('CSS', w) ? 'blink' : 'webkit'; // should be all of version 27+ on blink
// chrome uses a standard ua format and can always supported indexOf
detectedBrowser.engine.version = parseIntIfMatch(ua, /Chrome\/(\d+)/i, 10);
detectedBrowser.ua.version = detectedBrowser.engine.version;
detectedBrowser.version = getChromiumVersion(w, detectedBrowser.ua.version);
detectedBrowser.name = 'Chrome';
if (!looksLike(/Chrome/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.OPERA) {
detectedBrowser = getOsFromUa(w, ua);
if (looksLike(/Presto\/(\d+\.\d+)/i, ua)) {
detectedBrowser.engine.version = parseFloatIfMatch(ua, /Presto\/(\d+\.\d+)/i);
} else if (looksLike(/AppleWebKit\/(\d+)/i, ua)) {
detectedBrowser.engine.version = parseIntIfMatch(ua, /AppleWebKit\/(\d+)/i, 10);
}
if (looksLike(/Nintendo/i, ua)) {
detectedBrowser.engine.name = 'presto';
detectedBrowser.version = '9.0';
detectedBrowser.console = true;
} else {
if (can(has('opera', w), 'version')) {
detectedBrowser.feature.version = parseFloat(w.opera.version()); // presto reveals its version via api
detectedBrowser.version = detectedBrowser.feature.version;
detectedBrowser.engine.name = 'presto'; // should be all of version 27+ on blink
} else {
detectedBrowser.version = getChromiumVersion(w, detectedBrowser.ua.version);
detectedBrowser.engine.name = 'blink'; // chrome's blink engine would be the version of the engine here
detectedBrowser.engine.version = detectedBrowser.version; // chrome's blink engine would be the version of the engine here
if (looksLike(/OPR\/\d+.\d+/i, ua)) {
detectedBrowser.ua.version = parseFloatIfMatch(ua, /OPR\/\d+.\d+/i);
}
if (detectedBrowser.version >= 28) { // chrome version is 28+ then it's on chrome's release cycle.
detectedBrowser.version = getVersion(detectedBrowser.ua.version, detectedBrowser.version - 13, MAX_BROWSER_VER); // Guess the version based on chrome's version.
}
}
}
detectedBrowser.name = 'Opera';
if (!looksLike(/Opera/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.SAFARI) {
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.engine.version = parseIntIfMatch(ua, /AppleWebKit\/(\d+)/, 10);
// all versions: 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 2.0, 3.0, 3.1, 3.2, 4.0, 4.1, 5.0, 5.1, 6.0, 6.1, 7.0, 8.0
// optimal version coverage: 6.1, 7.0, 8.0
// versions accounted for: *
// versions covered by feature testing: 3.2, 4.0, 4.2, 5.0, 6.0, 7.0
detectedBrowser.desktop = true;
if (looksLike(/Version\/(\d\.\d)/i, ua)) {
detectedBrowser.ua.version = parseFloatIfMatch(ua, /Version\/(\d\.\d)/i); // this format was introduced at 3.0+browser.ua.version = parseFloat(ua.match()[1]);
}
if (looksLike(/Mac.OS.X.10.(\d+)/i, ua)) {
var macVersion = parseIntIfMatch(ua, /Mac.OS.X.10.(\d+)/i, 10); // this format was introduced at 3.0+
if (macVersion > 0) {
detectedBrowser.os.name = 'Mac';
detectedBrowser.os.version = '10.' + macVersion;
} else {
detectedBrowser = getOsFromUa(w, ua);
}
} else {
detectedBrowser = getOsFromUa(w, ua);
}
detectedBrowser.name = 'Safari';
detectedBrowser.engine.name = 'webkit';
detectedBrowser.version = getSafariVersion(w, detectedBrowser.ua.version);
if (!looksLike(/Safari/i, ua)) {
detectedBrowser.trustworthy = false;
}
} else if (browserType === TYPE.ANDROID) {
detectedBrowser = getAndroidOS(w, ua);
detectedBrowser.name = 'Android';
detectedBrowser.version = getAndroidVersion(w, detectedBrowser.ua.version);
if (!looksLike(/Android/i, ua) || !looksLike(/Mobile/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.SAFARI_MOBILE) {
detectedBrowser = getAppleOS(w, ua);
// Feature detect Mobile Safari so we can tell if it's the real deal or an imposter
detectedBrowser.name = 'Mobile Safari';
detectedBrowser.engine.name = 'webkit';
detectedBrowser.engine.version = parseIntIfMatch(ua, /AppleWebKit\/(\d+)/, 10);
if (looksLike(/Version\/(\d\.\d)/i, ua)) {
detectedBrowser.ua.version = parseFloatIfMatch(ua, /Version\/(\d\.\d)/i); // this format was introduced at 3.0+browser.ua.version = parseFloat(ua.match()[1]);
}
// all versions: 1.0, 1.1, 2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 4.3, 5.0, 5.1, 6.0, 6.1, 7.0, 7.1, 8.0
// optimal version coverage: 6.x, 7.x, 8.x
// versions accounted for: *
// versions covered by feature testing: 3.2, 4.0, 4.2, 5.0, 6.0, 7.0, 8.0
detectedBrowser.version = getSafariVersion(w, detectedBrowser.ua.version);
detectedBrowser.os.version = detectedBrowser.version;
if (!looksLike(/Safari/i, ua) || !looksLike(/iPhone|iPad/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.CHROME_MOBILE) {
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.name = 'Chrome Android';
detectedBrowser.ua.version = parseIntIfMatch(ua, /Chrome\/(\d+)/i, 10); // we trust the UA for now
detectedBrowser.version = detectedBrowser.ua.version;
if (!looksLike(/Chrome/i, ua) || !looksLike(/Mobile/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.FIREFOX_MOBILE) {
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.name = 'Firefox Android';
detectedBrowser.ua.version = parseIntIfMatch(ua, /Firefox\/(\d+)/i, 10); // we trust the UA for now
detectedBrowser.version = detectedBrowser.ua.version;
if (!looksLike(/Firefox/i, ua) || !looksLike(/Mobile/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.MICROSOFT_MOBILE) {
detectedBrowser = getMicrosoftOS(w, ua);
detectedBrowser.name = 'Mobile IE';
detectedBrowser.ua.version = parseIntIfMatch(ua, /IEMobile\/(\d+)/i, 10); // we trust the UA for now
detectedBrowser.version = detectedBrowser.ua.version;
if (!looksLike(/MSIE/i, ua) || !looksLike(/IEMobile/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.WEBVIEW) {
detectedBrowser = getAppleOS(w, ua);
detectedBrowser.name = 'iOS Webview';
if (!looksLike(/iPhone|iPad|iPod/i, ua) || !looksLike(/Mobile/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.OPERA_MINI) {
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.name = 'Opera Mini';
detectedBrowser.ua.version = parseIntIfMatch(ua, /Opera Mini\/(\d+)/i, 10); // we trust the UA for now
detectedBrowser.version = detectedBrowser.ua.version;
if (!looksLike(/Opera/i, ua) || !looksLike(/Mini/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.OPERA_ANDROID) {
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.name = 'Opera Android';
detectedBrowser.ua.version = parseIntIfMatch(ua, /Opera\/(\d+)/i, 10); // we trust the UA for now
detectedBrowser.version = detectedBrowser.ua.version;
if (!looksLike(/Opera/i, ua) || !looksLike(/Android/i, ua)) { detectedBrowser.trustworthy = false; }
} else if (browserType === TYPE.KINDLE) {
detectedBrowser = getKindleOS(w, ua);
detectedBrowser.name = 'Kindle';
// we rely on the user agent for all kindle detection, so we can't detect untrustworthiness
} else if (browserType === TYPE.UNKNOWN || browserType === TYPE.UNKNOWN_MOBILE) {
if (can(style, 'KhtmlUserInput')) {
detectedBrowser.name = 'Linux Browser';
detectedBrowser.engine.name = 'khtml';
detectedBrowser.os.name = 'Linux';
detectedBrowser.desktop = true;
} else {
detectedBrowser = getOsFromUa(w, ua);
detectedBrowser.name = 'Unknown';
detectedBrowser.engine.name = 'Unknown';
detectedBrowser.os.name = 'Unknown';
}
}
// if the user agent version is beyond
if (detectedBrowser.ua.version > detectedBrowser.version) {
detectedBrowser.max = MAX.EXCEED;
}
map.TRUSTWORTHY = save(detectedBrowser.trustworthy);
map.BROWSER_NAME = save(detectedBrowser.name);
map.BROWSER_VERSION = save(detectedBrowser.version);
map.ENGINE_NAME = save(detectedBrowser.engine.name);
map.ENGINE_VERSION = save(detectedBrowser.engine.version);
map.OS_NAME = save(detectedBrowser.os.name);
map.OS_VERSION = save(detectedBrowser.os.version);
map.DESKTOP = save(detectedBrowser.desktop);
map.MOBILE = save(detectedBrowser.mobile);
map.TABLET = save(detectedBrowser.tablet);
map.CONSOLE = save(detectedBrowser.console);
map.MAX = save(detectedBrowser.max);
detectedBrowser.isIE = (detectedBrowser.name === 'Internet Explorer');
detectedBrowser.isFF = (detectedBrowser.name === 'Firefox');
detectedBrowser.isOpera = (detectedBrowser.name === 'Opera');
detectedBrowser.isChrome = (detectedBrowser.name === 'Chrome');
detectedBrowser.isSafari = (detectedBrowser.name === 'Safari');
/**
* Retrieve any results in the map by name because they're returned in an array without names.
* @param {String} key
* @returns {*}
*/
module.exports.read = function (key) {
return results[key];
};
module.exports.map = map;
module.exports.results = results;
module.exports.details = detectedBrowser;
return detectedBrowser;
};
exports.detect = detect;
exports.isMobile = isMobile;
exports.mathMLSupports = mathMLSupport;
exports.getVersion = getVersion;
exports.looksLike = looksLike;
exports.parseIntIfMatch = parseIntIfMatch;
exports.parseFloatIfMatch = parseFloatIfMatch;
exports.getAndroidVersion = getAndroidVersion;
exports.getChromiumVersion = getChromiumVersion;
exports.getSafariVersion = getSafariVersion;
exports.getKindleVersion = getKindleVersion;
exports.getOtherOS = getOtherOS;
exports.getAppleOS = getAppleOS;
exports.getMicrosoftOS = getMicrosoftOS;
exports.getAndroidOS = getAndroidOS;
exports.getKindleOS = getKindleOS;
exports.getOsFromUa = getOsFromUa;