IFrames and cross-domain security, part 2

Update 10/2010: A new working example with cleaned up code is available.

About six weeks ago, I wrote a post about some issues I was encountering with iframes and cross-domain security. I promised I would write about whatever workaround I decided to use; this post details that workaround. Warning: it feels more complicated than it is, and may take a while to get your head around. Once you get the gist of it, it’s actually a pretty straightforward system.

The problem

Recap of the issue

  1. I have a JavaScript-powered HTML course interface residing in our learning management system at domain A. For the purposes of this post, let’s call this domain myLMS.com.
  2. The course interface loads content stored on a website at domain B, which is a completely unrelated website and not a subdomain of domain A. Let’s call this site content.org.
  3. The interface loads the external content into an iframe; this content includes course activities that must send a completion notice to the interface.
  4. Since the content is from an external domain, JavaScript communication between the iframe and the parent frame is explicitly forbidden, meaning the content can never send the completion notice to the parent frame.

Illustration of iframe communication with parent frame

Example scenario

  1. The interface at myLMS.com loads a JavaScript-powered quiz question from content.org into the iframe.
  2. The user is not supposed to be allowed to move forward until the question is answered; this means the interface must disallow forward progress until it gets a completion notice (via JavaScript) from the quiz question.
  3. Since the quiz question is loaded into an iframe and is not from the same domain as the parent frame, it is not allowed to interact with the parent frame and can’t use JavaScript to communicate with the course interface. This means the completion notice can’t be sent to the course interface, and the learner can’t move forward.

The hack

What’s a boy to do? Make a hack, of course. 😉

After searching high and low for answers, I decided to use a nested-iframe hack. From what i can tell, it’s a fairly common workaround. Here’s how it works:

  1. Parent frame is from domain A.
  2. Content in child frame (iframe) is from domain B.
  3. Child frame loads a hidden iframe which is served from domain A. Since this nested iframe is served from domain A, it has unrestricted access to the JavaScript in the parent frame.

Iframe workaround: Nested iframe inside content iframe.

Ok, ok, it’s starting to make your head spin, right? Well, what makes it even more complicated is that the nested frame can’t communicate with the child frame, either! In fact, it all seems like a pointless exercise until you add in one crucial ingredient: a querystring in the nested iframe’s URL.

The querystring

The HTML page loaded into the nested iframe is a dedicated proxy (helper) file; it only contains JavaScript, and includes a JavaScript-based querystring parser that examines the string and executes a function based on the content of the querystring.

Here’s how the final sequence of events would look:

  1. myLMS.com loads quiz question from content.org into an iframe.
  2. When quiz question is completed, it uses JavaScript to dynamically create a nested iframe.
  3. Before loading the nested iframe, a querystring is added to the URL, such as proxy.html<strong>?result=passed</strong>.
  4. The JavaScript in the proxy HTML examines the querystring and acts accordingly. In this example, it sees that the variable “result” has a value of “passed”, so it sends a completion notice directly to the JavaScript contained in the parent frame.

The scripts

Parent frame (myLMS.com)

For this example, we’re going to assume the parent frame contains a JavaScript function named interactionCompleted.

function interactionCompleted(result){
   //Do something with result
}

Content iframe (content.org)

The content iframe, which is loaded from the external domain content.org, contains a function that creates the nested iframe element, then loads the proxy.html file with the proper querystring. You can invoke the proxy whenever you need it. In this example, it gets invoked via the setCompletion function.

function setCompletion(result){

    //The name of the frame
    var id = "proxyframe";

    //Look for existing frame with name "proxyframe"
    var proxy = frames[id];

    //Set URL and querystring
    var url = "http://myLMS.com/proxy.html?result=" +result;

    //If the proxy iframe has already been created
    if(proxy){

        //Redirect to the new URL
        proxy.location.href = url;

    } else {

        //Create the proxy iframe element.
        var iframe = document.createElement("iframe");
        iframe.id = id;
        iframe.name = id;
        iframe.src = url;
        iframe.style.display = "none";
        document.body.appendChild(iframe);

    }

}

Nested “proxy” iframe (myLMS.com)

When the content iframe creates the nested proxy frame, it appends a querystring. The proxy frame therefore needs to examine the proxy’s window.location parameter for a querystring, then act on the value of the querystring.

