Posted
Filed under Scorm

Overview

SCORM content interoperability depends on the ability of SCORM content objects to discover a SCORM API instance provided by the LMS in a related frame of window. Sometimes the discovery algorithm fails in FireFox because of an unexpected side effect of the FireFox scripting security implementation. This can happens if a SCO is launched in a mixed window or frame environment in which the discovery algorithm stumbles on a window with content from another domain. This document describes an improved API discovery scripting technique that prevents this problem without compromising functionality or security.

The generic API discovery script

The basic SCORM API discovery algorithm is well specified in IEEE 1484.11.2 and in the SCORM Runtime Environment book. The same ECMAScript algorithm works for SCORM 2004 and SCORM 1.2, except for the fact that the name of the API object is different. In SCORM 1.2 the object is named "API" and in SCORM 2004 it is named "API_1484_11". In the rest of this document, we will assume SCORM 2004. While there have been proposals for "improvements" to the algorithm as it is implemented in the informative script samples provided in the IEEE standard and the SCORM document, such improvements do not modify the basic algorithm.

The algorithm searches the window context in which the SCO has been launched. If first looks at the parents, if any, of the frame in which the SCO might have been launched. If it does not find the API object there, it looks at the opener of the window in which the SCO has been launched and up its parent chain. This works very well most of the time. However, in some rare and obscure circumstances, the generic script will fail in the FireFox browser.

The failure case

The conditions under which the generic script fails are

  • The browser is FireFox, and
  • The search algorithm stumbles upon a window that displays content from another domain before it finds the window that contains the API object.

When both conditions are met, the Firefox script fails silently and completely. The FireFox JavaScript console shows an uncaught exception because "Window.API_1484_11" is not allowed. This is not displayed as a typical JavaScript error with a line number. Close inspection of the code shows that this happens when the window that is being inspected displays content from a different domain. Instead of returning null when the existence of a non-existent property of the "foreign" window is checked, FireFox triggers a fatal script exception. While this makes sense as a way to discourage "probing" cross-scripting exploits, it also makes life more difficult for legitimate content objects that can dynamically adapt to different environments.

This is an obscure bug, and one that can be difficult to even recognize or diagnose. The conditions where the search algorithm might encounter a "foreign" window are probably extremely rare in e-Learning environments. But people are showing more interest in using SCORM content in environments that are richer than the traditional CBT and that may gather content and resources from various sources. This might be through embedding in portals or simulations, or by mixing and matching resources in a Web 2.0 "mash-up". Also, SCOs are sometimes designed so that they can be used with or without a LMS, maybe for performance support or demonstration purposes. This FireFox "feature" and its effect on a SCO was actually discovered in the context of such a demonstration. In that demonstration, no LMS was expected. Initialization of some other features of the SCO failed as FireFox stopped script execution while the SCO was attempting to check whether a SCORM API was available. This bug was discovered by inspecting web server logs and wondering why another file which the SCO references in a normal run was never being loaded.

The fix

The fix for this problem, at the ECMAScript level, is quite simple but still requires some careful coding to avoid introducing new bugs. Basically, it consists of using a try/catch control structure when testing a window for presence of the API object. This in turn requires adding some complexity to the algorithm, as in the example below.

Consider the following generic discovery script:

var gAPI = null; // Stand-in for the API object, if found.

function _ScanForAPI(win)
{
  // This function is called by GetAPI
  var nFindAPITries = 500;
  while ((win.API_1484_11 == null)&&
         (win.parent != null)&&(win.parent != win))
  {
    nFindAPITries--;
    if (nFindAPITries < 0) return null;
    win = win.parent;
  }
  return win.API_1484_11;
}

function GetAPI(win)
{
  // Sets gAPI to be a reference to the API object provided by the RTE,
  // or leave it as null if no API object could be found.
  // Parameter win is the SCO's window
  if ((win.parent != null) && (win.parent != win))
  {
    gAPI = _ScanForAPI(win.parent);
  }
  if ((gAPI == null) && (win.opener != null))
  {
    gAPI = _ScanForAPI(win.opener);
  }
}

