Cross-Document Messaging

As a general rule, scripts loaded by web content served from one origin (host and domain) cannot access web content served by a different origin. This is an important security feature that prevents a multitude of different security attack vectors. However, it also makes it difficult for scripts to interact with one another across these boundaries.

To make communication between documents from different origins easier, the HTML 5 specification adds cross-document messaging. This feature is supported in Safari 4.0 and later.

Posting a Message to a Window

To post a message, you must first obtain the Window object of the document you want to message. In effect, this means that you can post messages only to:

Once you have obtained the Window object for the target document, you can send it a message with the following code:

windowObj.postMessage('test message', 'http://example.com');

The first parameter is an arbitrary message.

The second parameter is the target origin value. An origin value is just a URL with the path part removed. For example, the origin of a local file is file://. By specifying a target origin, you are saying that that your message should only be delivered if the target window’s current contents came from that origin.

Although you may specify an asterisk (*) wildcard for the target origin (to allow the message to be sent regardless of where the contents of the target window came from), you should do so only if you are certain that it would not be harmful if your message were received by content originating from a different website.

Receiving a Message Posted to a Window

To receive messages, you must add an event listener for the message event type to your document’s window object. To do this, use the following code:

function messageReceive(evt) {
    if (evt.origin == 'http://example.com') {
        // The message came from an origin that
        // your code trusts.  Work with the message.
        alert('Received data '+evt.data);
 
        // Send a message back to the source:
        evt.source.postMessage('response', evt.origin);
    } else {
        alert('unexpected message from origin '+evt.origin);
    }
}
window.addEventListener('message', messageReceive, false);

The message event you receive has three properties of interest:

A Service Discovery Example: Message Boxes

This section contains two code listings: index.html and msg_contents.html that, when combined, implement basic service discovery and data relaying on top of the cross-document messaging architecture.

To use this code, you must first do the following things:

Once you have completed these steps, you should see several boxes, one per entry in the boxes object. Each of these boxes should contain a small form with a text input box, a series of checkboxes (one for each of the outer boxes), and a submit button.

If you type something into the text box, check one of the checkboxes, and click submit, the text should appear at the bottom of the window whose name corresponds with the checkbox.

Listing 1  Cross-document messaging example: index.html

<html><head>
<script language='javascript' type='text/javascript'><!--
/*global alert, navigator, document, window */
 
 
var window_list = [];
var origin_list = [];
 
var boxes = {
    local: "http://host1.domain1.top/messages2/msg_contents.html",
    remote: "http://host2.domain2.top/messages2/msg_contents.html",
    third: "http://host3.domain3.top/messages2/msg_contents.html"
};
 
var allowed_origins = {
    'http://host1.domain1.top': 1,
    'http://host2.domain2.top': 1,
    'http://host3.domain3.top': 1
};
 
function smartsplit(string, pattern, count)
{
    // alert('string '+string);
    // alert('pattern "'+pattern+'"');
    // alert('count '+count);
 
    var lastpos = count - 1;
 
    var arr = string.split(/ /);
    // alert('AC: '+arr.length+" "+string);
    if (arr.length > lastpos) {
        var temparr = [];
        for (var i=lastpos; i<arr.length; i++) {
            temparr[i-lastpos] = arr[i];
            arr[i] = undefined;
        }
        arr[lastpos] = temparr.join(pattern);
    }
    return arr;
}
 