window.onload = function (){

    var result, pairs;
    var querystring = window.location.href.split("?")[1] || false;

    //Ensure querystring exists and has valid result identifier
    if(!querystring || querystring.indexOf("result=") === -1){ return false; }

    //Ensure all ampersands are single (not entities) so we can split using "&"
    querystring = querystring.replace(/&/g, "&");    

    //Create an array of value pairs. This gives us flexibility
    //to add more items to the querystring later.
    pairs = querystring.split("&");

    //Loop through the pairs and act on each one.
    for(var i = 0; i < pairs.length; i++){

        //We're currently only looking for the 'result' value
        //We can add more if needed by adding more 'if' and 'switch' statements

        //Find 'result' variable
        if(pairs[i].indexOf("result=")){

            //Extract the value from the string by replacing the
            //identifier/assignment portion of the string with nothing ""
            result = pairs[i].replace("result=", "");

        }

    }

    //Only act on valid values.
    //DO NOT try to use eval() here.  Big no-no.
    switch(result){

        //Must specify "top." before the function invocation so that
        //the browser knows to send the JS to the topmost parent frame

        case "passed" : top.interactionCompleted("passed"); break;
        case "failed" : top.interactionCompleted("failed"); break;

    }

};

If you remove the comments and extra line breaks, this is a very short script that weighs in at 16 lines. Also, as I mentioned in the comments, please don’t try to use eval() on the querystring; it would be very unwise and would cause major security vulnerabilities, much like an SQL injection.

Thoughts and observations

Now that I’ve covered the basics, here are some general thoughts and observations on the topic.

Does this hack work in all browsers?

I can’t guarantee anything, but in my personal tests it worked in the following browsers: IE6 (XP), IE7 (Vista), Firefox 3 (XP, Vista, OS X, Ubuntu 8.10), Safari 3.1 (XP, Vista, OS X), and Opera 9.5 (XP, Vista, OS X).

Where is the proxy.html file stored?

In my example, the proxy.html file (used for the nested iframe) must be stored on the same domain as the parent file (the course interface); the key to this workaround is that the proxy.html file has unrestricted access to the topmost parent file, which it can only have when being served from the same domain.

Do the nested iframes screw up the back button?

Yes and no. I didn’t document what happens in which browser, but I know some browsers’ back buttons are unaffected by the nested iframe, while others will require one extra ‘back’ click. I don’t know about you, but I can live with one extra back button click.

What if I have some files that aren’t on a different domain?

Sometimes you’ll have some files that are on a different domain, and some that aren’t. If you have files on the same domain, you certainly wouldn’t want to use the iframe hack — it would be completely unnecessary. In my system, I expanded on the previous example by adding a domain-matching check before invoking the proxy. It requires specifying the parent’s domain in the querystring for the content iframe. The JavaScript in the content iframe looks like this:

//Get the querystring
var querystring = window.location.href.split("?")[1] || false;

//Create boolean indicating whether domains match.
//If domains match, no need for proxy workaround.
var isSameDomain = (querystring && querystring.contains("domain=" +document.domain));

function setCompletion(result){

    if(isSameDomain && parent){
        top.interactionCompleted(result);
    } else {
        //do what's in the previous example
    }
}

document.domain can be used to find out what the current domain is for security purposes. A simple string search will tell you whether the content frame’s domain is found inside the querystring that was passed from the parent.

What if I have multiple actions I’d like to perform using the proxy?

Simple: Just add it to your querystring and build the logic into your proxy.html file’s JavaScript. You can make it as simple or complicated as you like!

Disclaimer: I have no affiliation with any real sites that may be located at mylms.com or content.org; these addresses were picked at random and only used for demonstration purposes.

Dealing with Internet Explorer in your JavaScript Code

It’s almost the end of 2008, and thanks to the hard work of web standardistas, browser vendors, and JavaScript framework developers, cross-browser JavaScript code is much less of an issue than it used to be. Even Microsoft is feeling the love — the upcoming Internet Explorer 8 will be a (mostly) clean break from legacy Internet Explorer releases and will behave much more like Firefox, Safari (WebKit) and Opera. …And they rejoiced.

