2 * Core Service - egHatch
4 * Dispatches print and data storage requests to the appropriate handler.
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
11 * Most handlers also provide direct remote and local variants to the
12 * application can decide to which to use as needed.
14 * Local storage requests are handled by $window.localStorage.
16 * Note that all top-level and remote requests return promises. All
17 * local requests return immediate values, since local requests are
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()
25 angular.module('egCoreMod')
28 ['$q','$window','$timeout','$interpolate','$cookies',
29 function($q , $window , $timeout , $interpolate , $cookies) {
33 service.messages = {};
34 service.hatchAvailable = false;
36 // key/value cache -- avoid unnecessary Hatch extension requests.
37 // Only affects *RemoteItem calls.
38 service.keyCache = {};
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.
52 service.onCallPrefixes = ['eg.workstation'];
54 // Returns true if the key can be set/get in localStorage even when
55 // Hatch is not available.
56 service.keyIsOnCall = function(key) {
58 angular.forEach(service.onCallPrefixes, function(pfx) {
59 if (key.match(new RegExp('^' + pfx)))
65 // write a message to the Hatch port
66 service.sendToHatch = function(msg) {
69 // shallow copy and scrub msg before sending
70 angular.forEach(msg, function(val, key) {
71 if (key.match(/deferred/)) return;
75 console.debug("sending to Hatch: " + JSON.stringify(msg2));
78 $window.postMessage(msg2, $window.location.origin);
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();
86 if (service.hatchAvailable) {
87 service.messages[msg.msgid] = msg;
88 service.sendToHatch(msg);
92 'Hatch request attempted but Hatch is not available');
93 msg.deferred.reject(msg);
96 return msg.deferred.promise;
100 // resolve the promise on the given request and remove
101 // it from our tracked requests.
102 service.resolveRequest = function(msg) {
104 if (!service.messages[msg.msgid]) {
105 console.error('no cached message for id = ' + msg.msgid);
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
114 if (msg.status == 200) {
115 msg.deferred.resolve(msg.content);
117 console.warn("Hatch command failed with status="
118 + msg.status + " and message=" + msg.message);
119 msg.deferred.reject();
123 service.openHatch = function() {
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");
132 $window.addEventListener("message", function(event) {
133 // We only accept messages from our own content script.
134 if (event.source != window) return;
136 // We only care about messages from the Hatch extension.
137 if (event.data && event.data.from == 'extension') {
139 // Avoid logging full Hatch responses. they can get large.
141 'Hatch responded to message ID ' + event.data.msgid);
143 service.resolveRequest(event.data);
147 service.hatchAvailable = true; // public flag
150 service.remotePrint = function(
151 context, contentType, content, withDialog) {
153 return service.getPrintConfig(context).then(
155 // print configuration retrieved; print
156 return service.attemptHatchDelivery({
160 contentType : contentType,
161 showDialog : withDialog,
167 service.getPrintConfig = function(context) {
168 return service.getRemoteItem('eg.print.config.' + context);
171 service.setPrintConfig = function(context, config) {
172 return service.setRemoteItem('eg.print.config.' + context, config);
175 service.getPrinterOptions = function(name) {
176 return service.attemptHatchDelivery({
177 action : 'printer-options',
182 service.getPrinters = function() {
183 if (service.printers) // cached printers
184 return $q.when(service.printers);
186 return service.attemptHatchDelivery({action : 'printers'}).then(
188 // we have remote printers; sort by name and return
190 service.printers = printers.sort(
191 function(a,b) {return a.name < b.name ? -1 : 1});
192 return service.printers;
195 // remote call failed and there is no such thing as local
196 // printers; return empty set.
197 function() { return [] }
201 service.usePrinting = function() {
202 return service.getLocalItem('eg.hatch.enable.printing');
205 service.useSettings = function() {
206 return service.getLocalItem('eg.hatch.enable.settings');
209 service.useOffline = function() {
210 return service.getLocalItem('eg.hatch.enable.offline');
213 // get the value for a stored item
214 service.getItem = function(key) {
216 if (!service.useSettings())
217 return $q.when(service.getLocalItem(key));
219 if (service.hatchAvailable)
220 return service.getRemoteItem(key);
222 if (service.keyIsOnCall(key)) {
223 console.warn("Unable to getItem from Hatch: " + key +
224 ". Retrieving item from local storage instead");
226 return $q.when(service.getLocalItem(key));
229 console.error("Unable to getItem from Hatch: " + key);
233 service.getRemoteItem = function(key) {
235 if (service.keyCache[key] != undefined)
236 return $q.when(service.keyCache[key])
238 return service.attemptHatchDelivery({
241 }).then(function(content) {
242 return service.keyCache[key] = content;
246 service.getLocalItem = function(key) {
247 var val = $window.localStorage.getItem(key);
248 if (val == null) return;
250 return JSON.parse(val);
253 "Deleting invalid JSON for localItem: " + key + " => " + val);
254 service.removeLocalItem(key);
259 service.getLoginSessionItem = function(key) {
260 var val = $cookies.get(key);
261 if (val == null) return;
262 return JSON.parse(val);
265 service.getSessionItem = function(key) {
266 var val = $window.sessionStorage.getItem(key);
267 if (val == null) return;
268 return JSON.parse(val);
272 * @param tmp bool Store the value as a session cookie only.
273 * tmp values are removed during logout or browser close.
275 service.setItem = function(key, value) {
276 if (!service.useSettings())
277 return $q.when(service.setLocalItem(key, value));
279 if (service.hatchAvailable)
280 return service.setRemoteItem(key, value);
282 if (service.keyIsOnCall(key)) {
283 console.warn("Unable to setItem in Hatch: " +
284 key + ". Setting in local storage instead");
286 return $q.when(service.setLocalItem(key, value));
289 console.error("Unable to setItem in Hatch: " + key);
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({
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);
313 service.appendItem = function(key, value) {
314 if (!service.useSettings())
315 return $q.when(service.appendLocalItem(key, value));
317 if (service.hatchAvailable)
318 return service.appendRemoteItem(key, value);
320 if (service.keyIsOnCall(key)) {
321 console.warn("Unable to appendItem in Hatch: " +
322 key + ". Setting in local storage instead");
324 return $q.when(service.appendLocalItem(key, value));
327 console.error("Unable to appendItem in Hatch: " + key);
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({
341 service.appendLocalItem = function(key, value, jsonified) {
342 if (jsonified === undefined )
343 jsonified = JSON.stringify(value);
345 var old_value = $window.localStorage.getItem(key) || '';
346 $window.localStorage.setItem( key, old_value + jsonified );
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);
361 // Set the value for the given key.
362 // "Session" items are browser tab-specific and are removed when the
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);
372 // remove a stored item
373 service.removeItem = function(key) {
374 if (!service.useSettings())
375 return $q.when(service.removeLocalItem(key));
377 if (service.hatchAvailable)
378 return service.removeRemoteItem(key);
380 if (service.keyIsOnCall(key)) {
381 console.warn("Unable to removeItem from Hatch: " + key +
382 ". Removing item from local storage instead");
384 return $q.when(service.removeLocalItem(key));
387 console.error("Unable to removeItem from Hatch: " + key);
391 service.removeRemoteItem = function(key) {
392 delete service.keyCache[key];
393 return service.attemptHatchDelivery({
399 service.removeLocalItem = function(key) {
400 $window.localStorage.removeItem(key);
403 service.removeLoginSessionItem = function(key) {
404 service.removeLoginSessionKey(key);
405 $cookies.remove(key);
408 service.removeSessionItem = function(key) {
409 $window.sessionStorage.removeItem(key);
413 * Remove all "LoginSession" items.
415 service.clearLoginSessionItems = function() {
416 angular.forEach(service.getLoginSessionKeys(), function(key) {
417 service.removeLoginSessionItem(key);
420 // remove the keys cache.
421 service.removeLocalItem('eg.hatch.login_keys');
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));
431 service.getRemoteKeys = function(prefix) {
432 return service.attemptHatchDelivery({
438 service.getLocalKeys = function(prefix) {
441 while ( (k = $window.localStorage.key(idx++)) !== null) {
442 // key prefix match test
443 if (prefix && k.substr(0, prefix.length) != prefix) continue;
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.
458 service.getLoginSessionKeys = function(prefix) {
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;
470 service.addLoginSessionKey = function(key) {
471 var keys = service.getLoginSessionKeys();
472 if (keys.indexOf(key) < 0) {
474 service.setLocalItem('eg.hatch.login_keys', keys);
478 service.removeLoginSessionKey = function(key) {
479 var keys = service.getLoginSessionKeys().filter(function(k) {
482 service.setLocalItem('eg.hatch.login_keys', keys);
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();
491 angular.forEach(keys, function(key) {
493 // Hatch keys are local-only
494 if (key.match(/^eg.hatch/)) return;
496 console.debug("Copying to Hatch Storage: " + key);
497 service.setRemoteItem(key, service.getLocalItem(key))
498 .then(function() { // key successfully cloned.
500 // delete the local copy if requested.
501 if (move) service.removeLocalItem(key);
503 // resolve the promise after processing the last key.
504 if (key == keys[keys.length-1])
509 return deferred.promise;
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();
517 service.getRemoteKeys().then(function(keys) {
518 angular.forEach(keys, function(key) {
519 service.getRemoteItem(key).then(function(val) {
521 console.debug("Copying to Local Storage: " + key);
522 service.setLocalItem(key, val);
524 // delete the remote copy if requested.
525 if (move) service.removeRemoteItem(key);
527 // resolve the promise after processing the last key.
528 if (key == keys[keys.length-1])
534 return deferred.promise;
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.