utils.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. var shellwords = require('shellwords');
  2. var cp = require('child_process');
  3. var semver = require('semver');
  4. var path = require('path');
  5. var url = require('url');
  6. var os = require('os');
  7. var fs = require('fs');
  8. function clone(obj) {
  9. return JSON.parse(JSON.stringify(obj));
  10. }
  11. module.exports.clone = clone;
  12. var escapeQuotes = function(str) {
  13. if (typeof str === 'string') {
  14. return str.replace(/(["$`\\])/g, '\\$1');
  15. } else {
  16. return str;
  17. }
  18. };
  19. var inArray = function(arr, val) {
  20. return arr.indexOf(val) !== -1;
  21. };
  22. var notifySendFlags = {
  23. u: 'urgency',
  24. urgency: 'urgency',
  25. t: 'expire-time',
  26. time: 'expire-time',
  27. e: 'expire-time',
  28. expire: 'expire-time',
  29. 'expire-time': 'expire-time',
  30. i: 'icon',
  31. icon: 'icon',
  32. c: 'category',
  33. category: 'category',
  34. subtitle: 'category',
  35. h: 'hint',
  36. hint: 'hint'
  37. };
  38. module.exports.command = function(notifier, options, cb) {
  39. notifier = shellwords.escape(notifier);
  40. if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
  41. console.info('node-notifier debug info (command):');
  42. console.info('[notifier path]', notifier);
  43. console.info('[notifier options]', options.join(' '));
  44. }
  45. return cp.exec(notifier + ' ' + options.join(' '), function(
  46. error,
  47. stdout,
  48. stderr
  49. ) {
  50. if (error) return cb(error);
  51. cb(stderr, stdout);
  52. });
  53. };
  54. module.exports.fileCommand = function(notifier, options, cb) {
  55. if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
  56. console.info('node-notifier debug info (fileCommand):');
  57. console.info('[notifier path]', notifier);
  58. console.info('[notifier options]', options.join(' '));
  59. }
  60. return cp.execFile(notifier, options, function(error, stdout, stderr) {
  61. if (error) return cb(error, stdout);
  62. cb(stderr, stdout);
  63. });
  64. };
  65. module.exports.fileCommandJson = function(notifier, options, cb) {
  66. if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
  67. console.info('node-notifier debug info (fileCommandJson):');
  68. console.info('[notifier path]', notifier);
  69. console.info('[notifier options]', options.join(' '));
  70. }
  71. return cp.execFile(notifier, options, function(error, stdout, stderr) {
  72. if (error) return cb(error, stdout);
  73. if (!stdout) return cb(error, {});
  74. try {
  75. var data = JSON.parse(stdout);
  76. cb(stderr, data);
  77. } catch (e) {
  78. cb(e, stdout);
  79. }
  80. });
  81. };
  82. module.exports.immediateFileCommand = function(notifier, options, cb) {
  83. if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
  84. console.info('node-notifier debug info (notifier):');
  85. console.info('[notifier path]', notifier);
  86. }
  87. notifierExists(notifier, function(_, exists) {
  88. if (!exists) {
  89. return cb(new Error('Notifier (' + notifier + ') not found on system.'));
  90. }
  91. cp.execFile(notifier, options);
  92. cb();
  93. });
  94. };
  95. function notifierExists(notifier, cb) {
  96. return fs.stat(notifier, function(err, stat) {
  97. if (!err) return cb(err, stat.isFile());
  98. // Check if Windows alias
  99. if (path.extname(notifier)) {
  100. // Has extentioon, no need to check more
  101. return cb(err, false);
  102. }
  103. // Check if there is an exe file in the directory
  104. return fs.stat(notifier + '.exe', function(err, stat) {
  105. if (err) return cb(err, false);
  106. cb(err, stat.isFile());
  107. });
  108. });
  109. }
  110. var mapAppIcon = function(options) {
  111. if (options.appIcon) {
  112. options.icon = options.appIcon;
  113. delete options.appIcon;
  114. }
  115. return options;
  116. };
  117. var mapText = function(options) {
  118. if (options.text) {
  119. options.message = options.text;
  120. delete options.text;
  121. }
  122. return options;
  123. };
  124. var mapIconShorthand = function(options) {
  125. if (options.i) {
  126. options.icon = options.i;
  127. delete options.i;
  128. }
  129. return options;
  130. };
  131. module.exports.mapToNotifySend = function(options) {
  132. options = mapAppIcon(options);
  133. options = mapText(options);
  134. for (var key in options) {
  135. if (key === 'message' || key === 'title') continue;
  136. if (options.hasOwnProperty(key) && notifySendFlags[key] !== key) {
  137. options[notifySendFlags[key]] = options[key];
  138. delete options[key];
  139. }
  140. }
  141. return options;
  142. };
  143. module.exports.mapToGrowl = function(options) {
  144. options = mapAppIcon(options);
  145. options = mapIconShorthand(options);
  146. options = mapText(options);
  147. if (options.icon && !Buffer.isBuffer(options.icon)) {
  148. try {
  149. options.icon = fs.readFileSync(options.icon);
  150. } catch (ex) {}
  151. }
  152. return options;
  153. };
  154. module.exports.mapToMac = function(options) {
  155. options = mapIconShorthand(options);
  156. options = mapText(options);
  157. if (options.icon) {
  158. options.appIcon = options.icon;
  159. delete options.icon;
  160. }
  161. if (options.sound === true) {
  162. options.sound = 'Bottle';
  163. }
  164. if (options.sound === false) {
  165. delete options.sound;
  166. }
  167. if (options.sound && options.sound.indexOf('Notification.') === 0) {
  168. options.sound = 'Bottle';
  169. }
  170. if (options.wait === true) {
  171. if (!options.timeout) {
  172. options.timeout = 5;
  173. }
  174. delete options.wait;
  175. }
  176. options.json = true;
  177. return options;
  178. };
  179. function isArray(arr) {
  180. return Object.prototype.toString.call(arr) === '[object Array]';
  181. }
  182. function noop() {}
  183. module.exports.actionJackerDecorator = function(emitter, options, fn, mapper) {
  184. options = clone(options);
  185. fn = fn || noop;
  186. if (typeof fn !== 'function') {
  187. throw new TypeError(
  188. 'The second argument must be a function callback. You have passed ' +
  189. typeof fn
  190. );
  191. }
  192. return function(err, data) {
  193. var resultantData = data;
  194. var metadata = {};
  195. // Allow for extra data if resultantData is an object
  196. if (resultantData && typeof resultantData === 'object') {
  197. metadata = resultantData;
  198. resultantData = resultantData.activationType;
  199. }
  200. // Sanitize the data
  201. if (resultantData) {
  202. resultantData = resultantData.toLowerCase().trim();
  203. if (resultantData.match(/^activate|clicked$/)) {
  204. resultantData = 'activate';
  205. }
  206. }
  207. fn.apply(emitter, [err, resultantData, metadata]);
  208. if (!mapper || !resultantData) return;
  209. var key = mapper(resultantData);
  210. if (!key) return;
  211. emitter.emit(key, emitter, options, metadata);
  212. };
  213. };
  214. module.exports.constructArgumentList = function(options, extra) {
  215. var args = [];
  216. extra = extra || {};
  217. // Massive ugly setup. Default args
  218. var initial = extra.initial || [];
  219. var keyExtra = extra.keyExtra || '';
  220. var allowedArguments = extra.allowedArguments || [];
  221. var noEscape = extra.noEscape !== void 0;
  222. var checkForAllowed = extra.allowedArguments !== void 0;
  223. var explicitTrue = !!extra.explicitTrue;
  224. var keepNewlines = !!extra.keepNewlines;
  225. var wrapper = extra.wrapper === void 0 ? '"' : extra.wrapper;
  226. var escapeFn = function(arg) {
  227. if (isArray(arg)) {
  228. return removeNewLines(arg.join(','));
  229. }
  230. if (!noEscape) {
  231. arg = escapeQuotes(arg);
  232. }
  233. if (typeof arg === 'string' && !keepNewlines) {
  234. arg = removeNewLines(arg);
  235. }
  236. return wrapper + arg + wrapper;
  237. };
  238. initial.forEach(function(val) {
  239. args.push(escapeFn(val));
  240. });
  241. for (var key in options) {
  242. if (
  243. options.hasOwnProperty(key) &&
  244. (!checkForAllowed || inArray(allowedArguments, key))
  245. ) {
  246. if (explicitTrue && options[key] === true) {
  247. args.push('-' + keyExtra + key);
  248. } else if (explicitTrue && options[key] === false) continue;
  249. else args.push('-' + keyExtra + key, escapeFn(options[key]));
  250. }
  251. }
  252. return args;
  253. };
  254. function removeNewLines(str) {
  255. var excapedNewline = process.platform === 'win32' ? '\\r\\n' : '\\n';
  256. return str.replace(/\r?\n/g, excapedNewline);
  257. }
  258. /*
  259. ---- Options ----
  260. [-t] <title string> | Displayed on the first line of the toast.
  261. [-m] <message string> | Displayed on the remaining lines, wrapped.
  262. [-p] <image URI> | Display toast with an image, local files only.
  263. [-w] | Wait for toast to expire or activate.
  264. [-id] <id> | sets the id for a notification to be able to close it later.
  265. [-s] <sound URI> | Sets the sound of the notifications, for possible values see http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx.
  266. [-silent] | Don't play a sound file when showing the notifications.
  267. [-appID] <App.ID> | Don't create a shortcut but use the provided app id.
  268. -close <id> | Closes a currently displayed notification, in order to be able to close a notification the parameter -w must be used to create the notification.
  269. */
  270. var allowedToasterFlags = [
  271. 't',
  272. 'm',
  273. 'p',
  274. 'w',
  275. 'id',
  276. 's',
  277. 'silent',
  278. 'appID',
  279. 'close',
  280. 'install'
  281. ];
  282. var toasterSoundPrefix = 'Notification.';
  283. var toasterDefaultSound = 'Notification.Default';
  284. module.exports.mapToWin8 = function(options) {
  285. options = mapAppIcon(options);
  286. options = mapText(options);
  287. if (options.icon) {
  288. if (/^file:\/+/.test(options.icon)) {
  289. // should parse file protocol URL to path
  290. options.p = url
  291. .parse(options.icon)
  292. .pathname.replace(/^\/(\w:\/)/, '$1')
  293. .replace(/\//g, '\\');
  294. } else {
  295. options.p = options.icon;
  296. }
  297. delete options.icon;
  298. }
  299. if (options.message) {
  300. // Remove escape char to debug "HRESULT : 0xC00CE508" exception
  301. options.m = options.message.replace(/\x1b/g, '');
  302. delete options.message;
  303. }
  304. if (options.title) {
  305. options.t = options.title;
  306. delete options.title;
  307. }
  308. if (options.appName) {
  309. options.appID = options.appName;
  310. delete options.appName;
  311. }
  312. if (typeof options.remove !== 'undefined') {
  313. options.close = options.remove;
  314. delete options.remove;
  315. }
  316. if (options.quiet || options.silent) {
  317. options.silent = options.quiet || options.silent;
  318. delete options.quiet;
  319. }
  320. if (typeof options.sound !== 'undefined') {
  321. options.s = options.sound;
  322. delete options.sound;
  323. }
  324. if (options.s === false) {
  325. options.silent = true;
  326. delete options.s;
  327. }
  328. // Silent takes precedence. Remove sound.
  329. if (options.s && options.silent) {
  330. delete options.s;
  331. }
  332. if (options.s === true) {
  333. options.s = toasterDefaultSound;
  334. }
  335. if (options.s && options.s.indexOf(toasterSoundPrefix) !== 0) {
  336. options.s = toasterDefaultSound;
  337. }
  338. if (options.wait) {
  339. options.w = options.wait;
  340. delete options.wait;
  341. }
  342. for (var key in options) {
  343. // Check if is allowed. If not, delete!
  344. if (
  345. options.hasOwnProperty(key) &&
  346. allowedToasterFlags.indexOf(key) === -1
  347. ) {
  348. delete options[key];
  349. }
  350. }
  351. return options;
  352. };
  353. module.exports.mapToNotifu = function(options) {
  354. options = mapAppIcon(options);
  355. options = mapText(options);
  356. if (options.icon) {
  357. options.i = options.icon;
  358. delete options.icon;
  359. }
  360. if (options.message) {
  361. options.m = options.message;
  362. delete options.message;
  363. }
  364. if (options.title) {
  365. options.p = options.title;
  366. delete options.title;
  367. }
  368. if (options.time) {
  369. options.d = options.time;
  370. delete options.time;
  371. }
  372. if (options.q !== false) {
  373. options.q = true;
  374. } else {
  375. delete options.q;
  376. }
  377. if (options.quiet === false) {
  378. delete options.q;
  379. delete options.quiet;
  380. }
  381. if (options.sound) {
  382. delete options.q;
  383. delete options.sound;
  384. }
  385. if (options.t) {
  386. options.d = options.t;
  387. delete options.t;
  388. }
  389. if (options.type) {
  390. options.t = sanitizeNotifuTypeArgument(options.type);
  391. delete options.type;
  392. }
  393. return options;
  394. };
  395. module.exports.isMac = function() {
  396. return os.type() === 'Darwin';
  397. };
  398. module.exports.isMountainLion = function() {
  399. return (
  400. os.type() === 'Darwin' &&
  401. semver.satisfies(garanteeSemverFormat(os.release()), '>=12.0.0')
  402. );
  403. };
  404. module.exports.isWin8 = function() {
  405. return (
  406. os.type() === 'Windows_NT' &&
  407. semver.satisfies(garanteeSemverFormat(os.release()), '>=6.2.9200')
  408. );
  409. };
  410. module.exports.isLessThanWin8 = function() {
  411. return (
  412. os.type() === 'Windows_NT' &&
  413. semver.satisfies(garanteeSemverFormat(os.release()), '<6.2.9200')
  414. );
  415. };
  416. function garanteeSemverFormat(version) {
  417. if (version.split('.').length === 2) {
  418. version += '.0';
  419. }
  420. return version;
  421. }
  422. function sanitizeNotifuTypeArgument(type) {
  423. if (typeof type === 'string' || type instanceof String) {
  424. if (type.toLowerCase() === 'info') return 'info';
  425. if (type.toLowerCase() === 'warn') return 'warn';
  426. if (type.toLowerCase() === 'error') return 'error';
  427. }
  428. return 'info';
  429. }