So why is it that when I look under the hood of some recently produced web pages (learning management systems, courses produced by e-learning rapid development tools, general web pages, etc.), the pages’ JavaScript often includes incredibly out-of-date and bad-practice Internet Explorer detection? Check out these samples I randomly copied:


_ObjBrowser.prototype.Init = function() {
    var $nBrowserChar    = "";
    var $nBrowserStart   = 0 ;
    if ( this.$strUA.indexOf("MSIE") >= 0 ){
        this.$nBrowser = BROWSER_IE ;
        this.$nBrowserVersion = "";
        $nBrowserStart   = this.$strUA.indexOf("MSIE")+5
        $nBrowserChar    = this.$strUA.charAt($nBrowserStart);
        while ( $nBrowserChar != ";" ){
            if ( ( $nBrowserChar >= '0' && $nBrowserChar <= '9' ) || $nBrowserChar == '.' )
                this.$nBrowserVersion += $nBrowserChar ;
            $nBrowserStart++;
            $nBrowserChar     = this.$strUA.charAt($nBrowserStart);
        };
        this.$nBrowserVersion = parseInt( parseFloat( this.$nBrowserVersion ) * 100 ) ;
    } else if ( this.$strUA.indexOf("Mozilla") >= 0 ){
        this.$nBrowser        = BROWSER_MOZILLA ;
        this.$nBrowserVersion = parseInt ( (this.$strUA.substring( this.$strUA.indexOf("/") + 1,  this.$strUA.indexOf("/") + 5  )) * 100 );
    }
};

or even these simpler yet equally problematic sniffers:

UserAgent detection

if (navigator.appName &&
    navigator.appName.indexOf("Microsoft") != -1 &&
    navigator.userAgent.indexOf("Windows") != -1 &&
    navigator.userAgent.indexOf("Windows 3.1") == -1) {
        //Do something
}

Bad object detection

if (document.all) {
    //Do something for Internet Explorer 4
} else if (document.layers) {
    //Do something for Netscape Navigator 4
} else {
    //Do something for other browsers
}

These examples are BAD BAD BAD. Why? Well, there are a million web articles explaining the topic, but I’ll give you a quick rundown.

You shouldn’t test for specific browsers, but for specific functionality instead

In most cases, browser detection is being used because the developer is making an assumption that a particular browser does or doesn’t have a specific feature. The problem is that browsers change over time, and detecting for a browser based on assumptions can come back to bite you. Even the prickly Internet Explorer gets updates from time to time. Case in point: The versions of IE6 in Windows 2000 and Windows XP have a different JavaScript engine (JScript version) than IE6 running in Windows XP Service Pack 3. This means a general IE6 detection script might not lead you down the path you expected.

If you test for features instead, your code will be more future-compatible and less likely to break.

if(document.getElementById){
   //It is safe to use document.getElementById()
} else {
   //document.getElementbyId() is not supported in this browser
}

When hacks for Internet Explorer are required

Nowadays, most browsers offer roughly the same features and adhere to W3C standards. But, as we know, our friend Internet Explorer is… different. If you must use hacks custom code for Internet Explorer, know your options (and please, for the love of… something… don’t use ActiveX controls. Just don’t.).

Use cleaner IE detection

The old User-Agent sniffing approach has been abused for years. Internet Explorer 7 (Windows XP SP2) uses the following User-Agent to identify itself:

Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)

Yes, you read that correctly: IE declares itself a Mozilla browser. Opera’s User-Agent can be changed on the fly, allowing a user to set it to Opera, IE, or Mozilla! This all renders User-Agent detection useless. So what are your other IE detection options?

Conditional comments. Conditional comments are a proprietary Microsoft mechanism that is often used to include IE-specific CSS files in a page. For our discussion, you could conceivably use conditional comments to include a JavaScript file containing IE hacks in your page. You could also do a quick and dirty hack like this:


<!--[if IE]>
   <script type="text/javascript">
      var isIE = true;
   </script>
<![endif]-->

But, as you can see, it’s a clunky solution and isn’t inline with the other JavaScript, which makes it a less-than-ideal option.

Conditional compilation. This method is THE method of choice right now. Dean Edwards described this gem of an approach on his blog post Sniff. As far as I can tell, it is the cleanest and most reliable way to detect IE.

var isMSIE = /*@cc_on!@*/false;

