f7083a7b67b68e321ce75f88fde9d688732cc71a
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / hatch.js
1 /**
2  * Core Service - egHatch
3  *
4  * Dispatches print and data storage requests to the appropriate handler.
5  *
6  * If Hatch is configured to honor the request -- current request types
7  * are 'settings', 'offline', and 'printing' -- the request will be
8  * relayed to the Hatch service.  Otherwise, the request is handled
9  * locally.
10  *
11  * Most handlers also provide direct remote and local variants to the
12  * application can decide to which to use as needed.
13  *
14  * Local storage requests are handled by $window.localStorage.
15  *
16  * Note that all top-level and remote requests return promises.  All
17  * local requests return immediate values, since local requests are
18  * never asynchronous.
19  *
20  * BEWARE: never store "fieldmapper" objects, since their structure
21  * may change over time as the IDL changes.  Always flatten objects
22  * into key/value pairs before calling set*Item()
23  *
24  */
25 angular.module('egCoreMod')
26
27 .factory('egHatch',
28            ['$q','$window','$timeout','$interpolate','$cookies',
29     function($q , $window , $timeout , $interpolate , $cookies) {
30
31     var service = {};
32     service.msgId = 1;
33     service.messages = {};
34     service.hatchAvailable = false;
35
36     // key/value cache -- avoid unnecessary Hatch extension requests.
37     // Only affects *RemoteItem calls.
38     service.keyCache = {}; 
39
40     /**
41      * List string prefixes for On-Call storage keys. On-Call keys
42      * are those that can be set/get/remove'd from localStorage when
43      * Hatch is not avaialable, even though Hatch is configured as the
44      * primary storage location for the key in question.  On-Call keys
45      * are those that allow the user to login and perform basic admin
46      * tasks (like disabling Hatch) even when Hatch is down.
47      * AKA Browser Staff Run Level 3.
48      * Note that no attempt is made to synchronize data between Hatch
49      * and localStorage for On-Call keys.  Only one destation is active 
50      * at a time and each maintains its own data separately.
51      */
52     service.onCallPrefixes = ['eg.workstation'];
53
54     // Returns true if the key can be set/get in localStorage even when 
55     // Hatch is not available.
56     service.keyIsOnCall = function(key) {
57         var oncall = false;
58         angular.forEach(service.onCallPrefixes, function(pfx) {
59             if (key.match(new RegExp('^' + pfx))) 
60                 oncall = true;
61         });
62         return oncall;
63     }
64
65     // write a message to the Hatch port
66     service.sendToHatch = function(msg) {
67         var msg2 = {};
68
69         // shallow copy and scrub msg before sending
70         angular.forEach(msg, function(val, key) {
71             if (key.match(/deferred/)) return;
72             msg2[key] = val;
73         });
74
75         console.debug("sending to Hatch: " + JSON.stringify(msg2));
76
77         msg2.from = 'page';
78         $window.postMessage(msg2, $window.location.origin);
79     }
80
81     // Send request to Hatch or reject if Hatch is unavailable
82     service.attemptHatchDelivery = function(msg) {
83         msg.msgid = service.msgId++;
84         msg.deferred = $q.defer();
85
86         if (service.hatchAvailable) {
87             service.messages[msg.msgid] = msg;
88             service.sendToHatch(msg);
89
90         } else {
91             console.error(
92                 'Hatch request attempted but Hatch is not available');
93             msg.deferred.reject(msg);
94         }
95
96         return msg.deferred.promise;
97     }
98
99
100     // resolve the promise on the given request and remove
101     // it from our tracked requests.
102     service.resolveRequest = function(msg) {
103
104         if (!service.messages[msg.msgid]) {
105             console.error('no cached message for id = ' + msg.msgid);
106             return;
107         }
108
109         // for requests sent through Hatch, only the cached 
110         // request will have the original promise attached
111         msg.deferred = service.messages[msg.msgid].deferred;
112         delete service.messages[msg.msgid]; // un-cache
113
114         if (msg.status == 200) {
115             msg.deferred.resolve(msg.content);
116         } else {
117             console.warn("Hatch command failed with status=" 
118                 + msg.status + " and message=" + msg.message);
119             msg.deferred.reject();
120         }
121     }
122
123     service.openHatch = function() {
124
125         // When the Hatch extension loads, it tacks an attribute onto
126         // the top-level documentElement to indicate it's available.
127         if (!$window.document.documentElement.getAttribute('hatch-is-open')) {
128             //console.debug("Hatch is not available");
129             return;
130         }
131
132         $window.addEventListener("message", function(event) {
133             // We only accept messages from our own content script.
134             if (event.source != window) return;
135
136             // We only care about messages from the Hatch extension.
137             if (event.data && event.data.from == 'extension') {
138
139                 // Avoid logging full Hatch responses. they can get large.
140                 console.debug(
141                     'Hatch responded to message ID ' + event.data.msgid);
142
143                 service.resolveRequest(event.data);
144             }
145         }); 
146
147         service.hatchAvailable = true; // public flag
148     }
149
150     service.remotePrint = function(
151         context, contentType, content, withDialog) {
152
153         return service.getPrintConfig(context).then(
154             function(config) {
155                 // print configuration retrieved; print
156                 return service.attemptHatchDelivery({
157                     action : 'print',
158                     settings : config,
159                     content : content, 
160                     contentType : contentType,
161                     showDialog : withDialog,
162                 });
163             }
164         );
165     }
166
167     service.getPrintConfig = function(context) {
168         return service.getRemoteItem('eg.print.config.' + context);
169     }
170
171     service.setPrintConfig = function(context, config) {
172         return service.setRemoteItem('eg.print.config.' + context, config);
173     }
174
175     service.getPrinterOptions = function(name) {
176         return service.attemptHatchDelivery({
177             action : 'printer-options',
178             printer : name
179         });
180     }
181
182     service.getPrinters = function() {
183         if (service.printers) // cached printers
184             return $q.when(service.printers);
185
186         return service.attemptHatchDelivery({action : 'printers'}).then(
187
188             // we have remote printers; sort by name and return
189             function(printers) {
190                 service.printers = printers.sort(
191                     function(a,b) {return a.name < b.name ? -1 : 1});
192                 return service.printers;
193             },
194
195             // remote call failed and there is no such thing as local
196             // printers; return empty set.
197             function() { return [] } 
198         );
199     }
200
201     service.usePrinting = function() {
202         return service.getLocalItem('eg.hatch.enable.printing');
203     }
204
205     service.useSettings = function() {
206         return service.getLocalItem('eg.hatch.enable.settings');
207     }
208
209     service.useOffline = function() {
210         return service.getLocalItem('eg.hatch.enable.offline');
211     }
212
213     // get the value for a stored item
214     service.getItem = function(key) {
215
216         if (!service.useSettings())
217             return $q.when(service.getLocalItem(key));
218
219         if (service.hatchAvailable) 
220             return service.getRemoteItem(key);
221
222         if (service.keyIsOnCall(key)) {
223             console.warn("Unable to getItem from Hatch: " + key + 
224                 ". Retrieving item from local storage instead");
225
226             return $q.when(service.getLocalItem(key));
227         }
228
229         console.error("Unable to getItem from Hatch: " + key);
230         return $q.reject();
231     }
232
233     service.getRemoteItem = function(key) {
234         
235         if (service.keyCache[key] != undefined)
236             return $q.when(service.keyCache[key])
237
238         return service.attemptHatchDelivery({
239             key : key,
240             action : 'get'
241         }).then(function(content) {
242             return service.keyCache[key] = content;
243         });
244     }
245
246     service.getLocalItem = function(key) {
247         var val = $window.localStorage.getItem(key);
248         if (val == null) return;
249         try {
250             return JSON.parse(val);
251         } catch(E) {
252             console.error(
253                 "Deleting invalid JSON for localItem: " + key + " => " + val);
254             service.removeLocalItem(key);
255             return null;
256         }
257     }
258
259     service.getLoginSessionItem = function(key) {
260         var val = $cookies.get(key);
261         if (val == null) return;
262         return JSON.parse(val);
263     }
264
265     service.getSessionItem = function(key) {
266         var val = $window.sessionStorage.getItem(key);
267         if (val == null) return;
268         return JSON.parse(val);
269     }
270
271     /**
272      * @param tmp bool Store the value as a session cookie only.  
273      * tmp values are removed during logout or browser close.
274      */
275     service.setItem = function(key, value) {
276         if (!service.useSettings())
277             return $q.when(service.setLocalItem(key, value));
278
279         if (service.hatchAvailable)
280             return service.setRemoteItem(key, value);
281
282         if (service.keyIsOnCall(key)) {
283             console.warn("Unable to setItem in Hatch: " + 
284                 key + ". Setting in local storage instead");
285
286             return $q.when(service.setLocalItem(key, value));
287         }
288
289         console.error("Unable to setItem in Hatch: " + key);
290         return $q.reject();
291     }
292
293     // set the value for a stored or new item
294     service.setRemoteItem = function(key, value) {
295         service.keyCache[key] = value;
296         return service.attemptHatchDelivery({
297             key : key, 
298             content : value, 
299             action : 'set',
300         });
301     }
302
303     // Set the value for the given key.
304     // "Local" items persist indefinitely.
305     // If the value is raw, pass it as 'value'.  If it was
306     // externally JSONified, pass it via jsonified.
307     service.setLocalItem = function(key, value, jsonified) {
308         if (jsonified === undefined ) 
309             jsonified = JSON.stringify(value);
310         $window.localStorage.setItem(key, jsonified);
311     }
312
313     service.appendItem = function(key, value) {
314         if (!service.useSettings())
315             return $q.when(service.appendLocalItem(key, value));
316
317         if (service.hatchAvailable)
318             return service.appendRemoteItem(key, value);
319
320         if (service.keyIsOnCall(key)) {
321             console.warn("Unable to appendItem in Hatch: " + 
322                 key + ". Setting in local storage instead");
323
324             return $q.when(service.appendLocalItem(key, value));
325         }
326
327         console.error("Unable to appendItem in Hatch: " + key);
328         return $q.reject();
329     }
330
331     // append the value to a stored or new item
332     service.appendRemoteItem = function(key, value) {
333         service.keyCache[key] = value;
334         return service.attemptHatchDelivery({
335             key : key, 
336             content : value, 
337             action : 'append',
338         });
339     }
340
341     service.appendLocalItem = function(key, value, jsonified) {
342         if (jsonified === undefined ) 
343             jsonified = JSON.stringify(value);
344
345         var old_value = $window.localStorage.getItem(key) || '';
346         $window.localStorage.setItem( key, old_value + jsonified );
347     }
348
349     // Set the value for the given key.  
350     // "LoginSession" items are removed when the user logs out or the 
351     // browser is closed.
352     // If the value is raw, pass it as 'value'.  If it was
353     // externally JSONified, pass it via jsonified.
354     service.setLoginSessionItem = function(key, value, jsonified) {
355         service.addLoginSessionKey(key);
356         if (jsonified === undefined ) 
357             jsonified = JSON.stringify(value);
358         $cookies.put(key, jsonified);
359     }
360
361     // Set the value for the given key.  
362     // "Session" items are browser tab-specific and are removed when the
363     // tab is closed.
364     // If the value is raw, pass it as 'value'.  If it was
365     // externally JSONified, pass it via jsonified.
366     service.setSessionItem = function(key, value, jsonified) {
367         if (jsonified === undefined ) 
368             jsonified = JSON.stringify(value);
369         $window.sessionStorage.setItem(key, jsonified);
370     }
371
372     // remove a stored item
373     service.removeItem = function(key) {
374         if (!service.useSettings())
375             return $q.when(service.removeLocalItem(key));
376
377         if (service.hatchAvailable) 
378             return service.removeRemoteItem(key);
379
380         if (service.keyIsOnCall(key)) {
381             console.warn("Unable to removeItem from Hatch: " + key + 
382                 ". Removing item from local storage instead");
383
384             return $q.when(service.removeLocalItem(key));
385         }
386
387         console.error("Unable to removeItem from Hatch: " + key);
388         return $q.reject();
389     }
390
391     service.removeRemoteItem = function(key) {
392         delete service.keyCache[key];
393         return service.attemptHatchDelivery({
394             key : key,
395             action : 'remove'
396         });
397     }
398
399     service.removeLocalItem = function(key) {
400         $window.localStorage.removeItem(key);
401     }
402
403     service.removeLoginSessionItem = function(key) {
404         service.removeLoginSessionKey(key);
405         $cookies.remove(key);
406     }
407
408     service.removeSessionItem = function(key) {
409         $window.sessionStorage.removeItem(key);
410     }
411
412     /**
413      * Remove all "LoginSession" items.
414      */
415     service.clearLoginSessionItems = function() {
416         angular.forEach(service.getLoginSessionKeys(), function(key) {
417             service.removeLoginSessionItem(key);
418         });
419
420         // remove the keys cache.
421         service.removeLocalItem('eg.hatch.login_keys');
422     }
423
424     // if set, prefix limits the return set to keys starting with 'prefix'
425     service.getKeys = function(prefix) {
426         if (service.useSettings()) 
427             return service.getRemoteKeys(prefix);
428         return $q.when(service.getLocalKeys(prefix));
429     }
430
431     service.getRemoteKeys = function(prefix) {
432         return service.attemptHatchDelivery({
433             key : prefix,
434             action : 'keys'
435         });
436     }
437
438     service.getLocalKeys = function(prefix) {
439         var keys = [];
440         var idx = 0;
441         while ( (k = $window.localStorage.key(idx++)) !== null) {
442             // key prefix match test
443             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
444             keys.push(k);
445         }
446         return keys;
447     }
448
449
450     /**
451      * Array of "LoginSession" keys.
452      * Note we have to store these as "Local" items so browser tabs can
453      * share them.  We could store them as cookies, but it's more data
454      * that has to go back/forth to the server.  A "LoginSession" key name is
455      * not private, though, so it's OK if they are left in localStorage
456      * until the next login.
457      */
458     service.getLoginSessionKeys = function(prefix) {
459         var keys = [];
460         var idx = 0;
461         var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
462         angular.forEach(login_keys, function(k) {
463             // key prefix match test
464             if (prefix && k.substr(0, prefix.length) != prefix) return;
465             keys.push(k);
466         });
467         return keys;
468     }
469
470     service.addLoginSessionKey = function(key) {
471         var keys = service.getLoginSessionKeys();
472         if (keys.indexOf(key) < 0) {
473             keys.push(key);
474             service.setLocalItem('eg.hatch.login_keys', keys);
475         }
476     }
477
478     service.removeLoginSessionKey = function(key) {
479         var keys = service.getLoginSessionKeys().filter(function(k) {
480             return k != key;
481         });
482         service.setLocalItem('eg.hatch.login_keys', keys);
483     }
484
485     // Copy all stored settings from localStorage to Hatch.
486     // If 'move' is true, delete the local settings once cloned.
487     service.copySettingsToHatch = function(move) {
488         var deferred = $q.defer();
489         var keys = service.getLocalKeys();
490
491         angular.forEach(keys, function(key) {
492
493             // Hatch keys are local-only
494             if (key.match(/^eg.hatch/)) return;
495
496             console.debug("Copying to Hatch Storage: " + key);
497             service.setRemoteItem(key, service.getLocalItem(key))
498             .then(function() { // key successfully cloned.
499
500                 // delete the local copy if requested.
501                 if (move) service.removeLocalItem(key);
502
503                 // resolve the promise after processing the last key.
504                 if (key == keys[keys.length-1]) 
505                     deferred.resolve();
506             });
507         });
508
509         return deferred.promise;
510     }
511
512     // Copy all stored settings from Hatch to localStorage.
513     // If 'move' is true, delete the Hatch settings once cloned.
514     service.copySettingsToLocal = function(move) {
515         var deferred = $q.defer();
516
517         service.getRemoteKeys().then(function(keys) {
518             angular.forEach(keys, function(key) {
519                 service.getRemoteItem(key).then(function(val) {
520
521                     console.debug("Copying to Local Storage: " + key);
522                     service.setLocalItem(key, val);
523
524                     // delete the remote copy if requested.
525                     if (move) service.removeRemoteItem(key);
526
527                     // resolve the promise after processing the last key.
528                     if (key == keys[keys.length-1]) 
529                         deferred.resolve();
530                 });
531             });
532         });
533
534         return deferred.promise;
535     }
536
537     // The only requirement for opening Hatch is that the DOM be loaded.
538     // Open the connection now so its state will be immediately available.
539     service.openHatch();
540
541     return service;
542 }])
543