UNPKG

14.5 kBJavaScriptView Raw
1const shellwords = require('shellwords');
2const cp = require('child_process');
3const semver = require('semver');
4const isWSL = require('is-wsl');
5const path = require('path');
6const url = require('url');
7const os = require('os');
8const fs = require('fs');
9const net = require('net');
10
11const BUFFER_SIZE = 1024;
12
13function clone(obj) {
14 return JSON.parse(JSON.stringify(obj));
15}
16
17module.exports.clone = clone;
18
19const escapeQuotes = function (str) {
20 if (typeof str === 'string') {
21 return str.replace(/(["$`\\])/g, '\\$1');
22 } else {
23 return str;
24 }
25};
26
27const inArray = function (arr, val) {
28 return arr.indexOf(val) !== -1;
29};
30
31const notifySendFlags = {
32 u: 'urgency',
33 urgency: 'urgency',
34 t: 'expire-time',
35 time: 'expire-time',
36 timeout: 'expire-time',
37 e: 'expire-time',
38 expire: 'expire-time',
39 'expire-time': 'expire-time',
40 i: 'icon',
41 icon: 'icon',
42 c: 'category',
43 category: 'category',
44 subtitle: 'category',
45 h: 'hint',
46 hint: 'hint',
47 a: 'app-name',
48 'app-name': 'app-name'
49};
50
51module.exports.command = function (notifier, options, cb) {
52 notifier = shellwords.escape(notifier);
53 if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
54 console.info('node-notifier debug info (command):');
55 console.info('[notifier path]', notifier);
56 console.info('[notifier options]', options.join(' '));
57 }
58
59 return cp.exec(notifier + ' ' + options.join(' '), function (
60 error,
61 stdout,
62 stderr
63 ) {
64 if (error) return cb(error);
65 cb(stderr, stdout);
66 });
67};
68
69module.exports.fileCommand = function (notifier, options, cb) {
70 if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
71 console.info('node-notifier debug info (fileCommand):');
72 console.info('[notifier path]', notifier);
73 console.info('[notifier options]', options.join(' '));
74 }
75
76 return cp.execFile(notifier, options, function (error, stdout, stderr) {
77 if (error) return cb(error, stdout);
78 cb(stderr, stdout);
79 });
80};
81
82module.exports.fileCommandJson = function (notifier, options, cb) {
83 if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
84 console.info('node-notifier debug info (fileCommandJson):');
85 console.info('[notifier path]', notifier);
86 console.info('[notifier options]', options.join(' '));
87 }
88 return cp.execFile(notifier, options, function (error, stdout, stderr) {
89 if (error) return cb(error, stdout);
90 if (!stdout) return cb(error, {});
91
92 try {
93 const data = JSON.parse(stdout);
94 cb(!stderr ? null : stderr, data);
95 } catch (e) {
96 cb(e, stdout);
97 }
98 });
99};
100
101module.exports.immediateFileCommand = function (notifier, options, cb) {
102 if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
103 console.info('node-notifier debug info (notifier):');
104 console.info('[notifier path]', notifier);
105 }
106
107 notifierExists(notifier, function (_, exists) {
108 if (!exists) {
109 return cb(new Error('Notifier (' + notifier + ') not found on system.'));
110 }
111 cp.execFile(notifier, options);
112 cb();
113 });
114};
115
116function notifierExists(notifier, cb) {
117 return fs.stat(notifier, function (err, stat) {
118 if (!err) return cb(err, stat.isFile());
119
120 // Check if Windows alias
121 if (path.extname(notifier)) {
122 // Has extentioon, no need to check more
123 return cb(err, false);
124 }
125
126 // Check if there is an exe file in the directory
127 return fs.stat(notifier + '.exe', function (err, stat) {
128 if (err) return cb(err, false);
129 cb(err, stat.isFile());
130 });
131 });
132}
133
134const mapAppIcon = function (options) {
135 if (options.appIcon) {
136 options.icon = options.appIcon;
137 delete options.appIcon;
138 }
139
140 return options;
141};
142
143const mapText = function (options) {
144 if (options.text) {
145 options.message = options.text;
146 delete options.text;
147 }
148
149 return options;
150};
151
152const mapIconShorthand = function (options) {
153 if (options.i) {
154 options.icon = options.i;
155 delete options.i;
156 }
157
158 return options;
159};
160
161module.exports.mapToNotifySend = function (options) {
162 options = mapAppIcon(options);
163 options = mapText(options);
164
165 if (options.timeout === false) {
166 delete options.timeout;
167 }
168 if (options.wait === true) {
169 options['expire-time'] = 5; // 5 seconds default time (multipled below)
170 }
171 for (const key in options) {
172 if (key === 'message' || key === 'title') continue;
173 if (options.hasOwnProperty(key) && notifySendFlags[key] !== key) {
174 options[notifySendFlags[key]] = options[key];
175 delete options[key];
176 }
177 }
178 if (typeof options['expire-time'] === 'undefined') {
179 options['expire-time'] = 10 * 1000; // 10 sec timeout by default
180 } else if (typeof options['expire-time'] === 'number') {
181 options['expire-time'] = options['expire-time'] * 1000; // notify send uses milliseconds
182 }
183
184 return options;
185};
186
187module.exports.mapToGrowl = function (options) {
188 options = mapAppIcon(options);
189 options = mapIconShorthand(options);
190 options = mapText(options);
191
192 if (options.icon && !Buffer.isBuffer(options.icon)) {
193 try {
194 options.icon = fs.readFileSync(options.icon);
195 } catch (ex) {}
196 }
197
198 return options;
199};
200
201module.exports.mapToMac = function (options) {
202 options = mapIconShorthand(options);
203 options = mapText(options);
204
205 if (options.icon) {
206 options.appIcon = options.icon;
207 delete options.icon;
208 }
209
210 if (options.sound === true) {
211 options.sound = 'Bottle';
212 }
213
214 if (options.sound === false) {
215 delete options.sound;
216 }
217
218 if (options.sound && options.sound.indexOf('Notification.') === 0) {
219 options.sound = 'Bottle';
220 }
221
222 if (options.wait === true) {
223 if (!options.timeout) {
224 options.timeout = 5;
225 }
226 delete options.wait;
227 }
228
229 if (!options.wait && !options.timeout) {
230 if (options.timeout === false) {
231 delete options.timeout;
232 } else {
233 options.timeout = 10;
234 }
235 }
236
237 options.json = true;
238 return options;
239};
240
241function isArray(arr) {
242 return Object.prototype.toString.call(arr) === '[object Array]';
243}
244module.exports.isArray = isArray;
245
246function noop() {}
247module.exports.actionJackerDecorator = function (emitter, options, fn, mapper) {
248 options = clone(options);
249 fn = fn || noop;
250
251 if (typeof fn !== 'function') {
252 throw new TypeError(
253 'The second argument must be a function callback. You have passed ' +
254 typeof fn
255 );
256 }
257
258 return function (err, data) {
259 let resultantData = data;
260 let metadata = {};
261 // Allow for extra data if resultantData is an object
262 if (resultantData && typeof resultantData === 'object') {
263 metadata = resultantData;
264 resultantData = resultantData.activationType;
265 }
266
267 // Sanitize the data
268 if (resultantData) {
269 resultantData = resultantData.toLowerCase().trim();
270 if (resultantData.match(/^activate|clicked$/)) {
271 resultantData = 'activate';
272 }
273 if (resultantData.match(/^timedout$/)) {
274 resultantData = 'timeout';
275 }
276 }
277
278 fn.apply(emitter, [err, resultantData, metadata]);
279 if (!mapper || !resultantData) return;
280
281 const key = mapper(resultantData);
282 if (!key) return;
283 emitter.emit(key, emitter, options, metadata);
284 };
285};
286
287module.exports.constructArgumentList = function (options, extra) {
288 const args = [];
289 extra = extra || {};
290
291 // Massive ugly setup. Default args
292 const initial = extra.initial || [];
293 const keyExtra = extra.keyExtra || '';
294 const allowedArguments = extra.allowedArguments || [];
295 const noEscape = extra.noEscape !== undefined;
296 const checkForAllowed = extra.allowedArguments !== undefined;
297 const explicitTrue = !!extra.explicitTrue;
298 const keepNewlines = !!extra.keepNewlines;
299 const wrapper = extra.wrapper === undefined ? '"' : extra.wrapper;
300
301 const escapeFn = function escapeFn(arg) {
302 if (isArray(arg)) {
303 return removeNewLines(arg.map(escapeFn).join(','));
304 }
305
306 if (!noEscape) {
307 arg = escapeQuotes(arg);
308 }
309 if (typeof arg === 'string' && !keepNewlines) {
310 arg = removeNewLines(arg);
311 }
312 return wrapper + arg + wrapper;
313 };
314
315 initial.forEach(function (val) {
316 args.push(escapeFn(val));
317 });
318 for (const key in options) {
319 if (
320 options.hasOwnProperty(key) &&
321 (!checkForAllowed || inArray(allowedArguments, key))
322 ) {
323 if (explicitTrue && options[key] === true) {
324 args.push('-' + keyExtra + key);
325 } else if (explicitTrue && options[key] === false) continue;
326 else args.push('-' + keyExtra + key, escapeFn(options[key]));
327 }
328 }
329 return args;
330};
331
332function removeNewLines(str) {
333 const excapedNewline = process.platform === 'win32' ? '\\r\\n' : '\\n';
334 return str.replace(/\r?\n/g, excapedNewline);
335}
336
337/*
338---- Options ----
339[-t] <title string> | Displayed on the first line of the toast.
340[-m] <message string> | Displayed on the remaining lines, wrapped.
341[-b] <button1;button2 string>| Displayed on the bottom line, can list multiple buttons separated by ";"
342[-tb] | Displayed a textbox on the bottom line, only if buttons are not presented.
343[-p] <image URI> | Display toast with an image, local files only.
344[-id] <id> | sets the id for a notification to be able to close it later.
345[-s] <sound URI> | Sets the sound of the notifications, for possible values see http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx.
346[-silent] | Don't play a sound file when showing the notifications.
347[-appID] <App.ID> | Don't create a shortcut but use the provided app id.
348[-pid] <pid> | Query the appid for the process <pid>, use -appID as fallback. (Only relevant for applications that might be packaged for the store)
349[-pipeName] <\.\pipe\pipeName\> | Provide a name pipe which is used for callbacks.
350[-application] <C:\foo.exe> | Provide a application that might be started if the pipe does not exist.
351-close <id> | Closes a currently displayed notification.
352*/
353const allowedToasterFlags = [
354 't',
355 'm',
356 'b',
357 'tb',
358 'p',
359 'id',
360 's',
361 'silent',
362 'appID',
363 'pid',
364 'pipeName',
365 'close',
366 'install'
367];
368const toasterSoundPrefix = 'Notification.';
369const toasterDefaultSound = 'Notification.Default';
370module.exports.mapToWin8 = function (options) {
371 options = mapAppIcon(options);
372 options = mapText(options);
373
374 if (options.icon) {
375 if (/^file:\/+/.test(options.icon)) {
376 // should parse file protocol URL to path
377 options.p = new url.URL(options.icon).pathname
378 .replace(/^\/(\w:\/)/, '$1')
379 .replace(/\//g, '\\');
380 } else {
381 options.p = options.icon;
382 }
383 delete options.icon;
384 }
385
386 if (options.message) {
387 // Remove escape char to debug "HRESULT : 0xC00CE508" exception
388 options.m = options.message.replace(/\x1b/g, '');
389 delete options.message;
390 }
391
392 if (options.title) {
393 options.t = options.title;
394 delete options.title;
395 }
396
397 if (options.appName) {
398 options.appID = options.appName;
399 delete options.appName;
400 }
401
402 if (typeof options.remove !== 'undefined') {
403 options.close = options.remove;
404 delete options.remove;
405 }
406
407 if (options.quiet || options.silent) {
408 options.silent = options.quiet || options.silent;
409 delete options.quiet;
410 }
411
412 if (typeof options.sound !== 'undefined') {
413 options.s = options.sound;
414 delete options.sound;
415 }
416
417 if (options.s === false) {
418 options.silent = true;
419 delete options.s;
420 }
421
422 // Silent takes precedence. Remove sound.
423 if (options.s && options.silent) {
424 delete options.s;
425 }
426
427 if (options.s === true) {
428 options.s = toasterDefaultSound;
429 }
430
431 if (options.s && options.s.indexOf(toasterSoundPrefix) !== 0) {
432 options.s = toasterDefaultSound;
433 }
434
435 if (options.actions && isArray(options.actions)) {
436 options.b = options.actions.join(';');
437 delete options.actions;
438 }
439
440 for (const key in options) {
441 // Check if is allowed. If not, delete!
442 if (
443 options.hasOwnProperty(key) &&
444 allowedToasterFlags.indexOf(key) === -1
445 ) {
446 delete options[key];
447 }
448 }
449
450 return options;
451};
452
453module.exports.mapToNotifu = function (options) {
454 options = mapAppIcon(options);
455 options = mapText(options);
456
457 if (options.icon) {
458 options.i = options.icon;
459 delete options.icon;
460 }
461
462 if (options.message) {
463 options.m = options.message;
464 delete options.message;
465 }
466
467 if (options.title) {
468 options.p = options.title;
469 delete options.title;
470 }
471
472 if (options.time) {
473 options.d = options.time;
474 delete options.time;
475 }
476
477 if (options.q !== false) {
478 options.q = true;
479 } else {
480 delete options.q;
481 }
482
483 if (options.quiet === false) {
484 delete options.q;
485 delete options.quiet;
486 }
487
488 if (options.sound) {
489 delete options.q;
490 delete options.sound;
491 }
492
493 if (options.t) {
494 options.d = options.t;
495 delete options.t;
496 }
497
498 if (options.type) {
499 options.t = sanitizeNotifuTypeArgument(options.type);
500 delete options.type;
501 }
502
503 return options;
504};
505
506module.exports.isMac = function () {
507 return os.type() === 'Darwin';
508};
509
510module.exports.isMountainLion = function () {
511 return (
512 os.type() === 'Darwin' &&
513 semver.satisfies(garanteeSemverFormat(os.release()), '>=12.0.0')
514 );
515};
516
517module.exports.isWin8 = function () {
518 return (
519 os.type() === 'Windows_NT' &&
520 semver.satisfies(garanteeSemverFormat(os.release()), '>=6.2.9200')
521 );
522};
523
524module.exports.isWSL = function () {
525 return isWSL;
526};
527
528module.exports.isLessThanWin8 = function () {
529 return (
530 os.type() === 'Windows_NT' &&
531 semver.satisfies(garanteeSemverFormat(os.release()), '<6.2.9200')
532 );
533};
534
535function garanteeSemverFormat(version) {
536 if (version.split('.').length === 2) {
537 version += '.0';
538 }
539 return version;
540}
541
542function sanitizeNotifuTypeArgument(type) {
543 if (typeof type === 'string' || type instanceof String) {
544 if (type.toLowerCase() === 'info') return 'info';
545 if (type.toLowerCase() === 'warn') return 'warn';
546 if (type.toLowerCase() === 'error') return 'error';
547 }
548
549 return 'info';
550}
551
552module.exports.createNamedPipe = (server) => {
553 const buf = Buffer.alloc(BUFFER_SIZE);
554
555 return new Promise((resolve) => {
556 server.instance = net.createServer((stream) => {
557 stream.on('data', (c) => {
558 buf.write(c.toString());
559 });
560 stream.on('end', () => {
561 server.instance.close();
562 });
563 });
564 server.instance.listen(server.namedPipe, () => {
565 resolve(buf);
566 });
567 });
568};