Simple, isn’t it? No User-Agents, no 20 lines of conditional statements and string searches.

How does this code work? Internet Explorer uses JScript, a proprietary form of JavaScript (ECMAScript) developed by Microsoft. JScript has a feature called conditional compilation, which allows us to add JScript-specific JavaScript inside a specially formatted comment. Other JavaScript compilers will ignore this code, thinking it’s a comment, while JScript will recognize it and process it.

/*@cc_on begins the comment, and @*/ ends the comment. When any non-IE browser sees var isMSIE = /*@cc_on!@*/false;, it actually sees it as var isMSIE = <strong>false</strong>;. Internet Explorer’s JScript will add the exclamation point contained in the comment, meaning IE will see var isMSIE = <strong>!false</strong>;. Remember, an exclamation point reverses a boolean’s value, so !false is equivalent to true.

As Dean pointed out, there is an even simpler way to write this (though it’s slightly less legible):

var isMSIE/*@cc_on=1@*/;

If a variable is not assigned a value when it is declared, by default it will return ‘falsy‘ (null). Thus a non-IE browser will see this line as var isMSIE; while Internet Explorer’s JScript will see it as var isMSIE=1; (assigning a 1 makes the variable ‘truthy‘).

I prefer to shorten it a bit more by removing the MS from isMSIE, making it simply isIE. Note: it’s a best practice to give booleans a name that implies what the boolean stands for, usually in a quasi-question format. Using plain old var IE doesn’t tell us the variable is a boolean, and doesn’t tell us what question is being answered by the boolean’s value. var isIE does.


var isIE/*@cc_on=1@*/;
if(isIE){
   //Do something. I suggest downloading Firefox.  ^_^
}

Avoid forking your code for hacks whenever possible.

If your code can be written in a way that satisfies all browsers, do it that way, even if it veers to the left of official web standards — just be sure to document your decision in the comments. For example, using the standards-friendly setAttribute for specifying class names will fail in IE, while direct assignment will work in all major browsers.

//standards-friendly, but fails in IE
element.setAttribute("class", "myClass");

//direct assignment, works in all major browsers
element.className = "myClass";

In this case, I’m advocating using defacto standards that aren’t codified in the W3C standards, but are supported in every major browser. Some standardistas will balk, but this is a much easier way to maintain code; it’s a bad idea to use unnecessary conditional statements simply to prove that IE doesn’t always follow standards. We know, we don’t like it either, but get over it. If a non-standard but universally supported alternative is available, your extra “see I told you so” code will be pure bloat, increasing file size, bandwidth requirements, and browser processing requirements.

Don’t get me wrong: standards are important. Very important. But they only go so far; if we stuck to codified standards on principle, no one would be using AJAX today! That’s right, xmlhttprequest — the heart of AJAX — was created by Microsoft, then copied by other browsers. It is not an official standard, but it is universally supported, and is used by tens of millions of web site visitors every day.

If you’re not comfortable with non-standard code littered throughout your project, abstract it into a function so you can easily modify it later.

Old way:

element.className = "myClass";

Abstracted function:

function setClass(targetElement, name){
   /*
      not using setAttribute here because setAttribute
      won't work with class names in IE
   */
   targetElement.className = name;
}

setClass("element", "myClass");

Now whenever you need to set a class, you won’t worry about IE and can simply use your class function. If you decide to change how you assign the class name, you can just change it in the function and not have to dig around your code looking for every instance of direct assignment (className =).

Note: Many JavaScript frameworks, such as jQuery and MooTools, provide this type of support function for you. These frameworks protect you from needing to know about browser inconsistencies, and free you to focus on your application. If the above example were rewritten to use MooTools, it would simply be

element.addClass("myClass");

There are many ways to approach the problem, and all without using nasty browser detection scripts. It pays to know your options.

Use abstractions for forking code

Getting back to IE detection, if you aren’t using a framework and need to handle browser inconsistencies on your own, it’s a good idea to use support functions. These can encapsulate the browser inconsistencies and isolate them from the rest of your code. This makes your code more maintainable and readable.

Case in point: Internet Explorer will not allow you to dynamically assign a name value to an <input> element. This requires an IE-specific hack.

You would normally use the following W3C-compliant code:

var input = document.createElement("input");
input.setAttribute("name", "myInput");