function listWindows()
{
    var retstring = "";
    for (var i in origin_list) {
        if (origin_list.hasOwnProperty(i)) {
        // alert('UUID: '+i+' Origin: '+origin_list[i]);
        retstring += i+' '+origin_list[i]+'\n';
        }
    }
    return retstring;
}
 
 
function messageReceive(evt) {
    var windowlist;
 
    if (evt.origin === null || evt.origin in allowed_origins) {
    var arr = smartsplit(evt.data, " ", 2);
 
    if (arr[0] == 'sendto') {
        // usage: sendto UUID remote_origin message
        arr = smartsplit(evt.data, " ", 4);
        var remote_window = window_list[arr[1]];
        remote_window.postMessage('sendto_output '+arr[3], arr[2]);
    } else if (arr[0] == 'register') {
        // usage register UUID
        var name = arr[1];
 
        if (window_list[name]) {
            // name conflict.
            var add=1;
            while (window_list[name+'_'+add]) {
                add++;
            }
            name = name+'_'+add;
            evt.source.postMessage("register_newid "+name, evt.origin);
        }
 
        window_list[name] = evt.source;
        origin_list[name] = evt.origin;
        windowlist = listWindows();
        for (var windowid in window_list) {
            if (window_list.hasOwnProperty(windowid)) {
            // alert('windowid: '+windowid);
            window_list[windowid].postMessage("list_output "+windowlist, origin_list[windowid]);
            }
        }
    } else if (arr[0] == 'list') {
        // usage list
        windowlist = listWindows();
        evt.source.postMessage("list_output "+windowlist, evt.origin);
    } else {
        alert('unknown command '+arr[0]);
    }
    } else {
        alert('unexpected message from origin '+evt.origin);
    }
}
 
