UNPKG

14.6 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * This is where all the magic comes from, specially crafted for `useragent`.
5 */
6var regexps = require('./lib/regexps');
7
8/**
9 * Reduce references by storing the lookups.
10 */
11// OperatingSystem parsers:
12var osparsers = regexps.os
13 , osparserslength = osparsers.length;
14
15// UserAgent parsers:
16var agentparsers = regexps.browser
17 , agentparserslength = agentparsers.length;
18
19// Device parsers:
20var deviceparsers = regexps.device
21 , deviceparserslength = deviceparsers.length;
22
23/**
24 * The representation of a parsed user agent.
25 *
26 * @constructor
27 * @param {String} family The name of the browser
28 * @param {String} major Major version of the browser
29 * @param {String} minor Minor version of the browser
30 * @param {String} patch Patch version of the browser
31 * @param {String} source The actual user agent string
32 * @api public
33 */
34function Agent(family, major, minor, patch, source) {
35 this.family = family || 'Other';
36 this.major = major || '0';
37 this.minor = minor || '0';
38 this.patch = patch || '0';
39 this.source = source || '';
40}
41
42/**
43 * OnDemand parsing of the Operating System.
44 *
45 * @type {OperatingSystem}
46 * @api public
47 */
48Object.defineProperty(Agent.prototype, 'os', {
49 get: function lazyparse() {
50 var userAgent = this.source
51 , length = osparserslength
52 , parsers = osparsers
53 , i = 0
54 , parser
55 , res;
56
57 for (; i < length; i++) {
58 if (res = parsers[i][0].exec(userAgent)) {
59 parser = parsers[i];
60
61 if (parser[1]) res[1] = parser[1].replace('$1', res[1]);
62 break;
63 }
64 }
65
66 return Object.defineProperty(this, 'os', {
67 value: !parser || !res
68 ? new OperatingSystem()
69 : new OperatingSystem(
70 res[1]
71 , parser[2] || res[2]
72 , parser[3] || res[3]
73 , parser[4] || res[4]
74 )
75 }).os;
76 },
77
78 /**
79 * Bypass the OnDemand parsing and set an OperatingSystem instance.
80 *
81 * @param {OperatingSystem} os
82 * @api public
83 */
84 set: function set(os) {
85 if (!(os instanceof OperatingSystem)) return false;
86
87 return Object.defineProperty(this, 'os', {
88 value: os
89 }).os;
90 }
91});
92
93/**
94 * OnDemand parsing of the Device type.
95 *
96 * @type {Device}
97 * @api public
98 */
99Object.defineProperty(Agent.prototype, 'device', {
100 get: function lazyparse() {
101 var userAgent = this.source
102 , length = deviceparserslength
103 , parsers = deviceparsers
104 , i = 0
105 , parser
106 , res;
107
108 for (; i < length; i++) {
109 if (res = parsers[i][0].exec(userAgent)) {
110 parser = parsers[i];
111
112 if (parser[1]) res[1] = parser[1].replace('$1', res[1]);
113 break;
114 }
115 }
116
117 return Object.defineProperty(this, 'device', {
118 value: !parser || !res
119 ? new Device()
120 : new Device(
121 res[1]
122 , parser[2] || res[2]
123 , parser[3] || res[3]
124 , parser[4] || res[4]
125 )
126 }).device;
127 },
128
129 /**
130 * Bypass the OnDemand parsing and set an Device instance.
131 *
132 * @param {Device} device
133 * @api public
134 */
135 set: function set(device) {
136 if (!(device instanceof Device)) return false;
137
138 return Object.defineProperty(this, 'device', {
139 value: device
140 }).device;
141 }
142});
143/*** Generates a string output of the parsed user agent.
144 *
145 * @returns {String}
146 * @api public
147 */
148Agent.prototype.toAgent = function toAgent() {
149 var output = this.family
150 , version = this.toVersion();
151
152 if (version) output += ' '+ version;
153 return output;
154};
155
156/**
157 * Generates a string output of the parser user agent and operating system.
158 *
159 * @returns {String} "UserAgent 0.0.0 / OS"
160 * @api public
161 */
162Agent.prototype.toString = function toString() {
163 var agent = this.toAgent()
164 , os = this.os !== 'Other' ? this.os : false;
165
166 return agent + (os ? ' / ' + os : '');
167};
168
169/**
170 * Outputs a compiled veersion number of the user agent.
171 *
172 * @returns {String}
173 * @api public
174 */
175Agent.prototype.toVersion = function toVersion() {
176 var version = '';
177
178 if (this.major) {
179 version += this.major;
180
181 if (this.minor) {
182 version += '.' + this.minor;
183
184 // Special case here, the patch can also be Alpha, Beta etc so we need
185 // to check if it's a string or not.
186 if (this.patch) {
187 version += (isNaN(+this.patch) ? ' ' : '.') + this.patch;
188 }
189 }
190 }
191
192 return version;
193};
194
195/**
196 * Outputs a JSON string of the Agent.
197 *
198 * @returns {String}
199 * @api public
200 */
201Agent.prototype.toJSON = function toJSON() {
202 return {
203 family: this.family
204 , major: this.major
205 , minor: this.minor
206 , patch: this.patch
207 , device: this.device
208 , os: this.os
209 };
210};
211
212/**
213 * The representation of a parsed Operating System.
214 *
215 * @constructor
216 * @param {String} family The name of the os
217 * @param {String} major Major version of the os
218 * @param {String} minor Minor version of the os
219 * @param {String} patch Patch version of the os
220 * @api public
221 */
222function OperatingSystem(family, major, minor, patch) {
223 this.family = family || 'Other';
224 this.major = major || '0';
225 this.minor = minor || '0';
226 this.patch = patch || '0';
227}
228
229/**
230 * Generates a stringified version of the Operating System.
231 *
232 * @returns {String} "Operating System 0.0.0"
233 * @api public
234 */
235OperatingSystem.prototype.toString = function toString() {
236 var output = this.family
237 , version = this.toVersion();
238
239 if (version) output += ' '+ version;
240 return output;
241};
242
243/**
244 * Generates the version of the Operating System.
245 *
246 * @returns {String}
247 * @api public
248 */
249OperatingSystem.prototype.toVersion = function toVersion() {
250 var version = '';
251
252 if (this.major) {
253 version += this.major;
254
255 if (this.minor) {
256 version += '.' + this.minor;
257
258 // Special case here, the patch can also be Alpha, Beta etc so we need
259 // to check if it's a string or not.
260 if (this.patch) {
261 version += (isNaN(+this.patch) ? ' ' : '.') + this.patch;
262 }
263 }
264 }
265
266 return version;
267};
268
269/**
270 * Outputs a JSON string of the OS, values are defaulted to undefined so they
271 * are not outputed in the stringify.
272 *
273 * @returns {String}
274 * @api public
275 */
276OperatingSystem.prototype.toJSON = function toJSON(){
277 return {
278 family: this.family
279 , major: this.major || undefined
280 , minor: this.minor || undefined
281 , patch: this.patch || undefined
282 };
283};
284
285/**
286 * The representation of a parsed Device.
287 *
288 * @constructor
289 * @param {String} family The name of the device
290 * @param {String} major Major version of the device
291 * @param {String} minor Minor version of the device
292 * @param {String} patch Patch version of the device
293 * @api public
294 */
295function Device(family, major, minor, patch) {
296 this.family = family || 'Other';
297 this.major = major || '0';
298 this.minor = minor || '0';
299 this.patch = patch || '0';
300}
301
302/**
303 * Generates a stringified version of the Device.
304 *
305 * @returns {String} "Device 0.0.0"
306 * @api public
307 */
308Device.prototype.toString = function toString() {
309 var output = this.family
310 , version = this.toVersion();
311
312 if (version) output += ' '+ version;
313 return output;
314};
315
316/**
317 * Generates the version of the Device.
318 *
319 * @returns {String}
320 * @api public
321 */
322Device.prototype.toVersion = function toVersion() {
323 var version = '';
324
325 if (this.major) {
326 version += this.major;
327
328 if (this.minor) {
329 version += '.' + this.minor;
330
331 // Special case here, the patch can also be Alpha, Beta etc so we need
332 // to check if it's a string or not.
333 if (this.patch) {
334 version += (isNaN(+this.patch) ? ' ' : '.') + this.patch;
335 }
336 }
337 }
338
339 return version;
340};
341
342/**
343 * Outputs a JSON string of the Device, values are defaulted to undefined so they
344 * are not outputed in the stringify.
345 *
346 * @returns {String}
347 * @api public
348 */
349Device.prototype.toJSON = function toJSON() {
350 return {
351 family: this.family
352 , major: this.major || undefined
353 , minor: this.minor || undefined
354 , patch: this.patch || undefined
355 };
356};
357
358/**
359 * Small nifty thick that allows us to download a fresh set regexs from t3h
360 * Int3rNetz when we want to. We will be using the compiled version by default
361 * but users can opt-in for updates.
362 *
363 * @param {Boolean} refresh Refresh the dataset from the remote
364 * @api public
365 */
366module.exports = function updater() {
367 try {
368 require('./lib/update').update(function updating(err, results) {
369 if (err) {
370 console.log('[useragent] Failed to update the parsed due to an error:');
371 console.log('[useragent] '+ (err.message ? err.message : err));
372 return;
373 }
374
375 regexps = results;
376
377 // OperatingSystem parsers:
378 osparsers = regexps.os;
379 osparserslength = osparsers.length;
380
381 // UserAgent parsers:
382 agentparsers = regexps.browser;
383 agentparserslength = agentparsers.length;
384
385 // Device parsers:
386 deviceparsers = regexps.device;
387 deviceparserslength = deviceparsers.length;
388 });
389 } catch (e) {
390 console.error('[useragent] If you want to use automatic updating, please add:');
391 console.error('[useragent] - request (npm install request --save)');
392 console.error('[useragent] - yamlparser (npm install yamlparser --save)');
393 console.error('[useragent] To your own package.json');
394 }
395};
396
397// Override the exports with our newly set module.exports
398exports = module.exports;
399
400/**
401 * Nao that we have setup all the different classes and configured it we can
402 * actually start assembling and exposing everything.
403 */
404exports.Device = Device;
405exports.OperatingSystem = OperatingSystem;
406exports.Agent = Agent;
407
408/**
409 * Parses the user agent string with the generated parsers from the
410 * ua-parser project on google code.
411 *
412 * @param {String} userAgent The user agent string
413 * @param {String} jsAgent Optional UA from js to detect chrome frame
414 * @returns {Agent}
415 * @api public
416 */
417exports.parse = function parse(userAgent, jsAgent) {
418 if (!userAgent) return new Agent();
419
420 var length = agentparserslength
421 , parsers = agentparsers
422 , i = 0
423 , parser
424 , res;
425
426 for (; i < length; i++) {
427 if (res = parsers[i][0].exec(userAgent)) {
428 parser = parsers[i];
429
430 if (parser[1]) res[1] = parser[1].replace('$1', res[1]);
431 if (!jsAgent) return new Agent(
432 res[1]
433 , parser[2] || res[2]
434 , parser[3] || res[3]
435 , parser[4] || res[4]
436 , userAgent
437 );
438
439 break;
440 }
441 }
442
443 // Return early if we didn't find an match, but might still be able to parse
444 // the os and device, so make sure we supply it with the source
445 if (!parser || !res) return new Agent('', '', '', '', userAgent);
446
447 // Detect Chrome Frame, but make sure it's enabled! So we need to check for
448 // the Chrome/ so we know that it's actually using Chrome under the hood.
449 if (jsAgent && ~jsAgent.indexOf('Chrome/') && ~userAgent.indexOf('chromeframe')) {
450 res[1] = 'Chrome Frame (IE '+ res[1] +'.'+ res[2] +')';
451
452 // Run the JavaScripted userAgent string through the parser again so we can
453 // update the version numbers;
454 parser = parse(jsAgent);
455 parser[2] = parser.major;
456 parser[3] = parser.minor;
457 parser[4] = parser.patch;
458 }
459
460 return new Agent(
461 res[1]
462 , parser[2] || res[2]
463 , parser[3] || res[3]
464 , parser[4] || res[4]
465 , userAgent
466 );
467};
468
469/**
470 * If you are doing a lot of lookups you might want to cache the results of the
471 * parsed user agent string instead, in memory.
472 *
473 * @TODO We probably want to create 2 dictionary's here 1 for the Agent
474 * instances and one for the userAgent instance mapping so we can re-use simular
475 * Agent instance and lower our memory consumption.
476 *
477 * @param {String} userAgent The user agent string
478 * @param {String} jsAgent Optional UA from js to detect chrome frame
479 * @api public
480 */
481var LRU = require('lru-cache')(5000);
482exports.lookup = function lookup(userAgent, jsAgent) {
483 var key = (userAgent || '')+(jsAgent || '')
484 , cached = LRU.get(key);
485
486 if (cached) return cached;
487 LRU.set(key, (cached = exports.parse(userAgent, jsAgent)));
488
489 return cached;
490};
491
492/**
493 * Does a more inaccurate but more common check for useragents identification.
494 * The version detection is from the jQuery.com library and is licensed under
495 * MIT.
496 *
497 * @param {String} useragent The user agent
498 * @returns {Object} matches
499 * @api public
500 */
501exports.is = function is(useragent) {
502 var ua = (useragent || '').toLowerCase()
503 , details = {
504 chrome: false
505 , firefox: false
506 , ie: false
507 , mobile_safari: false
508 , mozilla: false
509 , opera: false
510 , safari: false
511 , webkit: false
512 , android: false
513 , version: (ua.match(exports.is.versionRE) || [0, "0"])[1]
514 };
515
516 if (~ua.indexOf('webkit')) {
517 details.webkit = true;
518
519 if (~ua.indexOf('android')){
520 details.android = true;
521 }
522
523 if (~ua.indexOf('chrome')) {
524 details.chrome = true;
525 } else if (~ua.indexOf('safari')) {
526 details.safari = true;
527
528 if (~ua.indexOf('mobile') && ~ua.indexOf('apple')) {
529 details.mobile_safari = true;
530 }
531 }
532 } else if (~ua.indexOf('opera')) {
533 details.opera = true;
534 } else if (~ua.indexOf('trident') || ~ua.indexOf('msie')) {
535 details.ie = true;
536 } else if (~ua.indexOf('mozilla') && !~ua.indexOf('compatible')) {
537 details.mozilla = true;
538
539 if (~ua.indexOf('firefox')) details.firefox = true;
540 }
541
542
543 return details;
544};
545
546/**
547 * Parses out the version numbers.
548 *
549 * @type {RegExp}
550 * @api private
551 */
552exports.is.versionRE = /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/;
553
554/**
555 * Transform a JSON object back to a valid userAgent string
556 *
557 * @param {Object} details
558 * @returns {Agent}
559 */
560exports.fromJSON = function fromJSON(details) {
561 if (typeof details === 'string') details = JSON.parse(details);
562
563 var agent = new Agent(details.family, details.major, details.minor, details.patch)
564 , os = details.os;
565
566 // The device family was added in v2.0
567 if ('device' in details) {
568 agent.device = new Device(details.device.family);
569 } else {
570 agent.device = new Device();
571 }
572
573 if ('os' in details && os) {
574 // In v1.1.0 we only parsed out the Operating System name, not the full
575 // version which we added in v2.0. To provide backwards compatible we should
576 // we should set the details.os as family
577 if (typeof os === 'string') {
578 agent.os = new OperatingSystem(os);
579 } else {
580 agent.os = new OperatingSystem(os.family, os.major, os.minor, os.patch);
581 }
582 }
583
584 return agent;
585};
586
587/**
588 * Library version.
589 *
590 * @type {String}
591 * @api public
592 */
593exports.version = require('./package.json').version;