iFrame: sending data back and forth - dan7davis/Lambda GitHub Wiki

Wanted

Imagine an intervention (e.g. a study planner or a search engine) that is not added directly as custom HTML element to one or more edX pages in a course, but instead added as an <iframe> element. edX supports iframes and in general this is a good idea to make writing and testing code much easier (no constant copy and pasting of code into a custom edX HTML element - tedious and not easily testable).

What's the problem?

iframes look great on first sight, however they have the issue of lack of access to edx data.

What if we build an application that requires data to be exchanged between edx and the iframe (e.g. click data, user profile data, discussion forum data)?

CORS

The first thing to try is a standard ajax request from within the iframe. If for instance our iframe wants access to some edx content (e.g. the search engine should also display search results from the edx forum), the browser that makes the HTTP request needs to send the edx cookie (which contains the authentication token) along with the request from the iframe; if the cookie is not send along, the edx content cannot be accessed. As the iframe is not hosted on the edx.org server, this requires Cross-Origin Resource Sharing or CORS. Withouth CORS, the browser still executes the request from the iframe, but for security reasons the browser does not send the edx cookie along (without the cookie no edx content can be accessed). Visually:

CORS diagram

The server (i.e. the edx.org server) needs to be configured to allow CORS, by default it is disabled. Whether or not a server supports CORS is visible in the HTTP header sent in response to an HTTP request to the server: if the header Access-Control-Allow-Credentials is set to true, CORS is allowed; if the header is not sent, it defaults to false and CORS is disabled. Right now, edx.org does not set this header and thus easy use of an iframe is not possible (once more: if the iframe needs to access data from edx.org).

window.postMessage

window.postMessage is part of the HTML5 API that can be used to send messages between windows/frames (in our case the browser window showing edx.org content and the embedded iframe with our intervention) across domains. Most importantly, this happens on the client-side, it does not matter what kind of security restrictions the server has. The idea is as follows: we embed a piece of JavaScript through a custom HTML element on our edx course. This piece of JavaScript can then make the call to edx.org, receive the data and send it via window.postMessage to the iframe. This code is only triggered when the iframe sends a message through window.postMessage that requests it. The advantage is lack of server involvement, the disadvantage is that two pieces of code have to be maintained: the script embedded in our custom HTML element and the piece of code within the iframe.

Concretely, lets assume a search engine wants to show edx forum search results in the iframe. Embedded is the following custom HTML snippet (via <script>..</script>) on the edx page:

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event)
{
  	console.log("[edx page] Message received, query is: "+event.data);
  	console.log("[edx page] Querying the edx forum ... ");

        //the query was sent by the iframe, lets construct a call to edx forum search
        //note that this will only work with this url from within an edge.edx.org course,
        //adapt the url if a live edx course is used
  	var query = event.data;
	var url = "https://edge.edx.org/courses/course-v1:DelftX+DD.003x+2017/discussion/forum/search?ajax=1&format=json&text="+query;
    
        //make an ajax request, if that is successful use postMessage to send the result (json) to the iframe
	$.ajax({url: url, 
          success: function(result){
          console.log("[edx page] Sending data to pienapple: "+result);
          var objString = JSON.stringify(result);
          console.log("[edx page] object JSONIFIED: "+objString);
          event.source.postMessage(objString,event.origin);
    }});
}

Inside the iframe we have a similar setup, with a listener on the window, waiting for incoming messages:

window.addEventListener('message', function (event) {
    console.log("[pienapple-front]" + event.data);
    console.log("[pienapple-front]" + event.origin);

    //are we receiving events from the right source?
    if (event.origin != "https://edge.edx.org" && event.orgin != "https://edx.org")
        return;
    
    var json = JSON.parse(event.data);
    
    var entries = json.discussion_data.length;
    console.log("[pienapple-front] Number of discussion entries: " + entries);

    for (let i = 0; i < entries; i++) {
        var t = json.discussion_data[i].title;
        var b = json.discussion_data[i].body;

        //the absolute URL of the discussion entry needs to be reconstructed (TODO: test this bit!)
        var u = event.origin + "/courses/" + json.discussion_data[i].course_id + "/discussion/forum/" + json.discussion_data[i].commentable_id + "/threads/" + json.discussion_data[i].id;

        console.log("[pienapple-front] " + t);
        console.log("[pienapple-front] " + b);
        console.log("[pienapple-front] " + u);
    }
}, false);

This works in general fairly well, though some browsers (e.g. Firefox) may not work as expected immediately.

Troubleshooting

  • Is your iframe included in edx with a https prefix (http does not work for security reasons)?
  • If messages don't make it to the parent/child window, check whether postMessage has the correct domain (including sub-domain) set; there is a difference between edx.org and edge.edx.org
⚠️ **GitHub.com Fallback** ⚠️