function dosetup()
{
    if (navigator.userAgent.match(/Safari/)) {
        var version = parseFloat(navigator.userAgent.replace(/.*AppleWebKit\//, "").replace(/[^0-9.].*$/, ""));
 
        if (version < 528) {
            alert('WebKit version '+version+' does not support this application.');
        }
 
    }
    // for (var i=0; i<nchannels; i++) {
        // alert('setup channel '+i);
        // channel[i] = new MessageChannel();
    // }
    window.addEventListener('message', messageReceive, false);
 
    var boxstr = "";
    for (var i in boxes) {
        if (boxes.hasOwnProperty(i)) {
        boxstr += "<iframe height='600' id='"+i+"' src='"+boxes[i]+"'></iframe>\n";
        }
    }
    var boxlistdiv = document.getElementById('boxlist');
    boxlistdiv.innerHTML = boxstr;
}
 
dosetup();
 
--></script>
</head>
<body onload='dosetup();'>
<div id='boxlist'>
</div>
</body>
</html>

Listing 2  Cross-document messaging example: msg_contents.html

<html><head>
<script language='javascript' type='text/javascript'><!--
/*global alert, document, window, navigator */
 
var allowed_origins = {
    'http://stavromula-beta.apple.com': 1,
    'http://holst.apple.com': 1
};
 
var received_root_origin = '';
 
var root_origin = '*';
 
function smartsplit(string, pattern, count)
{
        // alert('string '+string);
        // alert('pattern "'+pattern+'"');
        // alert('count '+count);
 
        var lastpos = count - 1;
 
        var arr = string.split(/ /);
        // alert('AC: '+arr.length+" "+string);
        if (arr.length > lastpos) {
                var temparr = [];
                for (var i=lastpos; i<arr.length; i++) {
                        temparr[i-lastpos] = arr[i];
            arr[i] = undefined;
                }
                arr[lastpos] = temparr.join(pattern);        }
        return arr;
}
 
function mkcheckbox(inpstr)
{
    var str = "";
 
    var arr = inpstr.split("\n");
    for (var entid in arr) {
        if (arr.hasOwnProperty(entid)) {
        var ent = arr[entid];
        if (ent !== "") {
            // alert('ent: '+ent);
            var bits = smartsplit(ent, " ", 2);
            str += "<input type=checkbox name='"+ent+"'>"+bits[0]+"</input>\n";
        }
        }
    }
    return str;
}
 
function messageReceive(evt) {
    if (evt.origin === null || evt.origin in allowed_origins) {
 
        // The message came from an origin that
        // your code trusts.  Work with the message.
 
        // alert('Received data: '+evt.data);
    // var tmp = 'Test this, please';
    // var x = smartsplit(tmp, " ", 2);
    // alert('x[0] = '+x[0]);
    // alert('x[1] = '+x[1]);
    // alert('x[2] = '+x[2]);
 
    var arr = smartsplit(evt.data, " ", 2);
    if (arr[0] == 'list_output') {
        if (evt.origin == root_origin || root_origin == '*') {
        var div2 = document.getElementById('temp2');
        div2.innerHTML = mkcheckbox(arr[1]);
        received_root_origin = evt.origin;
        // alert('arr[1] = '+arr[1]);
        } else {
        alert('received list_output message from unexpected origin: '+evt.origin);
        }
    } else if (arr[0] == 'sendto_output') {
            // alert('Received data: '+evt.data);
        var div3 = document.getElementById('temp3');
        div3.innerHTML += arr[1]+'<br />\n';
    } else if (arr[0] == 'register_newid') {
        var mydiv = document.getElementById('temp');
        mydiv.innerHTML = arr[1]+" box";
    }
 
        // Send a message back to the source:
        // evt.source.postMessage('response', evt.origin);
    } else {
        alert('unexpected message from origin '+evt.origin);
    }
}
 
function setup_listener()
{
    window.addEventListener('message', messageReceive, false);
}
 
function setup()
{
    var mydiv = document.getElementById('temp');
 
    // mydiv.innerHTML = 'Test';
    // mydiv.innerHTML = ' '+bigdoc;
 
    // If we can access the document object, we are locally loaded and should
    // talk to the remotely-loaded window.  Otherwise, the reverse
    // is true.
 
    var myid = '';
    if (window.parent.document) {
    myid = 'local';
    } else {
    myid = 'remote';
    }
 
    mydiv.innerHTML = myid+" box";
    // window.addEventListener('message', messageReceive, false);
    setup_listener();
 
    var topwindow = window;
    while (topwindow.parent && (topwindow.parent != topwindow)) { topwindow = topwindow.parent; }
 
    topwindow.postMessage('register '+myid, root_origin);
    // window.parent.postMessage('list', '*');
 
}
 
function sendmsg()
{
    var message = document.getElementById('sendtext').value;
    var mydiv2 = document.getElementById('temp2');
    var checkboxes = mydiv2.children;
 
    // alert('checkboxes: '+checkboxes);
    var topwindow = window;
    while (topwindow.parent && (topwindow.parent != topwindow)) { topwindow = topwindow.parent; }
 
    for (var boxid in checkboxes) {
        if (checkboxes.hasOwnProperty(boxid)) {
        var checkbox = checkboxes[boxid];
 
        // alert('checkbox: '+checkbox);
        if (checkbox.tagName == "INPUT") {
            // alert('input');
            if (checkbox.checked) {
                // alert('checked');
                var arr = smartsplit(checkbox.name, " ", 2);
                var uuid = arr[0];
                var origin = arr[1];
                // alert('send to: '+uuid+' origin '+origin);
 
                // alert('topwindow is '+topwindow);
                topwindow.postMessage('sendto '+uuid+' '+origin+' '+message, received_root_origin);
            }
        }
        }
    }
 
 
    return false;
}
 
 
--></script>
</head><body onload='setup();'>
<div id='temp'></div>
<form onsubmit='return false;'>
<input type='text' id='sendtext'></input>
<input type='submit' value='submit' onclick='sendmsg();'></input>
<div id='temp2'></div>
</form>
<div id='temp3'></div>
 
</body></html>

You can create numerous interesting extensions on top of this sort of design. For example, you might add a command message that asks the window at the other end what commands it supports, then communicate with it if you share a common set of commands. The possibilities are limitless.

Security Considerations

There are several key things you should be aware of when using cross-document messaging:

As with any software, for maximum reliability and security, you should write your output code carefully to minimize the risk of causing problems for other code, and you should write your code under the assumption that other code is maliciously trying to attack your code, and thus you should perform type, bounds, and other sanity checks accordingly.