-
Notifications
You must be signed in to change notification settings - Fork 332
Expand file tree
/
Copy pathqz-tray.js
More file actions
2852 lines (2574 loc) · 138 KB
/
qz-tray.js
File metadata and controls
2852 lines (2574 loc) · 138 KB
Edit and raw actions
OlderNewer
1
'use strict';
2
3
/**
4
* @version 2.2.4-SNAPSHOT
5
* @overview QZ Tray Connector
6
* @license LGPL-2.1-only
7
* <p/>
8
* Connects a web client to the QZ Tray software.
9
* Enables printing and device communication from javascript.
10
*/
11
var qz = (function() {
12
13
///// POLYFILLS /////
14
15
if (!Array.isArray) {
16
Array.isArray = function(arg) {
17
return Object.prototype.toString.call(arg) === '[object Array]';
18
};
19
}
20
21
if (!Number.isInteger) {
22
Number.isInteger = function(value) {
23
return typeof value === 'number' && isFinite(value) && Math.floor(value) === value;
24
};
25
}
26
27
///// PRIVATE METHODS /////
28
29
var _qz = {
30
VERSION: "2.2.4-SNAPSHOT", //must match @version above
31
DEBUG: false,
32
33
log: {
34
/** Debugging messages */
35
trace: function() { if (_qz.DEBUG) { console.log.apply(console, arguments); } },
36
/** General messages */
37
info: function() { console.info.apply(console, arguments); },
38
/** General warnings */
39
warn: function() { console.warn.apply(console, arguments); },
40
/** Debugging errors */
41
allay: function() { if (_qz.DEBUG) { console.warn.apply(console, arguments); } },
42
/** General errors */
43
error: function() { console.error.apply(console, arguments); }
44
},
45
46
47
//stream types
48
streams: {
49
serial: 'SERIAL', usb: 'USB', hid: 'HID', printer: 'PRINTER', file: 'FILE', socket: 'SOCKET'
50
},
51
52
53
websocket: {
54
/** The actual websocket object managing the connection. */
55
connection: null,
56
/** Track if a connection attempt is being cancelled. */
57
shutdown: false,
58
59
/** Default parameters used on new connections. Override values using options parameter on {@link qz.websocket.connect}. */
60
connectConfig: {
61
host: ["localhost", "localhost.qz.io"], //hosts QZ Tray can be running on
62
hostIndex: 0, //internal var - index on host array
63
usingSecure: true, //boolean use of secure protocol
64
protocol: {
65
secure: "wss://", //secure websocket
66
insecure: "ws://" //insecure websocket
67
},
68
port: {
69
secure: [8181, 8282, 8383, 8484], //list of secure ports QZ Tray could be listening on
70
insecure: [8182, 8283, 8384, 8485], //list of insecure ports QZ Tray could be listening on
71
portIndex: 0 //internal var - index on active port array
72
},
73
keepAlive: 60, //time between pings to keep connection alive, in seconds
74
retries: 0, //number of times to reconnect before failing
75
delay: 0 //seconds before firing a connection
76
},
77
78
setup: {
79
/** Loop through possible ports to open connection, sets web socket calls that will settle the promise. */
80
findConnection: function(config, resolve, reject) {
81
if (_qz.websocket.shutdown) {
82
reject(new Error("Connection attempt cancelled by user"));
83
return;
84
}
85
86
//force flag if missing ports
87
if (!config.port.secure.length) {
88
if (!config.port.insecure.length) {
89
reject(new Error("No ports have been specified to connect over"));
90
return;
91
} else if (config.usingSecure) {
92
_qz.log.error("No secure ports specified - forcing insecure connection");
93
config.usingSecure = false;
94
}
95
} else if (!config.port.insecure.length && !config.usingSecure) {
96
_qz.log.trace("No insecure ports specified - forcing secure connection");
97
config.usingSecure = true;
98
}
99
100
var deeper = function() {
101
if (_qz.websocket.shutdown) {
102
//connection attempt was cancelled, bail out
103
reject(new Error("Connection attempt cancelled by user"));
104
return;
105
}
106
107
config.port.portIndex++;
108
109
if ((config.usingSecure && config.port.portIndex >= config.port.secure.length)
110
|| (!config.usingSecure && config.port.portIndex >= config.port.insecure.length)) {
111
if (config.hostIndex >= config.host.length - 1) {
112
//give up, all hope is lost
113
reject(new Error("Unable to establish connection with QZ"));
114
return;
115
} else {
116
config.hostIndex++;
117
config.port.portIndex = 0;
118
}
119
}
120
121
// recursive call until connection established or all ports are exhausted
122
_qz.websocket.setup.findConnection(config, resolve, reject);
123
};
124
125
var address;
126
if (config.usingSecure) {
127
address = config.protocol.secure + config.host[config.hostIndex] + ":" + config.port.secure[config.port.portIndex];
128
} else {
129
address = config.protocol.insecure + config.host[config.hostIndex] + ":" + config.port.insecure[config.port.portIndex];
130
}
131
132
try {
133
_qz.log.trace("Attempting connection", address);
134
_qz.websocket.connection = new _qz.tools.ws(address);
135
}
136
catch(err) {
137
_qz.log.error(err);
138
deeper();
139
return;
140
}
141
142
if (_qz.websocket.connection != null) {
143
_qz.websocket.connection.established = false;
144
145
//called on successful connection to qz, begins setup of websocket calls and resolves connect promise after certificate is sent
146
_qz.websocket.connection.onopen = function(evt) {
147
if (!_qz.websocket.connection.established) {
148
_qz.log.trace(evt);
149
_qz.log.info("Established connection with QZ Tray on " + address);
150
151
_qz.websocket.setup.openConnection({ resolve: resolve, reject: reject });
152
153
if (config.keepAlive > 0) {
154
var interval = setInterval(function() {
155
if (!_qz.tools.isActive() || _qz.websocket.connection.interval !== interval) {
156
clearInterval(interval);
157
return;
158
}
159
160
_qz.websocket.connection.send("ping");
161
}, config.keepAlive * 1000);
162
163
_qz.websocket.connection.interval = interval;
164
}
165
}
166
};
167
168
//called during websocket close during setup
169
_qz.websocket.connection.onclose = function() {
170
// Safari compatibility fix to raise error event
171
if (_qz.websocket.connection && typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
172
_qz.websocket.connection.onerror();
173
}
174
};
175
176
//called for errors during setup (such as invalid ports), reject connect promise only if all ports have been tried
177
_qz.websocket.connection.onerror = function(evt) {
178
_qz.log.trace(evt);
179
180
_qz.websocket.connection = null;
181
182
deeper();
183
};
184
} else {
185
reject(new Error("Unable to create a websocket connection"));
186
}
187
},
188
189
/** Finish setting calls on successful connection, sets web socket calls that won't settle the promise. */
190
openConnection: function(openPromise) {
191
_qz.websocket.connection.established = true;
192
193
//called when an open connection is closed
194
_qz.websocket.connection.onclose = function(evt) {
195
_qz.log.trace(evt);
196
197
_qz.websocket.connection = null;
198
_qz.websocket.callClose(evt);
199
_qz.log.info("Closed connection with QZ Tray");
200
201
for(var uid in _qz.websocket.pendingCalls) {
202
if (_qz.websocket.pendingCalls.hasOwnProperty(uid)) {
203
_qz.websocket.pendingCalls[uid].reject(new Error("Connection closed before response received"));
204
}
205
}
206
207
//if this is set, then an explicit close call was made
208
if (this.promise != undefined) {
209
this.promise.resolve();
210
}
211
};
212
213
//called for any errors with an open connection
214
_qz.websocket.connection.onerror = function(evt) {
215
_qz.websocket.callError(evt);
216
};
217
218
//send JSON objects to qz
219
_qz.websocket.connection.sendData = function(obj) {
220
_qz.log.trace("Preparing object for websocket", obj);
221
222
if (obj.timestamp == undefined) {
223
obj.timestamp = Date.now();
224
if (typeof obj.timestamp !== 'number') {
225
obj.timestamp = new Date().getTime();
226
}
227
}
228
if (obj.promise != undefined) {
229
obj.uid = _qz.websocket.setup.newUID();
230
_qz.websocket.pendingCalls[obj.uid] = obj.promise;
231
}
232
233
// track requesting monitor
234
obj.position = {
235
x: typeof screen !== 'undefined' ? ((screen.availWidth || screen.width) / 2) + (screen.left || screen.availLeft || 0) : 0,
236
y: typeof screen !== 'undefined' ? ((screen.availHeight || screen.height) / 2) + (screen.top || screen.availTop || 0) : 0
237
};
238
239
try {
240
if (obj.call != undefined && obj.signature == undefined && _qz.security.needsSigned(obj.call)) {
241
var signObj = {
242
call: obj.call,
243
params: obj.params,
244
timestamp: obj.timestamp
245
};
246
247
//make a hashing promise if not already one
248
var hashing = _qz.tools.hash(_qz.tools.stringify(signObj));
249
if (!hashing.then) {
250
hashing = _qz.tools.promise(function(resolve) {
251
resolve(hashing);
252
});
253
}
254
255
hashing.then(function(hashed) {
256
return _qz.security.callSign(hashed);
257
}).then(function(signature) {
258
_qz.log.trace("Signature for call", signature);
259
obj.signature = signature || "";
260
obj.signAlgorithm = _qz.security.signAlgorithm;
261
262
_qz.signContent = undefined;
263
_qz.websocket.connection.send(_qz.tools.stringify(obj));
264
});
265
} else {
266
_qz.log.trace("Signature for call", obj.signature);
267
268
//called for pre-signed content and (unsigned) setup calls
269
_qz.websocket.connection.send(_qz.tools.stringify(obj));
270
}
271
}
272
catch(err) {
273
_qz.log.error(err);
274
275
if (obj.promise != undefined) {
276
obj.promise.reject(err);
277
delete _qz.websocket.pendingCalls[obj.uid];
278
}
279
}
280
};
281
282
//receive message from qz
283
_qz.websocket.connection.onmessage = function(evt) {
284
var returned = JSON.parse(evt.data);
285
286
if (returned.uid == null) {
287
if (returned.type == null) {
288
//incorrect response format, likely connected to incompatible qz version
289
_qz.websocket.connection.close(4003, "Connected to incompatible QZ Tray version");
290
291
} else {
292
//streams (callbacks only, no promises)
293
switch(returned.type) {
294
case _qz.streams.serial:
295
if (!returned.event) {
296
returned.event = JSON.stringify({ portName: returned.key, output: returned.data });
297
}
298
299
_qz.serial.callSerial(JSON.parse(returned.event));
300
break;
301
case _qz.streams.socket:
302
_qz.socket.callSocket(JSON.parse(returned.event));
303
break;
304
case _qz.streams.usb:
305
if (!returned.event) {
306
returned.event = JSON.stringify({ vendorId: returned.key[0], productId: returned.key[1], output: returned.data });
307
}
308
309
_qz.usb.callUsb(JSON.parse(returned.event));
310
break;
311
case _qz.streams.hid:
312
_qz.hid.callHid(JSON.parse(returned.event));
313
break;
314
case _qz.streams.printer:
315
_qz.printers.callPrinter(JSON.parse(returned.event));
316
break;
317
case _qz.streams.file:
318
_qz.file.callFile(JSON.parse(returned.event));
319
break;
320
default:
321
_qz.log.allay("Cannot determine stream type for callback", returned);
322
break;
323
}
324
}
325
326
return;
327
}
328
329
_qz.log.trace("Received response from websocket", returned);
330
331
var promise = _qz.websocket.pendingCalls[returned.uid];
332
if (promise == undefined) {
333
_qz.log.allay('No promise found for returned response');
334
} else {
335
if (returned.error != undefined) {
336
promise.reject(new Error(returned.error));
337
} else {
338
promise.resolve(returned.result);
339
}
340
}
341
342
delete _qz.websocket.pendingCalls[returned.uid];
343
};
344
345
346
//send up the certificate before making any calls
347
//also gives the user a chance to deny the connection
348
function sendCert(cert) {
349
if (cert === undefined) { cert = null; }
350
351
//websocket setup, query what version is connected
352
qz.api.getVersion().then(function(version) {
353
_qz.websocket.connection.version = version;
354
_qz.websocket.connection.semver = version.toLowerCase().replace(/-rc\./g, "-rc").split(/[\\+\\.-]/g);
355
for(var i = 0; i < _qz.websocket.connection.semver.length; i++) {
356
try {
357
if (i == 3 && _qz.websocket.connection.semver[i].toLowerCase().indexOf("rc") == 0) {
358
// Handle "rc1" pre-release by negating build info
359
_qz.websocket.connection.semver[i] = -(_qz.websocket.connection.semver[i].replace(/\D/g, ""));
360
continue;
361
}
362
_qz.websocket.connection.semver[i] = parseInt(_qz.websocket.connection.semver[i]);
363
}
364
catch(ignore) {}
365
366
if (_qz.websocket.connection.semver.length < 4) {
367
_qz.websocket.connection.semver[3] = 0;
368
}
369
}
370
371
//algorithm can be declared before a connection, check for incompatibilities now that we have one
372
_qz.compatible.algorithm(true);
373
}).then(function() {
374
_qz.websocket.connection.sendData({ certificate: cert, promise: openPromise });
375
});
376
}
377
378
_qz.security.callCert().then(sendCert).catch(function(error) {
379
_qz.log.warn("Failed to get certificate:", error);
380
381
if (_qz.security.rejectOnCertFailure) {
382
openPromise.reject(error);
383
} else {
384
sendCert(null);
385
}
386
});
387
},
388
389
/** Generate unique ID used to map a response to a call. */
390
newUID: function() {
391
var len = 6;
392
return (new Array(len + 1).join("0") + (Math.random() * Math.pow(36, len) << 0).toString(36)).slice(-len)
393
}
394
},
395
396
dataPromise: function(callName, params, signature, signingTimestamp) {
397
return _qz.tools.promise(function(resolve, reject) {
398
var msg = {
399
call: callName,
400
promise: { resolve: resolve, reject: reject },
401
params: params,
402
signature: signature,
403
timestamp: signingTimestamp
404
};
405
406
_qz.websocket.connection.sendData(msg);
407
});
408
},
409
410
/** Library of promises awaiting a response, uid -> promise */
411
pendingCalls: {},
412
413
/** List of functions to call on error from the websocket. */
414
errorCallbacks: [],
415
/** Calls all functions registered to listen for errors. */
416
callError: function(evt) {
417
if (Array.isArray(_qz.websocket.errorCallbacks)) {
418
for(var i = 0; i < _qz.websocket.errorCallbacks.length; i++) {
419
_qz.websocket.errorCallbacks[i](evt);
420
}
421
} else {
422
_qz.websocket.errorCallbacks(evt);
423
}
424
},
425
426
/** List of function to call on closing from the websocket. */
427
closedCallbacks: [],
428
/** Calls all functions registered to listen for closing. */
429
callClose: function(evt) {
430
if (Array.isArray(_qz.websocket.closedCallbacks)) {
431
for(var i = 0; i < _qz.websocket.closedCallbacks.length; i++) {
432
_qz.websocket.closedCallbacks[i](evt);
433
}
434
} else {
435
_qz.websocket.closedCallbacks(evt);
436
}
437
}
438
},
439
440
441
printing: {
442
/** Default options used for new printer configs. Can be overridden using {@link qz.configs.setDefaults}. */
443
defaultConfig: {
444
//value purposes are explained in the qz.configs.setDefaults docs
445
446
bounds: null,
447
colorType: 'color',
448
copies: 1,
449
density: 0,
450
duplex: false,
451
fallbackDensity: null,
452
interpolation: 'bicubic',
453
jobName: null,
454
legacy: false,
455
margins: 0,
456
orientation: null,
457
paperThickness: null,
458
printerTray: null,
459
rasterize: false,
460
rotation: 0,
461
scaleContent: true,
462
size: null,
463
units: 'in',
464
465
forceRaw: false,
466
encoding: null,
467
spool: null
468
}
469
},
470
471
472
serial: {
473
/** List of functions called when receiving data from serial connection. */
474
serialCallbacks: [],
475
/** Calls all functions registered to listen for serial events. */
476
callSerial: function(streamEvent) {
477
if (Array.isArray(_qz.serial.serialCallbacks)) {
478
for(var i = 0; i < _qz.serial.serialCallbacks.length; i++) {
479
_qz.serial.serialCallbacks[i](streamEvent);
480
}
481
} else {
482
_qz.serial.serialCallbacks(streamEvent);
483
}
484
}
485
},
486
487
488
socket: {
489
/** List of functions called when receiving data from network socket connection. */
490
socketCallbacks: [],
491
/** Calls all functions registered to listen for network socket events. */
492
callSocket: function(socketEvent) {
493
if (Array.isArray(_qz.socket.socketCallbacks)) {
494
for(var i = 0; i < _qz.socket.socketCallbacks.length; i++) {
495
_qz.socket.socketCallbacks[i](socketEvent);
496
}
497
} else {
498
_qz.socket.socketCallbacks(socketEvent);
499
}
500
}
501
},
502
503
504
usb: {
505
/** List of functions called when receiving data from usb connection. */
506
usbCallbacks: [],
507
/** Calls all functions registered to listen for usb events. */
508
callUsb: function(streamEvent) {
509
if (Array.isArray(_qz.usb.usbCallbacks)) {
510
for(var i = 0; i < _qz.usb.usbCallbacks.length; i++) {
511
_qz.usb.usbCallbacks[i](streamEvent);
512
}
513
} else {
514
_qz.usb.usbCallbacks(streamEvent);
515
}
516
}
517
},
518
519
520
hid: {
521
/** List of functions called when receiving data from hid connection. */
522
hidCallbacks: [],
523
/** Calls all functions registered to listen for hid events. */
524
callHid: function(streamEvent) {
525
if (Array.isArray(_qz.hid.hidCallbacks)) {
526
for(var i = 0; i < _qz.hid.hidCallbacks.length; i++) {
527
_qz.hid.hidCallbacks[i](streamEvent);
528
}
529
} else {
530
_qz.hid.hidCallbacks(streamEvent);
531
}
532
}
533
},
534
535
536
printers: {
537
/** List of functions called when receiving data from printer connection. */
538
printerCallbacks: [],
539
/** Calls all functions registered to listen for printer events. */
540
callPrinter: function(streamEvent) {
541
if (Array.isArray(_qz.printers.printerCallbacks)) {
542
for(var i = 0; i < _qz.printers.printerCallbacks.length; i++) {
543
_qz.printers.printerCallbacks[i](streamEvent);
544
}
545
} else {
546
_qz.printers.printerCallbacks(streamEvent);
547
}
548
}
549
},
550
551
552
file: {
553
/** List of functions called when receiving info regarding file changes. */
554
fileCallbacks: [],
555
/** Calls all functions registered to listen for file events. */
556
callFile: function(streamEvent) {
557
if (Array.isArray(_qz.file.fileCallbacks)) {
558
for(var i = 0; i < _qz.file.fileCallbacks.length; i++) {
559
_qz.file.fileCallbacks[i](streamEvent);
560
}
561
} else {
562
_qz.file.fileCallbacks(streamEvent);
563
}
564
}
565
},
566
567
568
security: {
569
/** Function used to resolve promise when acquiring site's public certificate. */
570
certHandler: function(resolve, reject) { reject(); },
571
/** Called to create new promise (using {@link _qz.security.certHandler}) for certificate retrieval. */
572
callCert: function() {
573
if (typeof _qz.security.certHandler.then === 'function') {
574
//already a promise
575
return _qz.security.certHandler;
576
} else if (_qz.security.certHandler.constructor.name === "AsyncFunction") {
577
//already callable as a promise
578
return _qz.security.certHandler();
579
} else {
580
//turn into a promise
581
return _qz.tools.promise(_qz.security.certHandler);
582
}
583
},
584
585
/** Function used to create promise resolver when requiring signed calls. */
586
signatureFactory: function() { return function(resolve) { resolve(); } },
587
/** Called to create new promise (using {@link _qz.security.signatureFactory}) for signed calls. */
588
callSign: function(toSign) {
589
if (_qz.security.signatureFactory.constructor.name === "AsyncFunction") {
590
//use directly
591
return _qz.security.signatureFactory(toSign);
592
} else {
593
//use in a promise
594
return _qz.tools.promise(_qz.security.signatureFactory(toSign));
595
}
596
},
597
598
/** Signing algorithm used on signatures */
599
signAlgorithm: "SHA1",
600
601
rejectOnCertFailure: false,
602
603
needsSigned: function(callName) {
604
const undialoged = [
605
"printers.getStatus",
606
"printers.stopListening",
607
"usb.isClaimed",
608
"usb.closeStream",
609
"usb.releaseDevice",
610
"hid.stopListening",
611
"hid.isClaimed",
612
"hid.closeStream",
613
"hid.releaseDevice",
614
"file.stopListening",
615
"getVersion"
616
];
617
618
return callName != null && undialoged.indexOf(callName) === -1;
619
}
620
},
621
622
623
tools: {
624
/** Create a new promise */
625
promise: function(resolver) {
626
//prefer global object for historical purposes
627
if (typeof RSVP !== 'undefined') {
628
return new RSVP.Promise(resolver);
629
} else if (typeof Promise !== 'undefined') {
630
return new Promise(resolver);
631
} else {
632
_qz.log.error("Promise/A+ support is required. See qz.api.setPromiseType(...)");
633
}
634
},
635
636
/** Stub for rejecting with an Error from withing a Promise */
637
reject: function(error) {
638
return _qz.tools.promise(function(resolve, reject) {
639
reject(error);
640
});
641
},
642
643
stringify: function(object) {
644
//old versions of prototype affect stringify
645
var pjson = Array.prototype.toJSON;
646
delete Array.prototype.toJSON;
647
648
function skipKeys(key, value) {
649
if (key === "promise") {
650
return undefined;
651
}
652
653
return value;
654
}
655
656
var result = JSON.stringify(object, skipKeys);
657
658
if (pjson) {
659
Array.prototype.toJSON = pjson;
660
}
661
662
return result;
663
},
664
665
hash: function(data) {
666
//prefer global object for historical purposes
667
if (typeof Sha256 !== 'undefined') {
668
return Sha256.hash(data);
669
} else {
670
return _qz.SHA.hash(data);
671
}
672
},
673
674
ws: typeof WebSocket !== 'undefined' ? WebSocket : null,
675
676
absolute: function(loc) {
677
if (typeof window !== 'undefined' && typeof document.createElement === 'function') {
678
var a = document.createElement("a");
679
a.href = loc;
680
return a.href;
681
} else if (typeof exports === 'object') {
682
//node.js
683
require('path').resolve(loc);
684
}
685
return loc;
686
},
687
688
relative: function(data) {
689
for(var i = 0; i < data.length; i++) {
690
if (data[i].constructor === Object) {
691
var absolute = false;
692
693
if (data[i].data && data[i].data.search && data[i].data.search(/data:image\/\w+;base64,/) === 0) {
694
//upgrade from old base64 behavior
695
data[i].flavor = "base64";
696
data[i].data = data[i].data.replace(/^data:image\/\w+;base64,/, "");
697
} else if (data[i].flavor) {
698
//if flavor is known, we can directly check for absolute flavor types
699
if (["FILE", "XML"].indexOf(data[i].flavor.toUpperCase()) > -1) {
700
absolute = true;
701
}
702
} else if (data[i].format && ["HTML", "IMAGE", "PDF", "FILE", "XML"].indexOf(data[i].format.toUpperCase()) > -1) {
703
//if flavor is not known, all valid pixel formats default to file flavor
704
//previous v2.0 data also used format as what is now flavor, so we check for those values here too
705
absolute = true;
706
} else if (data[i].type && ((["PIXEL", "IMAGE", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && !data[i].format)
707
|| (["HTML", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && (!data[i].format || data[i].format.toUpperCase() === "FILE")))) {
708
//if all we know is pixel type, then it is image's file flavor
709
//previous v2.0 data also used type as what is now format, so we check for those value here too
710
absolute = true;
711
}
712
713
if (absolute) {
714
//change relative links to absolute
715
data[i].data = _qz.tools.absolute(data[i].data);
716
}
717
if (data[i].options && typeof data[i].options.overlay === 'string') {
718
data[i].options.overlay = _qz.tools.absolute(data[i].options.overlay);
719
}
720
}
721
}
722
},
723
724
/** Performs deep copy to target from remaining params */
725
extend: function(target) {
726
//special case when reassigning properties as objects in a deep copy
727
if (typeof target !== 'object') {
728
target = {};
729
}
730
731
for(var i = 1; i < arguments.length; i++) {
732
var source = arguments[i];
733
if (!source) { continue; }
734
735
for(var key in source) {
736
if (source.hasOwnProperty(key)) {
737
if (target === source[key]) { continue; }
738
739
if (source[key] && source[key].constructor && source[key].constructor === Object) {
740
var clone;
741
if (Array.isArray(source[key])) {
742
clone = target[key] || [];
743
} else {
744
clone = target[key] || {};
745
}
746
747
target[key] = _qz.tools.extend(clone, source[key]);
748
} else if (source[key] !== undefined) {
749
target[key] = source[key];
750
}
751
}
752
}
753
}
754
755
return target;
756
},
757
758
versionCompare: function(major, minor, patch, build) {
759
if (_qz.tools.assertActive()) {
760
var semver = _qz.websocket.connection.semver;
761
if (semver[0] != major) {
762
return semver[0] - major;
763
}
764
if (minor != undefined && semver[1] != minor) {
765
return semver[1] - minor;
766
}
767
if (patch != undefined && semver[2] != patch) {
768
return semver[2] - patch;
769
}
770
if (build != undefined && semver.length > 3 && semver[3] != build) {
771
return Number.isInteger(semver[3]) && Number.isInteger(build) ? semver[3] - build : semver[3].toString().localeCompare(build.toString());
772
}
773
return 0;
774
}
775
},
776
777
isVersion: function(major, minor, patch, build) {
778
return _qz.tools.versionCompare(major, minor, patch, build) == 0;
779
},
780
781
isActive: function() {
782
return !_qz.websocket.shutdown && _qz.websocket.connection != null
783
&& (_qz.websocket.connection.readyState === _qz.tools.ws.OPEN
784
|| _qz.websocket.connection.readyState === _qz.tools.ws.CONNECTING);
785
},
786
787
assertActive: function() {
788
if (_qz.tools.isActive()) {
789
return true;
790
}
791
// Promise won't reject on throw; yet better than 'undefined'
792
throw new Error("A connection to QZ has not been established yet");
793
},
794
795
uint8ArrayToHex: function(uint8) {
796
return Array.from(uint8)
797
.map(function(i) { return i.toString(16).padStart(2, '0'); })
798
.join('');
799
},
800
801
uint8ArrayToBase64: function(uint8) {
802
/**
803
* Adapted from Egor Nepomnyaschih's code under MIT Licence (C) 2020
804
* see https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
805
*/
806
var map = [
807
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U",
808
"V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p",
809
"q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
810
];
811
812
var result = '', i, l = uint8.length;
813
for (i = 2; i < l; i += 3) {
814
result += map[uint8[i - 2] >> 2];
815
result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
816
result += map[((uint8[i - 1] & 0x0F) << 2) | (uint8[i] >> 6)];
817
result += map[uint8[i] & 0x3F];
818
}
819
if (i === l + 1) { // 1 octet yet to write
820
result += map[uint8[i - 2] >> 2];
821
result += map[(uint8[i - 2] & 0x03) << 4];
822
result += "==";
823
}
824
if (i === l) { // 2 octets yet to write
825
result += map[uint8[i - 2] >> 2];
826
result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
827
result += map[(uint8[i - 1] & 0x0F) << 2];
828
result += "=";
829
}
830
return result;
831
},
832
},
833
834
compatible: {
835
/** Converts message format to a previous version's */
836
data: function(printData) {
837
// special handling for Uint8Array
838
for(var i = 0; i < printData.length; i++) {
839
if (printData[i].constructor === Object && printData[i].data instanceof Uint8Array) {
840
if (printData[i].flavor) {
841
var flavor = printData[i].flavor.toString().toUpperCase();
842
switch(flavor) {
843
case 'BASE64':
844
printData[i].data = _qz.tools.uint8ArrayToBase64(printData[i].data);
845
break;
846
case 'HEX':
847
printData[i].data = _qz.tools.uint8ArrayToHex(printData[i].data);
848
break;
849
default:
850
throw new Error("Uint8Array conversion to '" + flavor + "' is not supported.");
851
}
852
}
853
}
854
}
855
856
if(_qz.tools.versionCompare(2, 2, 4) < 0) {
857
for(var i = 0; i < printData.length; i++) {
858
if (printData[i].constructor === Object) {
859
// dotDensity: "double-legacy|single-legacy" since 2.2.4. Fallback to "double|single"
860
if (printData[i].options && typeof printData[i].options.dotDensity === 'string') {
861
printData[i].options.dotDensity = printData[i].options.dotDensity.toLowerCase().replace("-legacy", "");
862
}
863
}
864
}
865
}
866
867
if (_qz.tools.isVersion(2, 0)) {
868
/*
869
2.0.x conversion
870
-----
871
type=pixel -> use format as 2.0 type (unless 'command' format, which forces 2.0 'raw' type)
872
type=raw -> 2.0 type has to be 'raw'
873
if format is 'image' -> force 2.0 'image' format, ignore everything else (unsupported in 2.0)
874
875
flavor translates straight to 2.0 format (unless forced to 'raw'/'image')
876
*/
877
_qz.log.trace("Converting print data to v2.0 for " + _qz.websocket.connection.version);
878
for(var i = 0; i < printData.length; i++) {
879
if (printData[i].constructor === Object) {
880
if (printData[i].type && printData[i].type.toUpperCase() === "RAW" && printData[i].format && printData[i].format.toUpperCase() === "IMAGE") {
881
if (printData[i].flavor && printData[i].flavor.toUpperCase() === "BASE64") {
882
//special case for raw base64 images
883
printData[i].data = "data:image/compat;base64," + printData[i].data;
884
}
885
printData[i].flavor = "IMAGE"; //forces 'image' format when shifting for conversion
886
}
887
if ((printData[i].type && printData[i].type.toUpperCase() === "RAW") || (printData[i].format && printData[i].format.toUpperCase() === "COMMAND")) {
888
printData[i].format = "RAW"; //forces 'raw' type when shifting for conversion
889
}
890
891
printData[i].type = printData[i].format;
892
printData[i].format = printData[i].flavor;
893
delete printData[i].flavor;
894
}
895
}
896
}
897
},
898
899
/* Converts config defaults to match previous version */
900
config: function(config, dirty) {
901
if (_qz.tools.isVersion(2, 0)) {
902
if (!dirty.rasterize) {
903
config.rasterize = true;
904
}
905
}
906
if(_qz.tools.versionCompare(2, 2) < 0) {
907
if(config.forceRaw !== 'undefined') {
908
config.altPrinting = config.forceRaw;
909
delete config.forceRaw;
910
}
911
}
912
if(_qz.tools.versionCompare(2, 1, 2, 11) < 0) {
913
if(config.spool) {
914
if(config.spool.size) {
915
config.perSpool = config.spool.size;
916
delete config.spool.size;
917
}
918
if(config.spool.end) {
919
config.endOfDoc = config.spool.end;
920
delete config.spool.end;
921
}
922
delete config.spool;
923
}
924
}
925
return config;
926
},
927
928
/** Compat wrapper with previous version **/
929
networking: function(hostname, port, signature, signingTimestamp, mappingCallback) {
930
// Use 2.0
931
if (_qz.tools.isVersion(2, 0)) {
932
return _qz.tools.promise(function(resolve, reject) {
933
_qz.websocket.dataPromise('websocket.getNetworkInfo', {
934
hostname: hostname,
935
port: port
936
}, signature, signingTimestamp).then(function(data) {
937
if (typeof mappingCallback !== 'undefined') {
938
resolve(mappingCallback(data));
939
} else {
940
resolve(data);
941
}
942
}, reject);
943
});
944
}
945
// Wrap 2.1
946
return _qz.tools.promise(function(resolve, reject) {
947
_qz.websocket.dataPromise('networking.device', {
948
hostname: hostname,
949
port: port
950
}, signature, signingTimestamp).then(function(data) {
951
resolve({ ipAddress: data.ip, macAddress: data.mac });
952
}, reject);
953
});
954
},
955
956
/** Check if QZ version supports chosen algorithm */
957
algorithm: function(quiet) {
958
//if not connected yet we will assume compatibility exists for the time being
959
if (_qz.tools.isActive()) {
960
if (_qz.tools.isVersion(2, 0)) {
961
if (!quiet) {
962
_qz.log.warn("Connected to an older version of QZ, alternate signature algorithms are not supported");
963
}
964
return false;
965
}
966
}
967
968
return true;
969
}
970
},
971
972
/**
973
* Adapted from Chris Veness's code under MIT Licence (C) 2002
974
* see http://www.movable-type.co.uk/scripts/sha256.html
975
*/
976
SHA: {
977
//@formatter:off - keep this block compact
978
hash: function(msg) {
979
// add trailing '1' bit (+ 0's padding) to string [§5.1.1]
980
msg = _qz.SHA._utf8Encode(msg) + String.fromCharCode(0x80);
981
982
// constants [§4.2.2]
983
var K = [
984
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
985
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
986
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
987
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
988
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
989
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
990
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
991
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
992
];
993
// initial hash value [§5.3.1]
994
var H = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ];
995
996
// convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
997
var l = msg.length / 4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length
998
var N = Math.ceil(l / 16); // number of 16-integer-blocks required to hold 'l' ints
999
var M = new Array(N);
1000