index.js 15 KB

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