blob: 4eeebfefbd95c1aede7f7253f69da554c1a13cf2 [file] [log] [blame]
'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;