You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

515 lines
12 KiB

4 years ago
  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. options.json = true;
  179. return options;
  180. };
  181. function isArray(arr) {
  182. return Object.prototype.toString.call(arr) === '[object Array]';
  183. }
  184. function noop() {}
  185. module.exports.actionJackerDecorator = function(emitter, options, fn, mapper) {
  186. options = clone(options);
  187. fn = fn || noop;
  188. if (typeof fn !== 'function') {
  189. throw new TypeError(
  190. 'The second argument must be a function callback. You have passed ' +
  191. typeof fn
  192. );
  193. }
  194. return function(err, data) {
  195. var resultantData = data;
  196. var metadata = {};
  197. // Allow for extra data if resultantData is an object
  198. if (resultantData && typeof resultantData === 'object') {
  199. metadata = resultantData;
  200. resultantData = resultantData.activationType;
  201. }
  202. // Sanitize the data
  203. if (resultantData) {
  204. resultantData = resultantData.toLowerCase().trim();
  205. if (resultantData.match(/^activate|clicked$/)) {
  206. resultantData = 'activate';
  207. }
  208. }
  209. fn.apply(emitter, [err, resultantData, metadata]);
  210. if (!mapper || !resultantData) return;
  211. var key = mapper(resultantData);
  212. if (!key) return;
  213. emitter.emit(key, emitter, options, metadata);
  214. };
  215. };
  216. module.exports.constructArgumentList = function(options, extra) {
  217. var args = [];
  218. extra = extra || {};
  219. // Massive ugly setup. Default args
  220. var initial = extra.initial || [];
  221. var keyExtra = extra.keyExtra || '';
  222. var allowedArguments = extra.allowedArguments || [];
  223. var noEscape = extra.noEscape !== void 0;
  224. var checkForAllowed = extra.allowedArguments !== void 0;
  225. var explicitTrue = !!extra.explicitTrue;
  226. var keepNewlines = !!extra.keepNewlines;
  227. var wrapper = extra.wrapper === void 0 ? '"' : extra.wrapper;
  228. var escapeFn = function(arg) {
  229. if (isArray(arg)) {
  230. return removeNewLines(arg.join(','));
  231. }
  232. if (!noEscape) {
  233. arg = escapeQuotes(arg);
  234. }
  235. if (typeof arg === 'string' && !keepNewlines) {
  236. arg = removeNewLines(arg);
  237. }
  238. return wrapper + arg + wrapper;
  239. };
  240. initial.forEach(function(val) {
  241. args.push(escapeFn(val));
  242. });
  243. for (var key in options) {
  244. if (
  245. options.hasOwnProperty(key) &&
  246. (!checkForAllowed || inArray(allowedArguments, key))
  247. ) {
  248. if (explicitTrue && options[key] === true) {
  249. args.push('-' + keyExtra + key);
  250. } else if (explicitTrue && options[key] === false) continue;
  251. else args.push('-' + keyExtra + key, escapeFn(options[key]));
  252. }
  253. }
  254. return args;
  255. };
  256. function removeNewLines(str) {
  257. var excapedNewline = process.platform === 'win32' ? '\\r\\n' : '\\n';
  258. return str.replace(/\r?\n/g, excapedNewline);
  259. }
  260. /*
  261. ---- Options ----
  262. [-t] <title string> | Displayed on the first line of the toast.
  263. [-m] <message string> | Displayed on the remaining lines, wrapped.
  264. [-p] <image URI> | Display toast with an image, local files only.
  265. [-w] | Wait for toast to expire or activate.
  266. [-id] <id> | sets the id for a notification to be able to close it later.
  267. [-s] <sound URI> | Sets the sound of the notifications, for possible values see http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx.
  268. [-silent] | Don't play a sound file when showing the notifications.
  269. [-appID] <App.ID> | Don't create a shortcut but use the provided app id.
  270. -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.
  271. */
  272. var allowedToasterFlags = [
  273. 't',
  274. 'm',
  275. 'p',
  276. 'w',
  277. 'id',
  278. 's',
  279. 'silent',
  280. 'appID',
  281. 'close',
  282. 'install'
  283. ];
  284. var toasterSoundPrefix = 'Notification.';
  285. var toasterDefaultSound = 'Notification.Default';
  286. module.exports.mapToWin8 = function(options) {
  287. options = mapAppIcon(options);
  288. options = mapText(options);
  289. if (options.icon) {
  290. if (/^file:\/+/.test(options.icon)) {
  291. // should parse file protocol URL to path
  292. options.p = new url.URL(options.icon).pathname
  293. .replace(/^\/(\w:\/)/, '$1')
  294. .replace(/\//g, '\\');
  295. } else {
  296. options.p = options.icon;
  297. }
  298. delete options.icon;
  299. }
  300. if (options.message) {
  301. // Remove escape char to debug "HRESULT : 0xC00CE508" exception
  302. options.m = options.message.replace(/\x1b/g, '');
  303. delete options.message;
  304. }
  305. if (options.title) {
  306. options.t = options.title;
  307. delete options.title;
  308. }
  309. if (options.appName) {
  310. options.appID = options.appName;
  311. delete options.appName;
  312. }
  313. if (typeof options.remove !== 'undefined') {
  314. options.close = options.remove;
  315. delete options.remove;
  316. }
  317. if (options.quiet || options.silent) {
  318. options.silent = options.quiet || options.silent;
  319. delete options.quiet;
  320. }
  321. if (typeof options.sound !== 'undefined') {
  322. options.s = options.sound;
  323. delete options.sound;
  324. }
  325. if (options.s === false) {
  326. options.silent = true;
  327. delete options.s;
  328. }
  329. // Silent takes precedence. Remove sound.
  330. if (options.s && options.silent) {
  331. delete options.s;
  332. }
  333. if (options.s === true) {
  334. options.s = toasterDefaultSound;
  335. }
  336. if (options.s && options.s.indexOf(toasterSoundPrefix) !== 0) {
  337. options.s = toasterDefaultSound;
  338. }
  339. if (options.wait) {
  340. options.w = options.wait;
  341. delete options.wait;
  342. }
  343. for (var key in options) {
  344. // Check if is allowed. If not, delete!
  345. if (
  346. options.hasOwnProperty(key) &&
  347. allowedToasterFlags.indexOf(key) === -1
  348. ) {
  349. delete options[key];
  350. }
  351. }
  352. return options;
  353. };
  354. module.exports.mapToNotifu = function(options) {
  355. options = mapAppIcon(options);
  356. options = mapText(options);
  357. if (options.icon) {
  358. options.i = options.icon;
  359. delete options.icon;
  360. }
  361. if (options.message) {
  362. options.m = options.message;
  363. delete options.message;
  364. }
  365. if (options.title) {
  366. options.p = options.title;
  367. delete options.title;
  368. }
  369. if (options.time) {
  370. options.d = options.time;
  371. delete options.time;
  372. }
  373. if (options.q !== false) {
  374. options.q = true;
  375. } else {
  376. delete options.q;
  377. }
  378. if (options.quiet === false) {
  379. delete options.q;
  380. delete options.quiet;
  381. }
  382. if (options.sound) {
  383. delete options.q;
  384. delete options.sound;
  385. }
  386. if (options.t) {
  387. options.d = options.t;
  388. delete options.t;
  389. }
  390. if (options.type) {
  391. options.t = sanitizeNotifuTypeArgument(options.type);
  392. delete options.type;
  393. }
  394. return options;
  395. };
  396. module.exports.isMac = function() {
  397. return os.type() === 'Darwin';
  398. };
  399. module.exports.isMountainLion = function() {
  400. return (
  401. os.type() === 'Darwin' &&
  402. semver.satisfies(garanteeSemverFormat(os.release()), '>=12.0.0')
  403. );
  404. };
  405. module.exports.isWin8 = function() {
  406. return (
  407. os.type() === 'Windows_NT' &&
  408. semver.satisfies(garanteeSemverFormat(os.release()), '>=6.2.9200')
  409. );
  410. };
  411. module.exports.isWSL = function() {
  412. return isWSL;
  413. };
  414. module.exports.isLessThanWin8 = function() {
  415. return (
  416. os.type() === 'Windows_NT' &&
  417. semver.satisfies(garanteeSemverFormat(os.release()), '<6.2.9200')
  418. );
  419. };
  420. function garanteeSemverFormat(version) {
  421. if (version.split('.').length === 2) {
  422. version += '.0';
  423. }
  424. return version;
  425. }
  426. function sanitizeNotifuTypeArgument(type) {
  427. if (typeof type === 'string' || type instanceof String) {
  428. if (type.toLowerCase() === 'info') return 'info';
  429. if (type.toLowerCase() === 'warn') return 'warn';
  430. if (type.toLowerCase() === 'error') return 'error';
  431. }
  432. return 'info';
  433. }