How to communicate between JavaScript sandboxes

iOS 8 introduced a great new development API called Action Extensions. Action Extensions are basically a safe way to extend certain apps in iOS 8 using JavaScript and Swift (Obj-C). The same concept applies with Chrome Extensions since they share similar security models for Content Scripts/Action Extensions.

One month prior to WWDC, I was one of the lucky developers invited by Apple (on behalf of Microsoft) to create a complex Action Extension. The extension I created was showcased in the WWDC keynote, Bing Translate. In this post, I will explain how Action Extensions are no different than Bookmarklets but with limitation, and how we can communicate between the protected JavaScript sandboxes.

I have published the JavaScript portion as open source, you can fork it over github.com/mohamedmansour/bing-translate/

What are Action Extensions #

They are basically bookmarklets sandboxed within the iOS app you bundle it with. Completely Isolated from the JavaScript running on that webpage. The two thing Action Extensions differ from Bookmarklets is that it exposes two interface methods to allow communication to native code behind and the extension does not share the same javascript context of the webpage. Similar to content scripts in Chrome. The exposed methods are: run(completionArgs and finalize(objArgParams). This sequence diagram shows how the communication between native code and the JavaScript code you inject.

CommunicationiOSJavaScriptResize.png

I wont explain how to create an Action Extension, you can refer how to create one by following the documentation over at developer.apple.com. The run method gets called as soon as you tap on the Action Extension icon from the sheet. And finalize gets called when you completed the Action UI Sheet. If your Action Extension has no User Interface, then it will call finalize from native.

The JavaScript that iOS injects in the webpage is sandboxed. That Action Extension JavaScript cannot communicate to the JavaScript on that webpage.

How Bing Translate Extension works #

When the user hits “Bing Translates” from the action sheet, it will bring up a view where they could input the “from” language and the “to” language. The “from” language will be populated by the detected language on the webpage by the Bing Translate Detection API. When they hit the “Translate” button, it will call the Bing Translate API to translate the webpage inline.

The main problem here is that there is no easy way to let the Action Extension (the script we injected into the webpage from the extension) communicate to the Bing Translate API. When you inject JavaScript from the bookmarklet, the context will only be available in the Browser not the extension. The Bing Translator API scripts creates a global variable named Microsoft. which is only available in the Browser Sandbox side not the extension sandbox. It is undefined in the extension sandbox. They are completely two isolated Sandboxes and the only thing they share is the DOM.

How to communicate between Sandboxes #

It is best to explain how it works by showing this sequence diagram.

ArchitectureIosExtensions.png

The only thing common between the Sandboxes is the DOM and Custom Events. The trick I use is basically you create a hidden <div> element that will act as a transfer bucket. You inject JavaScript code in the Browser Context through creating <script> elements:

function injectScript(fn) {
    var script = document.createElement('script');
    script.appendChild(document.createTextNode('(' + fn + ')();'));
    document.body.appendChild(script);
}

function someScripToInjectIntoBrowserContext() {
    (function() {
        console.log("Hello from Browser Context");
    })();
}

// Injects some code into the Browser Context
injectScript(someScripToInjectIntoBrowserContext);

Where both context’s listen to the custom events. You define a shared protocol that both onMessageReceived events can understand. When you want to send data to the other Sandbox, you JSON.stringify the data you want to be sent, place it in the transfer DOM, and just dispatch the custom event that the other context understands.

Gist: gist.github.com/mohamedmansour/dc0c56820015e7f

// Append the transfer dom to the document.
var transferDOM = document.createElement('div');
transferDOM.style.display = 'none';
transferDOM.id = 'transferdom';
document.body.appendChild(transferDOM);

function injectScript(fn) {
    var script = document.createElement('script');
    script.appendChild(document.createTextNode('(' + fn + ')();'));
    document.body.appendChild(script);
}

function someScripToInjectIntoBrowserContext() {
    (function() {
       var transferDOM = document.getElementById("transferdom");
       window.Microsoft = "assigned";
       console.log("Browser Context : window.Microsoft = " + window.Microsoft);
       window.addEventListener('ExtensionContextEvent', onMessageReceived);

       // Script is ready inform the extension context.
       function dispatch(msg) {
           console.log("Browser Context : Dispatching: " + msg);
           transferDOM.innerText = msg;
           var transferEvent = document.createEvent('Event');
           transferEvent.initEvent('BrowserContextEvent', true, true);
           window.dispatchEvent(transferEvent);
       }

       dispatch("ready");

       function onMessageReceived() {
           var data = transferDOM.innerText;
           console.log("Browser Context : DataReceived : " + data);
           dispatch("Microsoft = " + window.Microsoft);
       }
    })();
}

function dispatch(msg) {
       console.log("Extension Context : Dispatching: " + msg);
       transferDOM.innerText = msg;
       var transferExtensionEvent = document.createEvent('Event');
       transferExtensionEvent.initEvent('ExtensionContextEvent', true, true);
       window.dispatchEvent(transferExtensionEvent);
}

window.addEventListener('BrowserContextEvent', onMessageReceived);
function onMessageReceived() {
    var data = transferDOM.innerText;
    console.log("Extension Context : DataReceived : " + data);

    if (data == 'ready') {
       // Dispatch an event to fetch the Microsoft variable.
       dispatch("fetch");
    }
}
console.log("Extension Context : window.Microsoft = ", window.Microsoft);

// Injects some code into the Browser Context
injectScript(someScripToInjectIntoBrowserContext);

The console will look like this:

Extension Context : window.Microsoft =  undefined
Browser Context : window.Microsoft = assigned
Browser Context : Dispatching: ready
Extension Context : DataReceived : ready
Extension Context : Dispatching: fetch
Browser Context : DataReceived : fetch
Browser Context : Dispatching: Microsoft = assigned
Extension Context : DataReceived : Microsoft = assigned

Conclusion #

So that is how the Bing Translate extension works. We wanted Bing Translate API to get the detected language and show it on the Action Sheet UI. To do that, we have to use custom events and transfer DOM to communicate to the Browser Context and get back the detected language to the Extension Context because that is the only way to communicate back to iOS8. You can test the Bing Translate in any browser (IE, Safari, Firefox, Chrome), just copy paste the whole thing and paste it the web inspector.

 
70
Kudos
 
70
Kudos