FireFox fails in the _ScanForAPI function when it is asked to evaluate win.API_1484_11 if doing so leads to an uncaught security exception. There is a risk of failure if any of the window.xxx calls touches a "foreign" window. So, the fix involves catching and handling the exception if it occurs, as follows:

var gAPI = null; // Stand-in for the API object, if found.

function _ScanForAPI(win) // Revised to handle x-scripting errors
{
  // This function is called by GetAPI
  var nFindAPITries = 500; // paranoid to prevent runaway
  var objAPI = null;
  var bOK = true;
  var wndParent = null;
  while ((!objAPI)&&(bOK)&&(nFindAPITries>0))
  {
    nFindAPITries--;
    try { objAPI = win.API_1484_11; } catch (e) { bOK = false; }
    if ((!objAPI)&&(bOK))
    {
      try { wndParent = win.parent; } catch (e) { bOK = false; }
      if ((!bOK)||(!wndParent)||(wndParent==win))
      {
        break;
      }
      win = wndParent;
    }
  }
  return objAPI;
}

function GetAPI(win) // Revised to handle x-scripting errors
{
  // Sets gAPI to be a reference to the API object provided by the RTE,
  // or leave it as null if no API object could be found.
  // Parameter win is the SCO's window
  var wndParent = null;
  var wndOpener = null;
  try { wndParent = win.parent; } catch(e) { }
  try { wndOpener = win.opener; } catch(e) { }
  if ((wndParent != null) && (wndParent != win))
  {
    gAPI = _ScanForAPI(wndParent);
  }
  if ((gAPI == null) && (wndOpener != null))
  {
    gAPI = _ScanForAPI(wndOpener);
  }
}

And that is all there is to it. For SCORM 1.2, just substitute "API" for "API_1484_11".

Design notes: The structure of the new functions is obviously more complex, to ensure a clean break in case an exception gets triggered by cross-scripting restrictions when a "foreign" window is inspected. I considered replacing "(win.API_1484_11)" in the original function with a call to a function that would do the testing with try/catch, e.g. something like "(testForAPI(win))" but then I realized that for all we know FireFox might also add an uncaught exception for "(win.parent)" even if it does not seem to do it now. The revision above does not depend on another function, and it never encounters the "win.parent" situation even if it were to become invalid, because it stops testing the window as soon as an error is detected in "win.API_1484_11". This is "defensive" scripting.

Discussion

This solution does impose some constraints. However those constraints should not be an issue for most deployments.

  • If any "foreign" window is encountered while walking the windows, the discovery script will not search beyond that window in the window hierarchy that is being inspected. However, I could not come up with any realistic scenario where this would cause a problem, or with a robust solution to "skip over" a window if the browser decides that the window is not accessible due to cross-scripting restrictions.
  • This solution only works with "modern" browsers that implement the try/catch feature of ECMAScript. This should not be a problem since, for security reasons, older browsers should not be deployed in a real world situation. It might however be an issue with some assistive technology browsers that use a more primitive JavaScript engine.

If it is not possible to fix up the SCOs or to use up to date browsers, the infrastructure-level workaround is always available. It consists of drawing the SCORM content, the LMS and any other content used in the same browser context from the same server, or rather from what appears to the browser as being a single server, even if they actually come from different origin servers. This is typically done through the use of a reverse proxy.

Where it is not possible to fix up existing SCOs, another workaround is through the use of "wrapper SCO" that plays the SCO in a frame, and that contains a relay API object that is initialized by an error-handling discovery script. The actual SCO's discovery script will find this relay API first and stop there, never causing an error.

Conclusion

This is an obscure bug that has the potential for causing various levels of grief and mysterious failures. I would recommend to not worry overly about it when dealing with existing content unless the bug already causes problem. However, new content should use an updated, defensive discovery scripting approach to prevent uncaught script exceptions.

This scripting strategy allows a SCO to run anywhere without risk that the initialization scripts will be rudely stopped. Even if the script is stopped silently, there is usually more initialization to be done after discovering whether or not an API is available.

2009/12/22 19:14 2009/12/22 19:14