utils.js 12 KB

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