In Internet Explorer, we’re forced to do this:

var input = document.createElement("<input name='myInput'>");

In your project, you’d have to use conditional statements each time you need to create an input element.

var isIE/*@cc_on=1@*/;

if(isIE){
   var input1 = document.createElement("<input name='myInput1'>");
} else {
   var input1 = document.createElement("input");
   input.setAttribute("name", "myInput1");
}
input1.setAttribute("id", "myInput1");

//Later on in your code...
if(isIE){
   var input2 = document.createElement("<input name='myInput2'>");
} else {
   var input2 = document.createElement("input");
   input.setAttribute("name", "myInput2");
}
input2.setAttribute("id", "myInput2");

What a pain. The best way to deal with this is to wrap the code in a function like so

function createInput(parentElement, id){

   //Make sure everything is supported. Error-checking is divine.
   if(!parentElement || !id || !document.createElement || !parentElement.createElement){ return false; }

   var isIE/*@cc_on=1@*/;
   var input;

   if(isIE) {
      input = parentElement.createElement("<input name='" +id +"'>");
   } else {
      input = parentElement.createElement("input");
      input.setAttribute("name", id);
   }

   input.setAttribute("id", id);
   return input;

}

As you can see, the code is now easily reusable throughout our project, contains better support for IE detection, and also contains feature detection ensuring our function won’t throw any errors. To use the code in the project, you’d simply write:

var input1 = createInput(document, "myInput1");
var input2 = createInput(document, "myInput2");

In closing

I hope you’ve found this post helpful, and I especially hopes it helps spread the word about using best practices and cleaner code techniques. I’ll be the first to say I’m no programming expert, so feel free to add a comment if you have suggestions for improvements. 🙂

PS: If you have to write a conditional statement, follow Crockford’s advice and wrap it in curly braces.

Free web-based word processors

A friend of mine doesn’t have Word, and has been trying to use Google Docs for all her word processing needs. I’ve barely touched Google Docs, so I’ve never had much of an opinion. However, after trying to help her edit and format one of her files tonight, I must say it’s something of a clunker! (At least when it comes to formatting.)

Here are some great web-based alternatives to Word and Google Docs I just shared with her:

  • Thinkfree’s Write: an amazingly well-done (and purely Web-based) MS Word knockoff.
  • Zoho’s Writer: feels like Google Docs but fancier.
  • Adobe’s Buzzword: Part of the new Acrobat.com. Sure to have good integration with the PDF format.

If you don’t mind skipping web-based services, you can also download and install the free open-source and standards-based Open Office 3 suite, which is very MS Office-like, and is compatible with Office, too. It includes spreadsheet (Excel), database (Access), and presentation (PowerPoint) tools, all weighing in under 200MB, which is considerably less bloat than MS Office.

I’m sure there are other free apps/web services out there, but these are probably the most popular and well-supported of the bunch.

Installing Parallels Tools (v4) in Ubuntu 8.10

I had a hard time sorting this out, so I figured I’d post it in case anyone else needs to know.

Assumptions: You have Parallels v4 for Mac, and have already created an Ubuntu 8.10 image.

Instructions:

  1. Launch your Ubuntu image (in window mode).
  2. Go to Virtual Machine > Install Parallels Tools

    Virtual Machine - Install Parallels Tools
  3. A dialog box appears. It contains vague instructions that aren’t very helpful for Linux newbies (hence this blog post). Ignore the instructions and click “continue”.

    Installer dialog
  4. A CD icon appears on the desktop, and the window opens displaying the CD contents. You won’t be clicking anything in this window, so go ahead and close it.

    CD icon appears on desktop
  5. Launch the Terminal by going to Applications > Accessories > Terminal

    Launch Terminal
  6. Next run the installer by typing sudo sh /cdrom/install and pressing the return button on the keyboard. You will be prompted to enter your system administrator password.
  7. The Parallels Tools installer appears. Follow the instructions.

    Parallels Tools Installer Screen

That’s it! You’ll need to reboot the virtual machine, and then you’re good to go.

SWFObject.js finds a home on Google servers

Cool news for SWFObject users: the swfobject.js file is now being hosted on Google’s servers as part of the AJAX Libraries API. This means you can directly link to the file on a Google server instead of maintaining a copy on your own server.

