utils.js
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 var net = require('net'); 10 11 const BUFFER_SIZE = 1024; 12 13 function clone(obj) { 14 return JSON.parse(JSON.stringify(obj)); 15 } 16 17 module.exports.clone = clone; 18 19 var escapeQuotes = function (str) { 20 if (typeof str === 'string') { 21 return str.replace(/(["$`\\])/g, '\\$1'); 22 } else { 23 return str; 24 } 25 }; 26 27 var inArray = function (arr, val) { 28 return arr.indexOf(val) !== -1; 29 }; 30 31 var 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 51 module.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 69 module.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 82 module.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 var data = JSON.parse(stdout); 94 cb(!stderr ? null : stderr, data); 95 } catch (e) { 96 cb(e, stdout); 97 } 98 }); 99 }; 100 101 module.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 116 function 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 134 var mapAppIcon = function (options) { 135 if (options.appIcon) { 136 options.icon = options.appIcon; 137 delete options.appIcon; 138 } 139 140 return options; 141 }; 142 143 var mapText = function (options) { 144 if (options.text) { 145 options.message = options.text; 146 delete options.text; 147 } 148 149 return options; 150 }; 151 152 var mapIconShorthand = function (options) { 153 if (options.i) { 154 options.icon = options.i; 155 delete options.i; 156 } 157 158 return options; 159 }; 160 161 module.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 (var 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 187 module.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 201 module.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 241 function isArray(arr) { 242 return Object.prototype.toString.call(arr) === '[object Array]'; 243 } 244 module.exports.isArray = isArray; 245 246 function noop() {} 247 module.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 var resultantData = data; 260 var 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 var key = mapper(resultantData); 282 if (!key) return; 283 emitter.emit(key, emitter, options, metadata); 284 }; 285 }; 286 287 module.exports.constructArgumentList = function (options, extra) { 288 var args = []; 289 extra = extra || {}; 290 291 // Massive ugly setup. Default args 292 var initial = extra.initial || []; 293 var keyExtra = extra.keyExtra || ''; 294 var allowedArguments = extra.allowedArguments || []; 295 var noEscape = extra.noEscape !== undefined; 296 var checkForAllowed = extra.allowedArguments !== undefined; 297 var explicitTrue = !!extra.explicitTrue; 298 var keepNewlines = !!extra.keepNewlines; 299 var wrapper = extra.wrapper === undefined ? '"' : extra.wrapper; 300 301 var 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 (var 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 332 function removeNewLines(str) { 333 var 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 */ 353 var 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 ]; 368 var toasterSoundPrefix = 'Notification.'; 369 var toasterDefaultSound = 'Notification.Default'; 370 module.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 (var 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 453 module.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 506 module.exports.isMac = function () { 507 return os.type() === 'Darwin'; 508 }; 509 510 module.exports.isMountainLion = function () { 511 return ( 512 os.type() === 'Darwin' && 513 semver.satisfies(garanteeSemverFormat(os.release()), '>=12.0.0') 514 ); 515 }; 516 517 module.exports.isWin8 = function () { 518 return ( 519 os.type() === 'Windows_NT' && 520 semver.satisfies(garanteeSemverFormat(os.release()), '>=6.2.9200') 521 ); 522 }; 523 524 module.exports.isWSL = function () { 525 return isWSL; 526 }; 527 528 module.exports.isLessThanWin8 = function () { 529 return ( 530 os.type() === 'Windows_NT' && 531 semver.satisfies(garanteeSemverFormat(os.release()), '<6.2.9200') 532 ); 533 }; 534 535 function garanteeSemverFormat(version) { 536 if (version.split('.').length === 2) { 537 version += '.0'; 538 } 539 return version; 540 } 541 542 function 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 552 module.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 };