]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/hatch.js
1e907c5c13e187034235782689fa6d3b9f3ef4a4
[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','egNet','$injector',
29     function($q , $window , $timeout , $interpolate , $cookies , egNet , $injector ) {
30
31     var service = {};
32     service.msgId = 1;
33     service.messages = {};
34     service.hatchAvailable = false;
35     service.auth = null;  // ref to egAuth loaded on-demand to avoid circular ref.
36     service.disableServerSettings = false;
37
38     // key/value cache -- avoid unnecessary Hatch extension requests.
39     // Only affects *RemoteItem calls.
40     service.keyCache = {}; 
41
42     // Keep a local copy of all retrieved setting summaries, which indicate
43     // which setting types exist for each setting.  
44     service.serverSettingSummaries = {};
45
46     /**
47      * Settings with these prefixes will always live in the browser.
48      */
49     service.browserOnlyPrefixes = [
50         'eg.hatch.enable.settings', // deprecated
51         'eg.hatch.enable.offline', // deprecated
52         'eg.cache',
53         'current_tag_table_marc21_biblio',
54         'FFPos',
55         'FFValue'
56     ];
57
58     service.keyStoredInBrowser = function(key) {
59
60         if (service.disableServerSettings) {
61             // When server-side storage is disabled, treat every
62             // setting like it's stored locally.
63             return true;
64         }
65
66         var browserOnly = false;
67         service.browserOnlyPrefixes.forEach(function(pfx) {
68             if (key.match(new RegExp('^' + pfx))) 
69                 browserOnly = true;
70         });
71
72         return browserOnly;
73     }
74
75     // write a message to the Hatch port
76     service.sendToHatch = function(msg) {
77         var msg2 = {};
78
79         // shallow copy and scrub msg before sending
80         angular.forEach(msg, function(val, key) {
81             if (key.match(/deferred/)) return;
82             msg2[key] = val;
83         });
84
85         console.debug("sending to Hatch: " + JSON.stringify(msg2));
86
87         msg2.from = 'page';
88         $window.postMessage(msg2, $window.location.origin);
89     }
90
91     // Send request to Hatch or reject if Hatch is unavailable
92     service.attemptHatchDelivery = function(msg) {
93         msg.msgid = service.msgId++;
94         msg.deferred = $q.defer();
95
96         if (service.hatchAvailable) {
97             service.messages[msg.msgid] = msg;
98             service.sendToHatch(msg);
99
100         } else {
101             console.error(
102                 'Hatch request attempted but Hatch is not available');
103             msg.deferred.reject(msg);
104         }
105
106         return msg.deferred.promise;
107     }
108
109
110     // resolve the promise on the given request and remove
111     // it from our tracked requests.
112     service.resolveRequest = function(msg) {
113
114         if (!service.messages[msg.msgid]) {
115             console.error('no cached message for id = ' + msg.msgid);
116             return;
117         }
118
119         // for requests sent through Hatch, only the cached 
120         // request will have the original promise attached
121         msg.deferred = service.messages[msg.msgid].deferred;
122         delete service.messages[msg.msgid]; // un-cache
123
124         if (msg.status == 200) {
125             msg.deferred.resolve(msg.content);
126         } else {
127             console.warn("Hatch command failed with status=" 
128                 + msg.status + " and message=" + msg.message);
129             msg.deferred.reject();
130         }
131     }
132
133     service.openHatch = function() {
134
135         // When the Hatch extension loads, it tacks an attribute onto
136         // the top-level documentElement to indicate it's available.
137         if (!$window.document.documentElement.getAttribute('hatch-is-open')) {
138             //console.debug("Hatch is not available");
139             return;
140         }
141
142         $window.addEventListener("message", function(event) {
143             // We only accept messages from our own content script.
144             if (event.source != window) return;
145
146             // We only care about messages from the Hatch extension.
147             if (event.data && event.data.from == 'extension') {
148
149                 // Avoid logging full Hatch responses. they can get large.
150                 console.debug(
151                     'Hatch responded to message ID ' + event.data.msgid);
152
153                 service.resolveRequest(event.data);
154             }
155         }); 
156
157         service.hatchAvailable = true; // public flag
158     }
159
160     service.remotePrint = function(
161         context, contentType, content, withDialog) {
162
163         return service.getPrintConfig(context).then(
164             function(config) {
165                 if (config.printer == 'hatch_file_writer') {
166                     if (contentType == 'text/html') {
167                         content = service.html2txt(content);
168                     }
169                     return service.setRemoteItem(
170                         'receipt.' + context + '.txt', content, true);
171                 } 
172                 // print configuration retrieved; print
173                 return service.attemptHatchDelivery({
174                     action : 'print',
175                     settings : config,
176                     content : content, 
177                     contentType : contentType,
178                     showDialog : withDialog,
179                 });
180             }
181         );
182     }
183
184     service.getPrintConfig = function(context) {
185         return service.getItem('eg.print.config.' + context);
186     }
187
188     service.setPrintConfig = function(context, config) {
189         return service.setItem('eg.print.config.' + context, config);
190     }
191
192     service.getPrinterOptions = function(name) {
193         return service.attemptHatchDelivery({
194             action : 'printer-options',
195             printer : name
196         });
197     }
198
199     service.getPrinters = function() {
200         if (service.printers) // cached printers
201             return $q.when(service.printers);
202
203         return service.attemptHatchDelivery({action : 'printers'}).then(
204
205             // we have remote printers; sort by name and return
206             function(printers) {
207                 service.printers = printers.sort(
208                     function(a,b) {return a.name < b.name ? -1 : 1});
209                 return service.printers;
210             },
211
212             // remote call failed and there is no such thing as local
213             // printers; return empty set.
214             function() { return [] } 
215         );
216     }
217
218     service.usePrinting = function() {
219         return service.getItem('eg.hatch.enable.printing');
220     }
221
222     // DEPRECATED
223     service.useSettings = function() {
224         return service.getLocalItem('eg.hatch.enable.settings');
225     }
226
227     // DEPRECATED
228     service.useOffline = function() {
229         return service.getLocalItem('eg.hatch.enable.offline');
230     }
231
232     service.getWorkstations = function() {
233         if (service.hatchAvailable) {
234             return service.mergeWorkstations().then(
235                 function() {
236                     service.removeLocalItem('eg.workstation.all');
237                     return service.getRemoteItem('eg.workstation.all');
238                 }
239             );
240         } else {
241             return $q.when(service.getLocalItem('eg.workstation.all'));
242         }
243     }
244
245     // See if any workstations are stored in local storage.  If so, also
246     // see if we have any stored in Hatch.  If both, merged workstations
247     // from localStorage in Hatch storage, skipping any whose name
248     // collide with a workstation in Hatch.  If none exist in Hatch,
249     // copy the localStorage workstations over wholesale.
250     service.mergeWorkstations = function() {
251         var existing = service.getLocalItem('eg.workstation.all');
252
253         if (!existing || existing.length === 0) {
254             return $q.when();
255         }
256
257         return service.getRemoteItem('eg.workstation.all')
258         .then(function(inHatch) {
259
260             if (!inHatch || inHatch.length === 0) {
261                 // Nothing to merge, copy the data over directly
262                 console.debug('No workstations in hatch to merge');
263                 return service.setRemoteItem('eg.workstation.all', existing);
264             }
265
266             var addMe = [];
267             existing.forEach(function(ws) {
268                 var match = inHatch.filter(
269                     function(w) {return w.name === ws.name})[0];
270                 if (!match) {
271                     console.log(
272                         'Migrating workstation from local storage to hatch: ' 
273                         + ws.name
274                     );
275                     addMe.push(ws);
276                 }
277             });
278             inHatch = inHatch.concat(addMe);
279             return service.setRemoteItem('eg.workstation.all', inHatch);
280         });
281     }
282
283     service.getDefaultWorkstation = function() {
284
285         if (service.hatchAvailable) {
286             return service.getRemoteItem('eg.workstation.default')
287             .then(function(name) {
288                 if (name) {
289                     // We have a default in Hatch, remove any lingering
290                     // value from localStorage.
291                     service.removeLocalItem('eg.workstation.default');
292                     return name;
293                 }
294
295                 name = service.getLocalItem('eg.workstation.default');
296                 if (name) {
297                     console.log('Migrating default workstation to Hatch ' + name);
298                     return service.setRemoteItem('eg.workstation.default', name)
299                     .then(function() {return name;});
300                 }
301
302                 return null;
303             });
304         } else {
305             return $q.when(service.getLocalItem('eg.workstation.default'));
306         }
307     }
308
309     service.setWorkstations = function(workstations, isJson) {
310         if (service.hatchAvailable) {
311             return service.setRemoteItem('eg.workstation.all', workstations);
312         } else {
313             return $q.when(
314                 service.setLocalItem('eg.workstation.all', workstations, isJson));
315         }
316     }
317
318     service.setDefaultWorkstation = function(name, isJson) {
319         if (service.hatchAvailable) {
320             return service.setRemoteItem('eg.workstation.default', name);
321         } else {
322             return $q.when(
323                 service.setLocalItem('eg.workstation.default', name, isJson));
324         }
325     }
326
327     service.removeWorkstations = function() {
328         if (service.hatchAvailable) {
329             return service.removeRemoteItem('eg.workstation.all');
330         } else {
331             return $q.when(
332                 service.removeLocalItem('eg.workstation.all'));
333         }
334     }
335
336     service.removeDefaultWorkstation = function() {
337         if (service.hatchAvailable) {
338             return service.removeRemoteItem('eg.workstation.default');
339         } else {
340             return $q.when(
341                 service.removeLocalItem('eg.workstation.default'));
342         }
343     }
344
345
346     // Workstation actions always use Hatch when it's available
347     service.getWorkstationItem = function(key) {
348         if (service.hatchAvailable) {
349             return service.getRemoteItem(key);
350         } else {
351             return $q.when(service.getLocalItem(key));
352         }
353     }
354
355     service.setWorkstationItem = function(key, value) {
356         if (service.hatchAvailable) {
357             return service.setRemoteItem(key, value);
358         } else {
359             return $q.when(service.setLocalItem(key, value));
360         }
361     }
362
363     service.removeWorkstationItem = function(key) {
364         if (service.hatchAvailable) {
365             return service.removeRemoteItem(key);
366         } else {
367             return $q.when(service.removeLocalItem(key));
368         }
369     }
370
371     service.keyIsWorkstation = function(key) {
372         return Boolean(key.match(/eg.workstation/));
373     }
374
375     // get the value for a stored item
376     service.getItem = function(key) {
377
378         if (service.keyIsWorkstation(key)) {
379             return service.getWorkstationItem(key);
380         }
381
382         if (!service.keyStoredInBrowser(key)) {
383             return service.getServerItem(key);
384         }
385
386         var deferred = $q.defer();
387
388         service.getBrowserItem(key).then(
389             function(val) { deferred.resolve(val); },
390             function() { // Hatch error
391                 deferred.reject("Unable to getItem from Hatch: " + key);
392             }
393         );
394
395         return deferred.promise;
396     }
397
398     // Collect values in batch.
399     // For server-stored values espeically, this is more efficient 
400     // than a series of one-off calls.
401     service.getItemBatch = function(keys) {
402         var browserKeys = [];
403         var serverKeys = [];
404
405         // To take full advantage of the getServerItemBatch call,
406         // we have to know in advance which keys to send to the server
407         // vs those to handle in the browser.
408         keys.forEach(function(key) {
409             if (service.keyStoredInBrowser(key)) {
410                 browserKeys.push(key);
411             } else {
412                 serverKeys.push(key);
413             }
414         });
415
416         var settings = {};
417
418         var serverPromise = serverKeys.length === 0 ? $q.when() : 
419             service.getServerItemBatch(serverKeys).then(function(values) {
420                 angular.forEach(values, function(val, key) {
421                     settings[key] = val;
422                 });
423             });
424
425         var browserPromises = [];
426         browserKeys.forEach(function(key) {
427             browserPromises.push(
428                 service.getBrowserItem(key).then(function(val) {
429                     settings[key] = val;
430                 })
431             );
432         });
433
434         return $q.all(browserPromises.concat(serverPromise))
435             .then(function() {return settings});
436     }
437
438     service.getBrowserItem = function(key) {
439         if (service.useSettings()) {
440             if (service.hatchAvailable) {
441                 return service.getRemoteItem(key);
442             }
443         } else {
444             return $q.when(service.getLocalItem(key));
445         }
446         return $q.reject();
447     }
448
449     service.getRemoteItem = function(key) {
450         
451         if (service.keyCache[key] != undefined)
452             return $q.when(service.keyCache[key])
453
454         return service.attemptHatchDelivery({
455             key : key,
456             action : 'get'
457         }).then(function(content) {
458             return service.keyCache[key] = content;
459         });
460     }
461
462     service.getLocalItem = function(key) {
463         var val = $window.localStorage.getItem(key);
464         if (val === null || val === undefined) return;
465         try {
466             return JSON.parse(val);
467         } catch(E) {
468             console.error(
469                 "Deleting invalid JSON for localItem: " + key + " => " + val);
470             service.removeLocalItem(key);
471             return null;
472         }
473     }
474
475     // Force auth cookies to live under path "/" instead of "/eg/staff"
476     // so they may be shared with the Angular app.
477     // There's no way to tell under what path a cookie is stored in
478     // the browser, all we can do is migrate it regardless.
479     service.migrateAuthCookies = function() {
480         [   'eg.auth.token', 
481             'eg.auth.time', 
482             'eg.auth.token.oc', 
483             'eg.auth.time.oc'
484         ].forEach(function(key) {
485             var val = service.getLoginSessionItem(key);
486             if (val) {
487                 $cookies.remove(key, {path: '/eg/staff/'});
488                 service.setLoginSessionItem(key, val);
489             }
490         });
491     }
492
493     service.getLoginSessionItem = function(key) {
494         var val = $cookies.get(key);
495         if (val == null) return;
496         return JSON.parse(val);
497     }
498
499     service.getSessionItem = function(key) {
500         var val = $window.sessionStorage.getItem(key);
501         if (val == null) return;
502         return JSON.parse(val);
503     }
504
505     /**
506      * @param tmp bool Store the value as a session cookie only.  
507      * tmp values are removed during logout or browser close.
508      */
509     service.setItem = function(key, value) {
510
511         if (service.keyIsWorkstation(key)) {
512             return service.setWorkstationItem(key, value);
513         }
514
515         if (!service.keyStoredInBrowser(key)) {
516             return service.setServerItem(key, value);
517         }
518
519         var deferred = $q.defer();
520         return service.setBrowserItem(key, value).then(
521             function(val) {deferred.resolve(val);},
522
523             function() { // Hatch error
524                 deferred.reject("Unable to setItem in Hatch: " + key);
525             }
526         );
527     }
528
529     service.setBrowserItem = function(key, value) {
530         if (service.useSettings()) {
531             if (service.hatchAvailable) {
532                 return service.setRemoteItem(key, value);
533             } else {
534                 return $q.reject('Unable to get item from hatch');
535             }
536         } else {
537             return $q.when(service.setLocalItem(key, value));
538         }
539     }
540
541     service.setServerItem = function(key, value) {
542         if (!service.auth) service.auth = $injector.get('egAuth');
543         if (!service.auth.token()) return $q.when();
544
545         // If we have already attempted to retrieve a value for this
546         // setting, then we can tell up front whether applying a value
547         // at the server will be an option.  If not, store locally.
548         var summary = service.serverSettingSummaries[key];
549         if (summary && !summary.has_staff_setting) {
550
551             if (summary.has_org_setting === 't') {
552                 // When no user/ws setting types exist but an org unit
553                 // setting type does, it means the value cannot be
554                 // applied by an individual user.  Nothing left to do.
555                 return $q.when();
556             }
557
558             // No setting types of any flavor exist.
559             // Fall back to local storage.
560
561             if (value === null) {
562                 // a null value means clear the server setting.
563                 return service.removeBrowserItem(key);
564             } else {
565                 console.warn('No server setting type exists for ' + key);
566                 return service.setBrowserItem(key, value); 
567             }
568         }
569
570         var settings = {};
571         settings[key] = value;
572
573         return egNet.request(
574             'open-ils.actor',
575             'open-ils.actor.settings.apply.user_or_ws',
576             service.auth.token(), settings
577         ).then(function(appliedCount) {
578
579             if (appliedCount == 0) {
580                 console.warn('No server setting type exists for ' + key);
581                 // We were unable to store the setting on the server,
582                 // presumably becuase no server-side setting type exists.
583                 // Add to local storage instead.
584                 service.setLocalItem(key, value);
585             }
586
587             service.keyCache[key] = value;
588             return appliedCount;
589         });
590     }
591
592     service.getServerItem = function(key) {
593         if (key in service.keyCache) {
594             return $q.when(service.keyCache[key])
595         }
596
597         if (!service.auth) service.auth = $injector.get('egAuth');
598         if (!service.auth.token()) return $q.when(null);
599
600         return egNet.request(
601             'open-ils.actor',
602             'open-ils.actor.settings.retrieve.atomic',
603             [key], service.auth.token()
604         ).then(function(settings) {
605             return service.handleServerItemResponse(settings[0]);
606         });
607     }
608
609     service.handleServerItemResponse = function(summary) {
610         var key = summary.name;
611         var val = summary.value;
612
613         // For our purposes, we only care if a setting can be stored
614         // as an org setting or a user-or-workstation setting.
615         summary.has_staff_setting = (
616             summary.has_user_setting === 't' || 
617             summary.has_workstation_setting === 't'
618         );
619
620         summary.value = null; // avoid duplicate value caches
621         service.serverSettingSummaries[key] = summary;
622
623         if (val !== null) {
624             // We have a server setting.  Nothing left to do.
625             return $q.when(service.keyCache[key] = val);
626         }
627
628         if (!summary.has_staff_setting) {
629
630             if (summary.has_org_setting === 't') {
631                 // An org unit setting type exists but no value is applied
632                 // that this workstation has access to.  The existence of 
633                 // an org unit setting type and no user/ws setting type 
634                 // means applying a value locally is not allowed.  
635                 return $q.when(service.keyCache[key] = undefined);
636             }
637
638             console.warn('No server setting type exists for ' 
639                 + key + ', using local value.');
640
641             return service.getBrowserItem(key);
642         }
643
644         // A user/ws setting type exists, but no server value exists.
645         // Migrate the local setting to the server.
646
647         var deferred = $q.defer();
648         service.getBrowserItem(key).then(function(browserVal) {
649
650             if (browserVal === null || browserVal === undefined) {
651                 // No local value to migrate.
652                 return deferred.resolve(service.keyCache[key] = undefined);
653             }
654
655             // Migrate the local value to the server.
656
657             service.setServerItem(key, browserVal).then(
658                 function(appliedCount) {
659                     if (appliedCount == 1) {
660                         console.info('setting ' + key + ' successfully ' +
661                             'migrated to a server setting');
662                         service.removeBrowserItem(key); // fire & forget
663                     } else {
664                         console.error('error migrating setting to server,' 
665                             + ' falling back to local value');
666                     }
667                     deferred.resolve(service.keyCache[key] = browserVal);
668                 }
669             );
670         });
671
672         return deferred.promise;
673     }
674
675     service.getServerItemBatch = function(keys) {
676         // no cache checking for now.  assumes batch mode is only
677         // called once on page load.  maybe add cache checking later.
678         if (!service.auth) service.auth = $injector.get('egAuth');
679         if (!service.auth.token()) return $q.when({});
680
681         var foundValues = {};
682         return egNet.request(
683             'open-ils.actor',
684             'open-ils.actor.settings.retrieve.atomic',
685             keys, service.auth.token()
686         ).then(
687             function(settings) { 
688                 //return foundValues; 
689
690                 var deferred = $q.defer();
691                 function checkOne(setting) {
692                     if (!setting) {
693                         deferred.resolve(foundValues);
694                         return;
695                     }
696                     service.handleServerItemResponse(setting)
697                     .then(function(resp) {
698                         if (resp !== undefined) {
699                             foundValues[setting.name] = resp;
700                         }
701                         settings.shift();
702                         checkOne(settings[0]);
703                     });
704                 }
705
706                 checkOne(settings[0]);
707                 return deferred.promise;
708             }
709         );
710     }
711
712
713     // set the value for a stored or new item
714     // When "bare" is true, the value will not be JSON-encoded
715     // on the file system.
716     service.setRemoteItem = function(key, value, bare) {
717         service.keyCache[key] = value;
718         return service.attemptHatchDelivery({
719             key : key, 
720             content : value, 
721             action : 'set',
722             bare: bare
723         });
724     }
725
726     // Set the value for the given key.
727     // "Local" items persist indefinitely.
728     // If the value is raw, pass it as 'value'.  If it was
729     // externally JSONified, pass it via jsonified.
730     service.setLocalItem = function(key, value, jsonified) {
731         if (jsonified === undefined ) {
732             jsonified = JSON.stringify(value);
733         } else if (value === undefined) {
734             return;
735         }
736         try {
737             $window.localStorage.setItem(key, jsonified);
738         } catch (e) {
739             console.log('localStorage.setItem (overwrite) failed for '+key+': ', e);
740         }
741     }
742
743     service.appendItem = function(key, value) {
744         if (!service.useSettings())
745             return $q.when(service.appendLocalItem(key, value));
746
747         if (service.hatchAvailable)
748             return service.appendRemoteItem(key, value);
749
750         console.error("Unable to appendItem in Hatch: " + key);
751         return $q.reject();
752     }
753
754     // append the value to a stored or new item
755     service.appendRemoteItem = function(key, value) {
756         service.keyCache[key] = value;
757         return service.attemptHatchDelivery({
758             key : key, 
759             content : value, 
760             action : 'append',
761         });
762     }
763
764     service.appendLocalItem = function(key, value, jsonified) {
765         if (jsonified === undefined ) 
766             jsonified = JSON.stringify(value);
767
768         var old_value = $window.localStorage.getItem(key) || '';
769         try {
770             $window.localStorage.setItem( key, old_value + jsonified );
771         } catch (e) {
772             console.log('localStorage.setItem (append) failed for '+key+': ', e);
773         }
774     }
775
776     // Set the value for the given key.  
777     // "LoginSession" items are removed when the user logs out or the 
778     // browser is closed.
779     // If the value is raw, pass it as 'value'.  If it was
780     // externally JSONified, pass it via jsonified.
781     service.setLoginSessionItem = function(key, value, jsonified) {
782         service.addLoginSessionKey(key);
783         if (jsonified === undefined ) 
784             jsonified = JSON.stringify(value);
785         $cookies.put(key, jsonified, {path: '/'});
786     }
787
788     // Set the value for the given key.  
789     // "Session" items are browser tab-specific and are removed when the
790     // tab is closed.
791     // If the value is raw, pass it as 'value'.  If it was
792     // externally JSONified, pass it via jsonified.
793     service.setSessionItem = function(key, value, jsonified) {
794         if (jsonified === undefined ) 
795             jsonified = JSON.stringify(value);
796         $window.sessionStorage.setItem(key, jsonified);
797     }
798
799     // remove a stored item
800     service.removeItem = function(key) {
801
802         if (service.keyIsWorkstation(key)) {
803             return service.removeWorkstationItem(key);
804         }
805
806         if (!service.keyStoredInBrowser(key)) {
807             return service.removeServerItem(key);
808         }
809
810         var deferred = $q.defer();
811         service.removeBrowserItem(key).then(
812             function(response) {deferred.resolve(response);},
813             function() { // Hatch error
814                 deferred.reject("Unable to removeItem from Hatch: " + key);
815             }
816         );
817
818         return deferred.promise;
819     }
820
821     service.removeBrowserItem = function(key) {
822         if (service.useSettings()) {
823             if (service.hatchAvailable) {
824                 return service.removeRemoteItem(key);
825             } else {
826                 return $q.reject('error talking to Hatch');
827             }
828         } else {
829             return $q.when(service.removeLocalItem(key));
830         }
831     }
832
833     service.removeServerItem = function(key) {
834         return service.setServerItem(key, null);
835     }
836
837     service.removeRemoteItem = function(key) {
838         delete service.keyCache[key];
839         return service.attemptHatchDelivery({
840             key : key,
841             action : 'remove'
842         });
843     }
844
845     service.removeLocalItem = function(key) {
846         $window.localStorage.removeItem(key);
847     }
848
849     service.removeLoginSessionItem = function(key) {
850         service.removeLoginSessionKey(key);
851         $cookies.remove(key, {path: '/'});
852     }
853
854     service.removeSessionItem = function(key) {
855         $window.sessionStorage.removeItem(key);
856     }
857
858     /**
859      * Remove all "LoginSession" items.
860      */
861     service.clearLoginSessionItems = function() {
862         angular.forEach(service.getLoginSessionKeys(), function(key) {
863             service.removeLoginSessionItem(key);
864         });
865
866         // remove the keys cache.
867         service.removeLocalItem('eg.hatch.login_keys');
868     }
869
870     // if set, prefix limits the return set to keys starting with 'prefix'
871     service.getKeys = function(prefix) {
872         var promise = service.getServerKeys(prefix);
873         return service.getBrowserKeys(prefix).then(function(browserKeys) {
874             return promise.then(function(serverKeys) {
875                 return serverKeys.concat(browserKeys);
876             });
877         });
878     }
879
880     service.getRemoteKeys = function(prefix) {
881         return service.attemptHatchDelivery({
882             key : prefix,
883             action : 'keys'
884         });
885     }
886
887     service.getBrowserKeys = function(prefix) {
888         if (service.useSettings()) 
889             return service.getRemoteKeys(prefix);
890         return $q.when(service.getLocalKeys(prefix));
891     }
892
893     service.getServerKeys = function(prefix, options) {
894         if (!service.auth) service.auth = $injector.get('egAuth');
895         if (!service.auth.token()) return $q.when({});
896         return egNet.request(
897             'open-ils.actor',
898             'open-ils.actor.settings.staff.applied.names.authoritative.atomic',
899             service.auth.token(), prefix, options
900         );
901     }
902
903     service.getLocalKeys = function(prefix) {
904         var keys = [];
905         var idx = 0;
906         while ( (k = $window.localStorage.key(idx++)) !== null) {
907             // key prefix match test
908             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
909             keys.push(k);
910         }
911         return keys;
912     }
913
914
915     /**
916      * Array of "LoginSession" keys.
917      * Note we have to store these as "Local" items so browser tabs can
918      * share them.  We could store them as cookies, but it's more data
919      * that has to go back/forth to the server.  A "LoginSession" key name is
920      * not private, though, so it's OK if they are left in localStorage
921      * until the next login.
922      */
923     service.getLoginSessionKeys = function(prefix) {
924         var keys = [];
925         var idx = 0;
926         var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
927         angular.forEach(login_keys, function(k) {
928             // key prefix match test
929             if (prefix && k.substr(0, prefix.length) != prefix) return;
930             keys.push(k);
931         });
932         return keys;
933     }
934
935     service.addLoginSessionKey = function(key) {
936         var keys = service.getLoginSessionKeys();
937         if (keys.indexOf(key) < 0) {
938             keys.push(key);
939             service.setLocalItem('eg.hatch.login_keys', keys);
940         }
941     }
942
943     service.removeLoginSessionKey = function(key) {
944         var keys = service.getLoginSessionKeys().filter(function(k) {
945             return k != key;
946         });
947         service.setLocalItem('eg.hatch.login_keys', keys);
948     }
949
950     // Copy all stored settings from localStorage to Hatch.
951     // If 'move' is true, delete the local settings once cloned.
952     service.copySettingsToHatch = function(move) {
953         var deferred = $q.defer();
954         var keys = service.getLocalKeys();
955
956         angular.forEach(keys, function(key) {
957
958             // Hatch keys are local-only
959             if (key.match(/^eg.hatch/)) return;
960
961             console.debug("Copying to Hatch Storage: " + key);
962             service.setRemoteItem(key, service.getLocalItem(key))
963             .then(function() { // key successfully cloned.
964
965                 // delete the local copy if requested.
966                 if (move) service.removeLocalItem(key);
967
968                 // resolve the promise after processing the last key.
969                 if (key == keys[keys.length-1]) 
970                     deferred.resolve();
971             });
972         });
973
974         return deferred.promise;
975     }
976
977     // Copy all stored settings from Hatch to localStorage.
978     // If 'move' is true, delete the Hatch settings once cloned.
979     service.copySettingsToLocal = function(move) {
980         var deferred = $q.defer();
981
982         service.getRemoteKeys().then(function(keys) {
983             angular.forEach(keys, function(key) {
984                 service.getRemoteItem(key).then(function(val) {
985
986                     console.debug("Copying to Local Storage: " + key);
987                     service.setLocalItem(key, val);
988
989                     // delete the remote copy if requested.
990                     if (move) service.removeRemoteItem(key);
991
992                     // resolve the promise after processing the last key.
993                     if (key == keys[keys.length-1]) 
994                         deferred.resolve();
995                 });
996             });
997         });
998
999         return deferred.promise;
1000     }
1001
1002     service.hostname = function() {
1003         if (service.hatchAvailable) {
1004             return service.attemptHatchDelivery({action : 'hostname'})
1005             .then(
1006                 function(name) { return name; },
1007                 // Gracefully handle case where Hatch has not yet been 
1008                 // updated to include the hostname command.
1009                 function() {return null}
1010             );
1011         } 
1012         return $q.when(null);
1013     }
1014
1015     // COPIED FROM XUL util/text.js
1016     service.reverse_preserve_string_in_html = function( text ) {
1017         text = text.replace(/&amp;/g, '&');
1018         text = text.replace(/&quot;/g, '"');
1019         text = text.replace(/&#39;/g, "'");
1020         text = text.replace(/&nbsp;/g, ' ');
1021         text = text.replace(/&lt;/g, '<');
1022         text = text.replace(/&gt;/g, '>');
1023         return text;
1024     }
1025
1026     // COPIED FROM XUL util/print.js
1027     service.html2txt = function(html) {
1028         var lines = html.split(/\n/);
1029         var new_lines = [];
1030         for (var i = 0; i < lines.length; i++) {
1031             var line = lines[i];
1032             if (!line) {
1033                 new_lines.push(line);
1034                 continue;
1035             }
1036
1037             // This undoes the util.text.preserve_string_in_html 
1038             // call that spine_label.js does
1039             line = service.reverse_preserve_string_in_html(line);
1040
1041             // This looks for @hex attributes containing 2-digit hex 
1042             // codes, and converts them into real characters
1043             line = line.replace(/(<.+?)hex=['"](.+?)['"](.*?>)/gi, 
1044                 function(str,p1,p2,p3,offset,s) {
1045
1046                 var raw_chars = '';
1047                 var hex_chars = p2.match(/[0-9,a-f,A-F][0-9,a-f,A-F]/g);
1048                 for (var j = 0; j < hex_chars.length; j++) {
1049                     raw_chars += String.fromCharCode( parseInt(hex_chars[j],16) );
1050                 }
1051                 return p1 + p3 + raw_chars;
1052             });
1053
1054             line = line.replace(/<head.*?>.*?<\/head>/gi, '');
1055             line = line.replace(/<br.*?>/gi,'\r\n');
1056             line = line.replace(/<table.*?>/gi,'');
1057             line = line.replace(/<tr.*?>/gi,'');
1058             line = line.replace(/<hr.*?>/gi,'\r\n');
1059             line = line.replace(/<p.*?>/gi,'');
1060             line = line.replace(/<block.*?>/gi,'');
1061             line = line.replace(/<li.*?>/gi,' * ');
1062             line = line.replace(/<.+?>/gi,'');
1063             if (line) { new_lines.push(line); }
1064         }
1065
1066         return new_lines.join('\n');
1067     }
1068
1069     // The only requirement for opening Hatch is that the DOM be loaded.
1070     // Open the connection now so its state will be immediately available.
1071     service.openHatch();
1072
1073     return service;
1074 }])
1075