You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
622 lines
15 KiB
JavaScript
622 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* This is where all the magic comes from, specially crafted for `useragent`.
|
|
*/
|
|
var regexps = require('./lib/regexps');
|
|
|
|
/**
|
|
* Reduce references by storing the lookups.
|
|
*/
|
|
// OperatingSystem parsers:
|
|
var osparsers = regexps.os
|
|
, osparserslength = osparsers.length;
|
|
|
|
// UserAgent parsers:
|
|
var agentparsers = regexps.browser
|
|
, agentparserslength = agentparsers.length;
|
|
|
|
// Device parsers:
|
|
var deviceparsers = regexps.device
|
|
, deviceparserslength = deviceparsers.length;
|
|
|
|
/**
|
|
* The representation of a parsed user agent.
|
|
*
|
|
* @constructor
|
|
* @param {String} family The name of the browser
|
|
* @param {String} major Major version of the browser
|
|
* @param {String} minor Minor version of the browser
|
|
* @param {String} patch Patch version of the browser
|
|
* @param {String} source The actual user agent string
|
|
* @api public
|
|
*/
|
|
function Agent(family, major, minor, patch, source) {
|
|
this.family = family || 'Other';
|
|
this.major = major || '0';
|
|
this.minor = minor || '0';
|
|
this.patch = patch || '0';
|
|
this.source = source || '';
|
|
}
|
|
|
|
/**
|
|
* OnDemand parsing of the Operating System.
|
|
*
|
|
* @type {OperatingSystem}
|
|
* @api public
|
|
*/
|
|
Object.defineProperty(Agent.prototype, 'os', {
|
|
get: function lazyparse() {
|
|
var userAgent = this.source
|
|
, length = osparserslength
|
|
, parsers = osparsers
|
|
, i = 0
|
|
, parser
|
|
, res;
|
|
|
|
for (; i < length; i++) {
|
|
if (res = parsers[i][0].exec(userAgent)) {
|
|
parser = parsers[i];
|
|
|
|
if (parser[1]) res[1] = parser[1].replace('$1', res[1]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return Object.defineProperty(this, 'os', {
|
|
value: !parser || !res
|
|
? new OperatingSystem()
|
|
: new OperatingSystem(
|
|
res[1]
|
|
, parser[2] || res[2]
|
|
, parser[3] || res[3]
|
|
, parser[4] || res[4]
|
|
)
|
|
}).os;
|
|
},
|
|
|
|
/**
|
|
* Bypass the OnDemand parsing and set an OperatingSystem instance.
|
|
*
|
|
* @param {OperatingSystem} os
|
|
* @api public
|
|
*/
|
|
set: function set(os) {
|
|
if (!(os instanceof OperatingSystem)) return false;
|
|
|
|
return Object.defineProperty(this, 'os', {
|
|
value: os
|
|
}).os;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* OnDemand parsing of the Device type.
|
|
*
|
|
* @type {Device}
|
|
* @api public
|
|
*/
|
|
Object.defineProperty(Agent.prototype, 'device', {
|
|
get: function lazyparse() {
|
|
var userAgent = this.source
|
|
, length = deviceparserslength
|
|
, parsers = deviceparsers
|
|
, i = 0
|
|
, parser
|
|
, res;
|
|
|
|
for (; i < length; i++) {
|
|
if (res = parsers[i][0].exec(userAgent)) {
|
|
parser = parsers[i];
|
|
|
|
if (parser[1]) res[1] = parser[1].replace('$1', res[1]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return Object.defineProperty(this, 'device', {
|
|
value: !parser || !res
|
|
? new Device()
|
|
: new Device(
|
|
res[1]
|
|
, parser[2] || res[2]
|
|
, parser[3] || res[3]
|
|
, parser[4] || res[4]
|
|
)
|
|
}).device;
|
|
},
|
|
|
|
/**
|
|
* Bypass the OnDemand parsing and set an Device instance.
|
|
*
|
|
* @param {Device} device
|
|
* @api public
|
|
*/
|
|
set: function set(device) {
|
|
if (!(device instanceof Device)) return false;
|
|
|
|
return Object.defineProperty(this, 'device', {
|
|
value: device
|
|
}).device;
|
|
}
|
|
});
|
|
/*** Generates a string output of the parsed user agent.
|
|
*
|
|
* @returns {String}
|
|
* @api public
|
|
*/
|
|
Agent.prototype.toAgent = function toAgent() {
|
|
var output = this.family
|
|
, version = this.toVersion();
|
|
|
|
if (version) output += ' '+ version;
|
|
return output;
|
|
};
|
|
|
|
/**
|
|
* Generates a string output of the parser user agent and operating system.
|
|
*
|
|
* @returns {String} "UserAgent 0.0.0 / OS"
|
|
* @api public
|
|
*/
|
|
Agent.prototype.toString = function toString() {
|
|
var agent = this.toAgent()
|
|
, os = this.os !== 'Other' ? this.os : false;
|
|
|
|
return agent + (os ? ' / ' + os : '');
|
|
};
|
|
|
|
/**
|
|
* Outputs a compiled veersion number of the user agent.
|
|
*
|
|
* @returns {String}
|
|
* @api public
|
|
*/
|
|
Agent.prototype.toVersion = function toVersion() {
|
|
var version = '';
|
|
|
|
if (this.major) {
|
|
version += this.major;
|
|
|
|
if (this.minor) {
|
|
version += '.' + this.minor;
|
|
|
|
// Special case here, the patch can also be Alpha, Beta etc so we need
|
|
// to check if it's a string or not.
|
|
if (this.patch) {
|
|
version += (isNaN(+this.patch) ? ' ' : '.') + this.patch;
|
|
}
|
|
}
|
|
}
|
|
|
|
return version;
|
|
};
|
|
|
|
/**
|
|
* Outputs a JSON string of the Agent.
|
|
*
|
|
* @returns {String}
|
|
* @api public
|
|
*/
|
|
Agent.prototype.toJSON = function toJSON() {
|
|
return {
|
|
family: this.family
|
|
, major: this.major
|
|
, minor: this.minor
|
|
, patch: this.patch
|
|
, device: this.device
|
|
, os: this.os
|
|
};
|
|
};
|
|
|
|
/**
|
|
* The representation of a parsed Operating System.
|
|
*
|
|
* @constructor
|
|
* @param {String} family The name of the os
|
|
* @param {String} major Major version of the os
|
|
* @param {String} minor Minor version of the os
|
|
* @param {String} patch Patch version of the os
|
|
* @api public
|
|
*/
|
|
function OperatingSystem(family, major, minor, patch) {
|
|
this.family = family || 'Other';
|
|
this.major = major || '0';
|
|
this.minor = minor || '0';
|
|
this.patch = patch || '0';
|
|
}
|
|
|
|
/**
|
|
* Generates a stringified version of the Operating System.
|
|
*
|
|
* @returns {String} "Operating System 0.0.0"
|
|
* @api public
|
|
*/
|
|
OperatingSystem.prototype.toString = function toString() {
|
|
var output = this.family
|
|
, version = this.toVersion();
|
|
|
|
if (version) output += ' '+ version;
|
|
return output;
|
|
};
|
|
|
|
/**
|
|
* Generates the version of the Operating System.
|
|
*
|
|
* @returns {String}
|
|
* @api public
|
|
*/
|
|
OperatingSystem.prototype.toVersion = function toVersion() {
|
|
var version = '';
|
|
|
|
if (this.major) {
|
|
version += this.major;
|
|
|
|
if (this.minor) {
|
|
version += '.' + this.minor;
|
|
|
|
// Special case here, the patch can also be Alpha, Beta etc so we need
|
|
// to check if it's a string or not.
|
|
if (this.patch) {
|
|
version += (isNaN(+this.patch) ? ' ' : '.') + this.patch;
|
|
}
|
|
}
|
|
}
|
|
|
|
return version;
|
|
};
|
|
|
|
/**
|
|
* Outputs a JSON string of the OS, values are defaulted to undefined so they
|
|
* are not outputed in the stringify.
|
|
*
|
|
* @returns {String}
|
|
* @api public
|
|
*/
|
|
OperatingSystem.prototype.toJSON = function toJSON(){
|
|
return {
|
|
family: this.family
|
|
, major: this.major || undefined
|
|
, minor: this.minor || undefined
|
|
, patch: this.patch || undefined
|
|
};
|
|
};
|
|
|
|
/**
|
|
* The representation of a parsed Device.
|
|
*
|
|
* @constructor
|
|
* @param {String} family The name of the device
|
|
* @param {String} major Major version of the device
|
|
* @param {String} minor Minor version of the device
|
|
* @param {String} patch Patch version of the device
|
|
* @api public
|
|
*/
|
|
function Device(family, major, minor, patch) {
|
|
this.family = family || 'Other';
|
|
this.major = major || '0';
|
|
this.minor = minor || '0';
|
|
this.patch = patch || '0';
|
|
}
|
|
|
|
/**
|
|
* Generates a stringified version of the Device.
|
|
*
|
|
* @returns {String} "Device 0.0.0"
|
|
* @api public
|
|
*/
|
|
Device.prototype.toString = function toString() {
|
|
var output = this.family
|
|
, version = this.toVersion();
|
|
|
|
if (version) output += ' '+ version;
|
|
return output;
|
|
};
|
|
|
|
/**
|
|
* Generates the version of the Device.
|
|
*
|
|
* @returns {String}
|
|
* @api public
|
|
*/
|
|
Device.prototype.toVersion = function toVersion() {
|
|
var version = '';
|
|
|
|
if (this.major) {
|
|
version += this.major;
|
|
|
|
if (this.minor) {
|
|
version += '.' + this.minor;
|
|
|
|
// Special case here, the patch can also be Alpha, Beta etc so we need
|
|
// to check if it's a string or not.
|
|
if (this.patch) {
|
|
version += (isNaN(+this.patch) ? ' ' : '.') + this.patch;
|
|
}
|
|
}
|
|
}
|
|
|
|
return version;
|
|
};
|
|
|
|
/**
|
|
* Outputs a JSON string of the Device, values are defaulted to undefined so they
|
|
* are not outputed in the stringify.
|
|
*
|
|
* @returns {String}
|
|
* @api public
|
|
*/
|
|
Device.prototype.toJSON = function toJSON() {
|
|
return {
|
|
family: this.family
|
|
, major: this.major || undefined
|
|
, minor: this.minor || undefined
|
|
, patch: this.patch || undefined
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Small nifty thick that allows us to download a fresh set regexs from t3h
|
|
* Int3rNetz when we want to. We will be using the compiled version by default
|
|
* but users can opt-in for updates.
|
|
*
|
|
* @param {Boolean} refresh Refresh the dataset from the remote
|
|
* @api public
|
|
*/
|
|
module.exports = function updater() {
|
|
try {
|
|
require('./lib/update').update(function updating(err, results) {
|
|
if (err) {
|
|
console.log('[useragent] Failed to update the parsed due to an error:');
|
|
console.log('[useragent] '+ (err.message ? err.message : err));
|
|
return;
|
|
}
|
|
|
|
regexps = results;
|
|
|
|
// OperatingSystem parsers:
|
|
osparsers = regexps.os;
|
|
osparserslength = osparsers.length;
|
|
|
|
// UserAgent parsers:
|
|
agentparsers = regexps.browser;
|
|
agentparserslength = agentparsers.length;
|
|
|
|
// Device parsers:
|
|
deviceparsers = regexps.device;
|
|
deviceparserslength = deviceparsers.length;
|
|
});
|
|
} catch (e) {
|
|
console.error('[useragent] If you want to use automatic updating, please add:');
|
|
console.error('[useragent] - request (npm install request --save)');
|
|
console.error('[useragent] - yamlparser (npm install yamlparser --save)');
|
|
console.error('[useragent] To your own package.json');
|
|
}
|
|
};
|
|
|
|
// Override the exports with our newly set module.exports
|
|
exports = module.exports;
|
|
|
|
/**
|
|
* Nao that we have setup all the different classes and configured it we can
|
|
* actually start assembling and exposing everything.
|
|
*/
|
|
exports.Device = Device;
|
|
exports.OperatingSystem = OperatingSystem;
|
|
exports.Agent = Agent;
|
|
|
|
/**
|
|
* Check if the userAgent is something we want to parse with regexp's.
|
|
*
|
|
* @param {String} userAgent The userAgent.
|
|
* @returns {Boolean}
|
|
*/
|
|
function isSafe(userAgent) {
|
|
var consecutive = 0
|
|
, code = 0;
|
|
|
|
for (var i = 0; i < userAgent.length; i++) {
|
|
code = userAgent.charCodeAt(i);
|
|
// numbers between 0 and 9, letters between a and z
|
|
if ((code >= 48 && code <= 57) || (code >= 97 && code <= 122)) {
|
|
consecutive++;
|
|
} else {
|
|
consecutive = 0;
|
|
}
|
|
|
|
if (consecutive >= 100) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
/**
|
|
* Parses the user agent string with the generated parsers from the
|
|
* ua-parser project on google code.
|
|
*
|
|
* @param {String} userAgent The user agent string
|
|
* @param {String} [jsAgent] Optional UA from js to detect chrome frame
|
|
* @returns {Agent}
|
|
* @api public
|
|
*/
|
|
exports.parse = function parse(userAgent, jsAgent) {
|
|
if (!userAgent || !isSafe(userAgent)) return new Agent();
|
|
|
|
var length = agentparserslength
|
|
, parsers = agentparsers
|
|
, i = 0
|
|
, parser
|
|
, res;
|
|
|
|
for (; i < length; i++) {
|
|
if (res = parsers[i][0].exec(userAgent)) {
|
|
parser = parsers[i];
|
|
|
|
if (parser[1]) res[1] = parser[1].replace('$1', res[1]);
|
|
if (!jsAgent) return new Agent(
|
|
res[1]
|
|
, parser[2] || res[2]
|
|
, parser[3] || res[3]
|
|
, parser[4] || res[4]
|
|
, userAgent
|
|
);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Return early if we didn't find an match, but might still be able to parse
|
|
// the os and device, so make sure we supply it with the source
|
|
if (!parser || !res) return new Agent('', '', '', '', userAgent);
|
|
|
|
// Detect Chrome Frame, but make sure it's enabled! So we need to check for
|
|
// the Chrome/ so we know that it's actually using Chrome under the hood.
|
|
if (jsAgent && ~jsAgent.indexOf('Chrome/') && ~userAgent.indexOf('chromeframe')) {
|
|
res[1] = 'Chrome Frame (IE '+ res[1] +'.'+ res[2] +')';
|
|
|
|
// Run the JavaScripted userAgent string through the parser again so we can
|
|
// update the version numbers;
|
|
parser = parse(jsAgent);
|
|
parser[2] = parser.major;
|
|
parser[3] = parser.minor;
|
|
parser[4] = parser.patch;
|
|
}
|
|
|
|
return new Agent(
|
|
res[1]
|
|
, parser[2] || res[2]
|
|
, parser[3] || res[3]
|
|
, parser[4] || res[4]
|
|
, userAgent
|
|
);
|
|
};
|
|
|
|
/**
|
|
* If you are doing a lot of lookups you might want to cache the results of the
|
|
* parsed user agent string instead, in memory.
|
|
*
|
|
* @TODO We probably want to create 2 dictionary's here 1 for the Agent
|
|
* instances and one for the userAgent instance mapping so we can re-use simular
|
|
* Agent instance and lower our memory consumption.
|
|
*
|
|
* @param {String} userAgent The user agent string
|
|
* @param {String} jsAgent Optional UA from js to detect chrome frame
|
|
* @api public
|
|
*/
|
|
var LRU = require('lru-cache')(5000);
|
|
exports.lookup = function lookup(userAgent, jsAgent) {
|
|
var key = (userAgent || '')+(jsAgent || '')
|
|
, cached = LRU.get(key);
|
|
|
|
if (cached) return cached;
|
|
LRU.set(key, (cached = exports.parse(userAgent, jsAgent)));
|
|
|
|
return cached;
|
|
};
|
|
|
|
/**
|
|
* Does a more inaccurate but more common check for useragents identification.
|
|
* The version detection is from the jQuery.com library and is licensed under
|
|
* MIT.
|
|
*
|
|
* @param {String} useragent The user agent
|
|
* @returns {Object} matches
|
|
* @api public
|
|
*/
|
|
exports.is = function is(useragent) {
|
|
var ua = (useragent || '').toLowerCase()
|
|
, details = {
|
|
chrome: false
|
|
, firefox: false
|
|
, ie: false
|
|
, mobile_safari: false
|
|
, mozilla: false
|
|
, opera: false
|
|
, safari: false
|
|
, webkit: false
|
|
, android: false
|
|
, version: (ua.match(exports.is.versionRE) || [0, "0"])[1]
|
|
};
|
|
|
|
if (~ua.indexOf('webkit')) {
|
|
details.webkit = true;
|
|
|
|
if (~ua.indexOf('android')){
|
|
details.android = true;
|
|
}
|
|
|
|
if (~ua.indexOf('chrome')) {
|
|
details.chrome = true;
|
|
} else if (~ua.indexOf('safari')) {
|
|
details.safari = true;
|
|
|
|
if (~ua.indexOf('mobile') && ~ua.indexOf('apple')) {
|
|
details.mobile_safari = true;
|
|
}
|
|
}
|
|
} else if (~ua.indexOf('opera')) {
|
|
details.opera = true;
|
|
} else if (~ua.indexOf('trident') || ~ua.indexOf('msie')) {
|
|
details.ie = true;
|
|
} else if (~ua.indexOf('mozilla') && !~ua.indexOf('compatible')) {
|
|
details.mozilla = true;
|
|
|
|
if (~ua.indexOf('firefox')) details.firefox = true;
|
|
}
|
|
|
|
|
|
return details;
|
|
};
|
|
|
|
/**
|
|
* Parses out the version numbers.
|
|
*
|
|
* @type {RegExp}
|
|
* @api private
|
|
*/
|
|
exports.is.versionRE = /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/;
|
|
|
|
/**
|
|
* Transform a JSON object back to a valid userAgent string
|
|
*
|
|
* @param {Object} details
|
|
* @returns {Agent}
|
|
*/
|
|
exports.fromJSON = function fromJSON(details) {
|
|
if (typeof details === 'string') details = JSON.parse(details);
|
|
|
|
var agent = new Agent(details.family, details.major, details.minor, details.patch)
|
|
, os = details.os;
|
|
|
|
// The device family was added in v2.0
|
|
if ('device' in details) {
|
|
agent.device = new Device(details.device.family);
|
|
} else {
|
|
agent.device = new Device();
|
|
}
|
|
|
|
if ('os' in details && os) {
|
|
// In v1.1.0 we only parsed out the Operating System name, not the full
|
|
// version which we added in v2.0. To provide backwards compatible we should
|
|
// we should set the details.os as family
|
|
if (typeof os === 'string') {
|
|
agent.os = new OperatingSystem(os);
|
|
} else {
|
|
agent.os = new OperatingSystem(os.family, os.major, os.minor, os.patch);
|
|
}
|
|
}
|
|
|
|
return agent;
|
|
};
|
|
|
|
/**
|
|
* Library version.
|
|
*
|
|
* @type {String}
|
|
* @api public
|
|
*/
|
|
exports.version = require('./package.json').version;
|