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','egNet','$injector',
29 function($q , $window , $timeout , $interpolate , $cookies , egNet , $injector ) {
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;
38 // key/value cache -- avoid unnecessary Hatch extension requests.
39 // Only affects *RemoteItem calls.
40 service.keyCache = {};
42 // Keep a local copy of all retrieved setting summaries, which indicate
43 // which setting types exist for each setting.
44 service.serverSettingSummaries = {};
47 * List string prefixes for On-Call storage keys. On-Call keys
48 * are those that can be set/get/remove'd from localStorage when
49 * Hatch is not avaialable, even though Hatch is configured as the
50 * primary storage location for the key in question. On-Call keys
51 * are those that allow the user to login and perform basic admin
52 * tasks (like disabling Hatch) even when Hatch is down.
53 * AKA Browser Staff Run Level 3.
54 * Note that no attempt is made to synchronize data between Hatch
55 * and localStorage for On-Call keys. Only one destation is active
56 * at a time and each maintains its own data separately.
58 service.onCallPrefixes = ['eg.workstation'];
60 // Returns true if the key can be set/get in localStorage even when
61 // Hatch is not available.
62 service.keyIsOnCall = function(key) {
64 angular.forEach(service.onCallPrefixes, function(pfx) {
65 if (key.match(new RegExp('^' + pfx)))
72 * Settings with these prefixes will always live in the browser.
74 service.browserOnlyPrefixes = [
78 'current_tag_table_marc21_biblio',
83 service.keyStoredInBrowser = function(key) {
85 if (service.disableServerSettings) {
86 // When server-side storage is disabled, treat every
87 // setting like it's stored locally.
91 var browserOnly = false;
92 service.browserOnlyPrefixes.forEach(function(pfx) {
93 if (key.match(new RegExp('^' + pfx)))
100 // write a message to the Hatch port
101 service.sendToHatch = function(msg) {
104 // shallow copy and scrub msg before sending
105 angular.forEach(msg, function(val, key) {
106 if (key.match(/deferred/)) return;
110 console.debug("sending to Hatch: " + JSON.stringify(msg2));
113 $window.postMessage(msg2, $window.location.origin);
116 // Send request to Hatch or reject if Hatch is unavailable
117 service.attemptHatchDelivery = function(msg) {
118 msg.msgid = service.msgId++;
119 msg.deferred = $q.defer();
121 if (service.hatchAvailable) {
122 service.messages[msg.msgid] = msg;
123 service.sendToHatch(msg);
127 'Hatch request attempted but Hatch is not available');
128 msg.deferred.reject(msg);
131 return msg.deferred.promise;
135 // resolve the promise on the given request and remove
136 // it from our tracked requests.
137 service.resolveRequest = function(msg) {
139 if (!service.messages[msg.msgid]) {
140 console.error('no cached message for id = ' + msg.msgid);
144 // for requests sent through Hatch, only the cached
145 // request will have the original promise attached
146 msg.deferred = service.messages[msg.msgid].deferred;
147 delete service.messages[msg.msgid]; // un-cache
149 if (msg.status == 200) {
150 msg.deferred.resolve(msg.content);
152 console.warn("Hatch command failed with status="
153 + msg.status + " and message=" + msg.message);
154 msg.deferred.reject();
158 service.openHatch = function() {
160 // When the Hatch extension loads, it tacks an attribute onto
161 // the top-level documentElement to indicate it's available.
162 if (!$window.document.documentElement.getAttribute('hatch-is-open')) {
163 //console.debug("Hatch is not available");
167 $window.addEventListener("message", function(event) {
168 // We only accept messages from our own content script.
169 if (event.source != window) return;
171 // We only care about messages from the Hatch extension.
172 if (event.data && event.data.from == 'extension') {
174 // Avoid logging full Hatch responses. they can get large.
176 'Hatch responded to message ID ' + event.data.msgid);
178 service.resolveRequest(event.data);
182 service.hatchAvailable = true; // public flag
185 service.remotePrint = function(
186 context, contentType, content, withDialog) {
188 return service.getPrintConfig(context).then(
190 // print configuration retrieved; print
191 return service.attemptHatchDelivery({
195 contentType : contentType,
196 showDialog : withDialog,
202 service.getPrintConfig = function(context) {
203 return service.getItem('eg.print.config.' + context);
206 service.setPrintConfig = function(context, config) {
207 return service.setItem('eg.print.config.' + context, config);
210 service.getPrinterOptions = function(name) {
211 return service.attemptHatchDelivery({
212 action : 'printer-options',
217 service.getPrinters = function() {
218 if (service.printers) // cached printers
219 return $q.when(service.printers);
221 return service.attemptHatchDelivery({action : 'printers'}).then(
223 // we have remote printers; sort by name and return
225 service.printers = printers.sort(
226 function(a,b) {return a.name < b.name ? -1 : 1});
227 return service.printers;
230 // remote call failed and there is no such thing as local
231 // printers; return empty set.
232 function() { return [] }
236 // TODO: once Hatch is printing-only, should probably store
237 // this preference on the server.
238 service.usePrinting = function() {
239 return service.getLocalItem('eg.hatch.enable.printing');
242 service.useSettings = function() {
243 return service.getLocalItem('eg.hatch.enable.settings');
246 service.useOffline = function() {
247 return service.getLocalItem('eg.hatch.enable.offline');
250 // get the value for a stored item
251 service.getItem = function(key) {
253 console.debug('getting item: ' + key);
255 if (!service.keyStoredInBrowser(key)) {
256 return service.getServerItem(key);
259 var deferred = $q.defer();
261 service.getBrowserItem(key).then(
262 function(val) { deferred.resolve(val); },
264 function() { // Hatch error
265 if (service.keyIsOnCall(key)) {
266 console.warn("Unable to getItem from Hatch: " + key +
267 ". Retrieving item from local storage instead");
268 deferred.resolve(service.getLocalItem(key));
271 deferred.reject("Unable to getItem from Hatch: " + key);
275 return deferred.promise;
278 // Collect values in batch.
279 // For server-stored values espeically, this is more efficient
280 // than a series of one-off calls.
281 service.getItemBatch = function(keys) {
282 var browserKeys = [];
285 // To take full advantage of the getServerItemBatch call,
286 // we have to know in advance which keys to send to the server
287 // vs those to handle in the browser.
288 keys.forEach(function(key) {
289 if (service.keyStoredInBrowser(key)) {
290 browserKeys.push(key);
292 serverKeys.push(key);
298 var serverPromise = serverKeys.length === 0 ? $q.when() :
299 service.getServerItemBatch(serverKeys).then(function(values) {
300 angular.forEach(values, function(val, key) {
305 var browserPromises = [];
306 browserKeys.forEach(function(key) {
307 browserPromises.push(
308 service.getBrowserItem(key).then(function(val) {
314 return $q.all(browserPromises.concat(serverPromise))
315 .then(function() {return settings});
318 service.getBrowserItem = function(key) {
319 if (service.useSettings()) {
320 if (service.hatchAvailable) {
321 return service.getRemoteItem(key);
324 return $q.when(service.getLocalItem(key));
329 service.getRemoteItem = function(key) {
331 if (service.keyCache[key] != undefined)
332 return $q.when(service.keyCache[key])
334 return service.attemptHatchDelivery({
337 }).then(function(content) {
338 return service.keyCache[key] = content;
342 service.getLocalItem = function(key) {
343 var val = $window.localStorage.getItem(key);
344 if (val === null || val === undefined) return;
346 return JSON.parse(val);
349 "Deleting invalid JSON for localItem: " + key + " => " + val);
350 service.removeLocalItem(key);
355 // Force auth cookies to live under path "/" instead of "/eg/staff"
356 // so they may be shared with the Angular app.
357 // There's no way to tell under what path a cookie is stored in
358 // the browser, all we can do is migrate it regardless.
359 service.migrateAuthCookies = function() {
364 ].forEach(function(key) {
365 var val = service.getLoginSessionItem(key);
367 $cookies.remove(key, {path: '/eg/staff/'});
368 service.setLoginSessionItem(key, val);
373 service.getLoginSessionItem = function(key) {
374 var val = $cookies.get(key);
375 if (val == null) return;
376 return JSON.parse(val);
379 service.getSessionItem = function(key) {
380 var val = $window.sessionStorage.getItem(key);
381 if (val == null) return;
382 return JSON.parse(val);
386 * @param tmp bool Store the value as a session cookie only.
387 * tmp values are removed during logout or browser close.
389 service.setItem = function(key, value) {
391 if (!service.keyStoredInBrowser(key)) {
392 return service.setServerItem(key, value);
395 var deferred = $q.defer();
396 return service.setBrowserItem(key, value).then(
397 function(val) {deferred.resolve(val);},
399 function() { // Hatch error
401 if (service.keyIsOnCall(key)) {
402 console.warn("Unable to setItem in Hatch: " +
403 key + ". Setting in local storage instead");
405 deferred.resolve(service.setLocalItem(key, value));
407 deferred.reject("Unable to setItem in Hatch: " + key);
412 service.setBrowserItem = function(key, value) {
413 if (service.useSettings()) {
414 if (service.hatchAvailable) {
415 return service.setRemoteItem(key, value);
417 return $q.reject('Unable to get item from hatch');
420 return $q.when(service.setLocalItem(key, value));
424 service.setServerItem = function(key, value) {
425 if (!service.auth) service.auth = $injector.get('egAuth');
426 if (!service.auth.token()) return $q.when();
428 // If we have already attempted to retrieve a value for this
429 // setting, then we can tell up front whether applying a value
430 // at the server will be an option. If not, store locally.
431 var summary = service.serverSettingSummaries[key];
432 if (summary && !summary.has_staff_setting) {
434 if (summary.has_org_setting === 't') {
435 // When no user/ws setting types exist but an org unit
436 // setting type does, it means the value cannot be
437 // applied by an individual user. Nothing left to do.
441 // No setting types of any flavor exist.
442 // Fall back to local storage.
444 if (value === null) {
445 // a null value means clear the server setting.
446 return service.removeBrowserItem(key);
448 console.warn('No server setting type exists for ' + key);
449 return service.setBrowserItem(key, value);
454 settings[key] = value;
456 return egNet.request(
458 'open-ils.actor.settings.apply.user_or_ws',
459 service.auth.token(), settings
460 ).then(function(appliedCount) {
462 if (appliedCount == 0) {
463 console.warn('No server setting type exists for ' + key);
464 // We were unable to store the setting on the server,
465 // presumably becuase no server-side setting type exists.
466 // Add to local storage instead.
467 service.setLocalItem(key, value);
470 service.keyCache[key] = value;
475 service.getServerItem = function(key) {
476 if (key in service.keyCache) {
477 return $q.when(service.keyCache[key])
480 if (!service.auth) service.auth = $injector.get('egAuth');
481 if (!service.auth.token()) return $q.when(null);
483 return egNet.request(
485 'open-ils.actor.settings.retrieve.atomic',
486 [key], service.auth.token()
487 ).then(function(settings) {
488 return service.handleServerItemResponse(settings[0]);
492 service.handleServerItemResponse = function(summary) {
493 var key = summary.name;
494 var val = summary.value;
496 // For our purposes, we only care if a setting can be stored
497 // as an org setting or a user-or-workstation setting.
498 summary.has_staff_setting = (
499 summary.has_user_setting === 't' ||
500 summary.has_workstation_setting === 't'
503 summary.value = null; // avoid duplicate value caches
504 service.serverSettingSummaries[key] = summary;
507 // We have a server setting. Nothing left to do.
508 return $q.when(service.keyCache[key] = val);
511 if (!summary.has_staff_setting) {
513 if (summary.has_org_setting === 't') {
514 // An org unit setting type exists but no value is applied
515 // that this workstation has access to. The existence of
516 // an org unit setting type and no user/ws setting type
517 // means applying a value locally is not allowed.
518 return $q.when(service.keyCache[key] = undefined);
521 console.warn('No server setting type exists for '
522 + key + ', using local value.');
524 return service.getBrowserItem(key);
527 // A user/ws setting type exists, but no server value exists.
528 // Migrate the local setting to the server.
530 var deferred = $q.defer();
531 service.getBrowserItem(key).then(function(browserVal) {
533 if (browserVal === null || browserVal === undefined) {
534 // No local value to migrate.
535 return deferred.resolve(service.keyCache[key] = undefined);
538 // Migrate the local value to the server.
540 service.setServerItem(key, browserVal).then(
541 function(appliedCount) {
542 if (appliedCount == 1) {
543 console.info('setting ' + key + ' successfully ' +
544 'migrated to a server setting');
545 service.removeBrowserItem(key); // fire & forget
547 console.error('error migrating setting to server,'
548 + ' falling back to local value');
550 deferred.resolve(service.keyCache[key] = browserVal);
555 return deferred.promise;
558 service.getServerItemBatch = function(keys) {
559 // no cache checking for now. assumes batch mode is only
560 // called once on page load. maybe add cache checking later.
561 if (!service.auth) service.auth = $injector.get('egAuth');
562 if (!service.auth.token()) return $q.when({});
564 var foundValues = {};
565 return egNet.request(
567 'open-ils.actor.settings.retrieve.atomic',
568 keys, service.auth.token()
571 //return foundValues;
573 var deferred = $q.defer();
574 function checkOne(setting) {
576 deferred.resolve(foundValues);
579 service.handleServerItemResponse(setting)
580 .then(function(resp) {
581 if (resp !== undefined) {
582 foundValues[setting.name] = resp;
585 checkOne(settings[0]);
589 checkOne(settings[0]);
590 return deferred.promise;
596 // set the value for a stored or new item
597 service.setRemoteItem = function(key, value) {
598 service.keyCache[key] = value;
599 return service.attemptHatchDelivery({
606 // Set the value for the given key.
607 // "Local" items persist indefinitely.
608 // If the value is raw, pass it as 'value'. If it was
609 // externally JSONified, pass it via jsonified.
610 service.setLocalItem = function(key, value, jsonified) {
611 if (jsonified === undefined ) {
612 jsonified = JSON.stringify(value);
613 } else if (value === undefined) {
617 $window.localStorage.setItem(key, jsonified);
619 console.log('localStorage.setItem (overwrite) failed for '+key+': ', e);
623 service.appendItem = function(key, value) {
624 if (!service.useSettings())
625 return $q.when(service.appendLocalItem(key, value));
627 if (service.hatchAvailable)
628 return service.appendRemoteItem(key, value);
630 if (service.keyIsOnCall(key)) {
631 console.warn("Unable to appendItem in Hatch: " +
632 key + ". Setting in local storage instead");
634 return $q.when(service.appendLocalItem(key, value));
637 console.error("Unable to appendItem in Hatch: " + key);
641 // append the value to a stored or new item
642 service.appendRemoteItem = function(key, value) {
643 service.keyCache[key] = value;
644 return service.attemptHatchDelivery({
651 service.appendLocalItem = function(key, value, jsonified) {
652 if (jsonified === undefined )
653 jsonified = JSON.stringify(value);
655 var old_value = $window.localStorage.getItem(key) || '';
657 $window.localStorage.setItem( key, old_value + jsonified );
659 console.log('localStorage.setItem (append) failed for '+key+': ', e);
663 // Set the value for the given key.
664 // "LoginSession" items are removed when the user logs out or the
665 // browser is closed.
666 // If the value is raw, pass it as 'value'. If it was
667 // externally JSONified, pass it via jsonified.
668 service.setLoginSessionItem = function(key, value, jsonified) {
669 service.addLoginSessionKey(key);
670 if (jsonified === undefined )
671 jsonified = JSON.stringify(value);
672 $cookies.put(key, jsonified, {path: '/'});
675 // Set the value for the given key.
676 // "Session" items are browser tab-specific and are removed when the
678 // If the value is raw, pass it as 'value'. If it was
679 // externally JSONified, pass it via jsonified.
680 service.setSessionItem = function(key, value, jsonified) {
681 if (jsonified === undefined )
682 jsonified = JSON.stringify(value);
683 $window.sessionStorage.setItem(key, jsonified);
686 // remove a stored item
687 service.removeItem = function(key) {
689 if (!service.keyStoredInBrowser(key)) {
690 return service.removeServerItem(key);
693 var deferred = $q.defer();
694 service.removeBrowserItem(key).then(
695 function(response) {deferred.resolve(response);},
696 function() { // Hatch error
698 if (service.keyIsOnCall(key)) {
699 console.warn("Unable to removeItem from Hatch: " + key +
700 ". Removing item from local storage instead");
702 deferred.resolve(service.removeLocalItem(key));
705 deferred.reject("Unable to removeItem from Hatch: " + key);
709 return deferred.promise;
712 service.removeBrowserItem = function(key) {
713 if (service.useSettings()) {
714 if (service.hatchAvailable) {
715 return service.removeRemoteItem(key);
717 return $q.reject('error talking to Hatch');
720 return $q.when(service.removeLocalItem(key));
724 service.removeServerItem = function(key) {
725 return service.setServerItem(key, null);
728 service.removeRemoteItem = function(key) {
729 delete service.keyCache[key];
730 return service.attemptHatchDelivery({
736 service.removeLocalItem = function(key) {
737 $window.localStorage.removeItem(key);
740 service.removeLoginSessionItem = function(key) {
741 service.removeLoginSessionKey(key);
742 $cookies.remove(key, {path: '/'});
745 service.removeSessionItem = function(key) {
746 $window.sessionStorage.removeItem(key);
750 * Remove all "LoginSession" items.
752 service.clearLoginSessionItems = function() {
753 angular.forEach(service.getLoginSessionKeys(), function(key) {
754 service.removeLoginSessionItem(key);
757 // remove the keys cache.
758 service.removeLocalItem('eg.hatch.login_keys');
761 // if set, prefix limits the return set to keys starting with 'prefix'
762 service.getKeys = function(prefix) {
763 var promise = service.getServerKeys(prefix);
764 return service.getBrowserKeys(prefix).then(function(browserKeys) {
765 return promise.then(function(serverKeys) {
766 return serverKeys.concat(browserKeys);
771 service.getRemoteKeys = function(prefix) {
772 return service.attemptHatchDelivery({
778 service.getBrowserKeys = function(prefix) {
779 if (service.useSettings())
780 return service.getRemoteKeys(prefix);
781 return $q.when(service.getLocalKeys(prefix));
784 service.getServerKeys = function(prefix, options) {
785 if (!service.auth) service.auth = $injector.get('egAuth');
786 if (!service.auth.token()) return $q.when({});
787 return egNet.request(
789 'open-ils.actor.settings.staff.applied.names.authoritative.atomic',
790 service.auth.token(), prefix, options
794 service.getLocalKeys = function(prefix) {
797 while ( (k = $window.localStorage.key(idx++)) !== null) {
798 // key prefix match test
799 if (prefix && k.substr(0, prefix.length) != prefix) continue;
807 * Array of "LoginSession" keys.
808 * Note we have to store these as "Local" items so browser tabs can
809 * share them. We could store them as cookies, but it's more data
810 * that has to go back/forth to the server. A "LoginSession" key name is
811 * not private, though, so it's OK if they are left in localStorage
812 * until the next login.
814 service.getLoginSessionKeys = function(prefix) {
817 var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
818 angular.forEach(login_keys, function(k) {
819 // key prefix match test
820 if (prefix && k.substr(0, prefix.length) != prefix) return;
826 service.addLoginSessionKey = function(key) {
827 var keys = service.getLoginSessionKeys();
828 if (keys.indexOf(key) < 0) {
830 service.setLocalItem('eg.hatch.login_keys', keys);
834 service.removeLoginSessionKey = function(key) {
835 var keys = service.getLoginSessionKeys().filter(function(k) {
838 service.setLocalItem('eg.hatch.login_keys', keys);
841 // Copy all stored settings from localStorage to Hatch.
842 // If 'move' is true, delete the local settings once cloned.
843 service.copySettingsToHatch = function(move) {
844 var deferred = $q.defer();
845 var keys = service.getLocalKeys();
847 angular.forEach(keys, function(key) {
849 // Hatch keys are local-only
850 if (key.match(/^eg.hatch/)) return;
852 console.debug("Copying to Hatch Storage: " + key);
853 service.setRemoteItem(key, service.getLocalItem(key))
854 .then(function() { // key successfully cloned.
856 // delete the local copy if requested.
857 if (move) service.removeLocalItem(key);
859 // resolve the promise after processing the last key.
860 if (key == keys[keys.length-1])
865 return deferred.promise;
868 // Copy all stored settings from Hatch to localStorage.
869 // If 'move' is true, delete the Hatch settings once cloned.
870 service.copySettingsToLocal = function(move) {
871 var deferred = $q.defer();
873 service.getRemoteKeys().then(function(keys) {
874 angular.forEach(keys, function(key) {
875 service.getRemoteItem(key).then(function(val) {
877 console.debug("Copying to Local Storage: " + key);
878 service.setLocalItem(key, val);
880 // delete the remote copy if requested.
881 if (move) service.removeRemoteItem(key);
883 // resolve the promise after processing the last key.
884 if (key == keys[keys.length-1])
890 return deferred.promise;
893 // The only requirement for opening Hatch is that the DOM be loaded.
894 // Open the connection now so its state will be immediately available.