The benefits of using this service are many, with my favorites being:

  • You will not need to maintain your own copy of the library (which for many developers means we don’t have to maintain multiple copies spread all over the place).
  • Load time should be very fast because A) Google has speedy servers, B) Google’s versions are minified, and C) Google’s versions are properly gzipped.
  • Downloading from Google’s server benefits your page’s load time because if other people also use Google’s API service, there’s a good chance the swfobject.js file is already cached in your browser and won’t need to be downloaded again.

The AJAX Libraries API also hosts other script files, most notably the JS frameworks MooTools, jQuery and Dojo. There’s a great intro to the AJAX Libraries API by Dion Almaer at ajaxian.com.

Bobby Van der Sluis has updated the SWFObject documentation to include instructions for using the hosted version of swfobject.js.

Note: I have updated all of my SWFObject 2.0 examples to use the AJAX Libraries API; if you encounter any examples that don’t work properly, please let me know.


Adobe E-Learning Products “Sneak Peeks”

Today’s Adobe Summit had a session named “Sneak Peeks.” It was (unofficially I think) mentioned that the Adobe E-Learning Suite is coming, and will include Captivate 4, Flash, Photoshop, Acrobat Professional, Device Central, and more.

Here’s a quick list of topics covered.

Versions of Dreamweaver and Flash in E-Learning Suite will NOT be same as those in CS4 suites, and will include e-learning specific bits.

No date for E-Learning Suite given; will only say 2009.

Captivate 4 will include:

  • Automatic panning that follow your screen actions.
  • Previewing in Device Central
    • Allows you to preview on an actual mobile device
    • Allows you to preview with fake screen reflections
  • Inline text editing for captions (no more dialog boxes)
  • Basic drawing tools (shapes)
  • Integration with Adobe Bridge
  • Import > Photoshop files (PSD)
    • Can flatten layers or choose to have them import to separate layers
    • Converts each layer to hi-res PNG
    • Allow syou to animate layers individually on a single slide
  • Support for custom variables, such as using a person’s first name throughout the course
    • Uses $$variablename$$ syntax
    • Means you can use custom static variables throughout a course
    • You can customize an external RDL file to templatize variable use; means you can instruct Captivate to automatically insert variables when recording a demo without having to manually edit the recording afterwards
  • Support for Flash widgets
    • Will work much like components in Flash Professional
    • Captivate 4 will ship with many widgets (including source FLA)
    • Widgets can be customized, and users can create own from scratch
    • Can talk to Captivate (including quizzes) and retrieve variables
    • Example: certificate widget can display person’s name, score for course, date, etc.
    • Example: “perpetual buttons” widget that customizes navigation (forward/back) for movie; hides “back” biutton when on first slide, hide “next” button when on last slide
    • Captivate Exchange on Adobe site will be available for users to post and download custom widgets
  • Support for multiple actions on a single slide
  • Support for ActionScript 3 (can publish to AS2 or AS3 depending on user settings)
  • Can create image slideshow
  • Ability to show/hide toolbar during a course without using customizations
  • Captivate 4 will be on Windows, but work will soon begin on a Mac version
  • Can publish directly to PDF (embeds SWF into new PDF file)
  • Single-SWF output (all files are embedded into single SWF, including nav and full-motion recordings)
  • Auto-generation of Table of Contents (embedded into SWF)
  • SWFs are searchable (text caption content is searchable)
  • “Aggregator” feature allows you to add external Captivate SWFs to project (package multiple SWFs together… is a SWF that loads other SWFs)
  • Supports Flash Player 7, 8, 9 & 10
  • “Reviewer” feature allows users without Captivate to comment on a Captivate SWF
    • Uses Adobe AIR
    • Synchronizes comments form multiple reviewers
  • Supports placeholders to demonstrate where content will be, such as an FLV that isn’t available yet.
    • Helpful when using Reviewer feature so others can know what you have planned
    • Placeholders make it easy to insert content when it becomes available
  • Improved Support for PowerPoint imports
    • Support for dynamic link to PowerPoint presentation — if PowerPoint file is updated, changes wil be reflected in Captivate file
    • Makes PPT file a smart object; you can open and edit PPT file via Captivate (just like how smart vector objects in Photoshop open in Illustrator)