LP#1640255 Hatch declarativeContent plugin user/berick/lp1640255-hatch-native-messaging-declarative
authorBill Erickson <berickxx@gmail.com>
Wed, 23 Nov 2016 21:41:35 +0000 (16:41 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 23 Nov 2016 21:41:45 +0000 (16:41 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/web/js/ui/default/staff/services/hatch.js

index 2983f4c..c38920e 100644 (file)
  */
 angular.module('egCoreMod')
 
-.constant("HATCH_CONFIG", {
-    /* TODO: Extension name will change once the extension is 
-     * registered and will presumably be different per browser.
-     * Current extension name is borrowed from:
-     * https://chromium.googlesource.com/chromium/src/+/master/chrome/common/extensions/docs/examples/api/nativeMessaging
-     */
-    EXT_NAME_CHROME : "knldjmfmopnpolahpmmgbagdohdnhkik"
-})
-
 .factory('egHatch',
-           ['$q','$window','$timeout','$interpolate','$http','$cookies','HATCH_CONFIG',
-    function($q , $window , $timeout , $interpolate , $http , $cookies , HATCH_CONFIG) {
+           ['$q','$window','$timeout','$interpolate','$http','$cookies',
+    function($q , $window , $timeout , $interpolate , $http , $cookies) {
 
     var service = {};
-    service.port = null; // Hatch extension connection
     service.msgId = 1;
     service.messages = {};
     service.pending = [];
     service.hatchAvailable = null;
+    service.state = 'IDLE'; // IDLE, INIT, CONNECTED, NO_CONNECTION
 
     // write a message to the Hatch port
     service.sendToHatch = function(msg) {
@@ -55,7 +46,9 @@ angular.module('egCoreMod')
         });
 
         console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
-        service.port.postMessage(msg2);
+
+        msg2.from = 'page';
+        $window.postMessage(msg2, $window.location.origin);
     }
 
     // Send the request to Hatch if it's available.  
@@ -65,27 +58,18 @@ angular.module('egCoreMod')
         msg.msgid = service.msgId++;
         msg.deferred = $q.defer();
 
-        if (service.hatchAvailable === false) { // Hatch is closed
+        if (service.state == 'NO_CONNECTION') {
             msg.deferred.reject(msg);
 
-        } else if (service.hatchAvailable === true) { // Hatch is open
+        } else if (service.state.match(/CONNECTED|INIT/)) {
             // Hatch is known to be open
             service.messages[msg.msgid] = msg;
             service.sendToHatch(msg);
 
-        } else {  // Hatch status unknown; attempt to connect
+        } else if (service.state == 'IDLE') { 
             service.messages[msg.msgid] = msg;
             service.pending.push(msg);
-            // Connect to Hatch asynchronously. Othwerise, results
-            // in a $rootScope:inprog error in Firefox, possibly
-            // because hatchConnect() does not yet work in FF.
-            //
-            // Firefox does not yet support runtime.connect(..)
-            // from the browser to "externally_connectable" extensions.
-            // http://stackoverflow.com/questions/38487552/externally-connectable-and-firefox-webextensions
-            // http://stackoverflow.com/questions/10526995/can-a-site-invoke-a-browser-extension/10527809#10527809
-            // https://bugzilla.mozilla.org/show_bug.cgi?id=1204583
-            $timeout(service.hatchConnect);
+            $timeout(service.openHatch);
         }
 
         return msg.deferred.promise;
@@ -107,18 +91,71 @@ angular.module('egCoreMod')
         msg.deferred = service.messages[msg.msgid].deferred;
         delete service.messages[msg.msgid]; // un-cache
 
-        if (msg.status != 200) {
-            msg.deferred.reject();
-            throw new Error("Hatch command failed with status=" 
-                + msg.status + " and message=" + msg.message);
+        switch (service.state) {
+
+            case 'CONNECTED': // received a standard Hatch response
+                if (msg.status == 200) {
+                    msg.deferred.resolve(msg.content);
+                } else {
+                    msg.deferred.reject();
+                    console.warn("Hatch command failed with status=" 
+                        + msg.status + " and message=" + msg.message);
+                }
+                break;
+
+            case 'INIT':
+                if (msg.status == 200) {
+                    service.hatchAvailable = true; // public flag
+                    service.state = 'CONNECTED';
+                    service.hatchOpened();
+                } else {
+                    msg.deferred.reject();
+                    service.hatchWontOpen(msg.message);
+                }
+                break;
+
+            default:
+                console.warn(
+                    "Received message in unexpected state: " + service.state); 
+        }
+    }
+
+    service.openHatch = function() {
+
+        // When the Hatch extension loads, it tacks an attribute onto
+        // the page body to indicate it's available.
+
+        if (!$window.document.body.getAttribute('hatch-is-open')) {
+            service.hatchWontOpen('Hatch is not available');
+            return;
+        }
+
+        $window.addEventListener("message", function(event) {
+            // We only accept messages from our own content script.
+            if (event.source != window) return;
+
+            // We only care about messages from the Hatch extension.
+            if (event.data && event.data.from == 'extension') {
+
+                console.debug('Hatch says: ' 
+                    + JSON.stringify(event.data, null, 2));
+
+                service.resolveRequest(event.data);
+            }
+        }); 
+
+        service.state = 'INIT';
+        service.attemptHatchDelivery({action : 'init'});
+    }
 
-        } else {
-            msg.deferred.resolve(msg.content);
-        } 
+    service.hatchWontOpen = function(err) {
+        console.debug("Hatch connection failed: " + err);
+        service.state = 'NO_CONNECTION';
+        service.hatchAvailable = false;
+        service.hatchClosed();
     }
 
     service.hatchClosed = function() {
-        service.port = null;
         service.printers = [];
         service.printConfig = {};
         while ( (msg = service.pending.shift()) ) {
@@ -132,7 +169,7 @@ angular.module('egCoreMod')
     // Returns true if Hatch is required or if we are currently
     // communicating with the Hatch service. 
     service.usingHatch = function() {
-        return service.hatchAvailable || service.hatchRequired();
+        return service.state == 'CONNECTED' || service.hatchRequired();
     }
 
     // Returns true if this browser (via localStorage) is 
@@ -141,102 +178,6 @@ angular.module('egCoreMod')
         return service.getLocalItem('eg.hatch.required');
     }
 
-    /**
-     * See if the Hatch extension is registered before we try to
-     * connect. 
-     *
-     * Since chrome.runtime.connect() will return a valid Port object
-     * regardless of whether the requested extenion exists, first attempt
-     * a simple call/request that will loudly fail when the extension
-     * is not available.
-     *
-     * Returns a promise.
-     */
-    service.checkHatchExtension = function() {
-        var deferred = $q.defer();
-        try {
-            console.debug("Sending 'ping' request to Hatch extension");
-            chrome.runtime.sendMessage(
-                HATCH_CONFIG.EXT_NAME_CHROME, {ping : true},
-                function (reply) {
-                    if (reply && reply.pong) {
-                        deferred.resolve();
-                    } else {
-                        deferred.reject(
-                            "Extension failed to reply to 'ping' request");
-                    }
-                }
-            );
-        } catch (E) {
-            deferred.reject(E);
-        }
-        return deferred.promise;
-    }
-
-    service.hatchConnect = function() {
-        service.checkHatchExtension().then(
-            service.openHatchPort,      // extension is available
-            service.extConnectFailed    // extension is not available
-        );
-    }
-
-    service.extConnectFailed = function(err) {
-        console.debug("Hatch connection failed: " + err);
-        service.hatchAvailable = false;
-        service.hatchClosed();
-    }
-
-    service.openHatchPort = function() {
-
-        if (service.port) return;
-
-        service.initting = true;
-
-        try {
-            service.port = 
-                chrome.runtime.connect(HATCH_CONFIG.EXT_NAME_CHROME);
-        } catch(e) {
-            service.extConnectFailed(e);
-            return;
-        }
-
-        if (!service.port) {
-            service.extConnectFailed(
-                'runtime.connect() did not return a Port');
-            return;
-        }
-
-        service.port.onDisconnect.addListener(function() {
-            if (service.hatchAvailable === false) return; // already noted
-            service.hatchAvailable = null; // reset
-            service.hatchClosed();
-        });
-
-        service.port.onMessage.addListener(function(msg) {
-            console.debug('Hatch says: ' + JSON.stringify(msg, null, 2));
-
-            if (service.initting) {
-                service.initting = false;
-                console.debug("Hatch init completed with " + msg.message);
-
-                if (msg.status == 200) {
-                    service.hatchOpened();
-                } else {
-                    console.warn("Hatch init failed");
-                }
-
-            } else {
-                service.resolveRequest(msg); 
-            }
-        });
-
-        console.debug('Hatch port open, sending "init" message');
-        service.hatchAvailable = true;
-
-        // The first message to Hatch must be "init"
-        service.attemptHatchDelivery({action : 'init'});
-    }
-
     service.hatchOpened = function() {
         // let others know we're connected
         if (service.onHatchOpen) service.onHatchOpen();