The XMLHttpPRequest
object was introduced IE5 as a way for web applications to download content outside of traditional page navigations. It was quickly adopted by browser vendors and web developers in the its use in conjunction with markup and script was given the term AJAX (Asynchronous JavaScript and XML). Since then, the XMLHttpRequest
object and AJAX have become synonymous with dynamic websites, allowing websites to offer functionality and feature sets that were only available in desktop applications.
In this chapter I walk through new and updated features in Internet Explorer that allow you to enhance and streamline your dynamic web applications. I discuss compatibility and interoperability scenarios, highlight IE product changes made in IE that may affect your existing applications, and provide useful and detailed examples that demonstrate how to integrate your website with IE's updated AJAX feature set.
The XMLHttpRequest
object enables web applications to communicate with web servers asynchronously, independent of page navigations; this object is arguably the heart of the AJAX programming model. It was first released as an ActiveX control in the year 2000 as a way for Microsoft Outlook Web Access (the online counterpart to the Microsoft Office Outlook application) to send and receive email data without having to reload a whole webpage. A wide variety of websites have since adopted the object for the same purpose.
The Microsoft XML Core Services Library (MSXML) is a set of interfaces that allow applications and scripting languages to easily read and write XML. XMLHTTP
is the part of MSXML that contains the XMLHttpRequest
object. It facilitates synchronous and asynchronous communication between an application and a remote server (however, synchronous calls should be avoided since they cause applications to hang until a response is received). XMLHTTP
is exposed in two ways: through the IXMLHttpRequest
interface, accessible to applications wishing to implement or extend the base functionality of this object, or with the XMLHttpRequest
ActiveX control, built for use in OLE applications.
Websites running in Internet Explorer 5 and 6 can access the XMLHttpRequest
object via this ActiveX control.
Example 3.1. Using the XMLHttpRequest ActiveX Object in JavaScript
<html> <head> <title>Using JavaScript</title> </head> <body> <h3>XMLHttpRequest object loaded? <span id="spanXhrJS">No</span></h3> <script type="text/javascript"> try { // Create a new XMLHttpRequest object and send a GET // request to http://examples.proiedev.com var xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); xmlHttp.open("get", "http://examples.proiedev.com", true); // Indicate success if the script made it this far setText(document.getElementById("spanXhrJS"), "Yes"); } catch(e) { } // Set element text (cross-browser innerText/textContent) function setText(element, text) { try { if(typeof element.textContent == typeof undefined) element.innerText = text; else element.textContent = text; } catch(e) { } } </script> </body> </html>
Listing 3-1 demonstrates some JavaScript that loads a new instance of the XMLHTTP
library into a variable; the xmlHttp
variable is set to grab a new object associated with the "Microsoft.XMLHTTP
" ProgID (Programmatic Identifier). IE instantiates the object and sets the variable to an XMLHttpObject
instance. This object is not limited to JavaScript.
Any language that supports loading OLE controls, such as VBScript, can use this object. Listing 3-2 demonstrates VBScript code that creates an instance of XMLHttpRequest
.
Example 3.2. Using the XMLHttpRequest ActiveX Object in VBScript
<html> <head> <title>Using VBScript</title> </head> <body> <h3>XMLHttpRequest object loaded? <span id="spanXhrVB">No</span></h3> <script type="text/vbscript"> '' Create a new XMLHttpRequest object Set xmlHttp = CreateObject("Microsoft.XmlHttp")
'' Send a GET request to http://examples.proiedev.com If Err = 0 Then xmlHttp.open "get", "http://examples.proiedev.com", TRUE '' Indicate success if the script made it this far If Err = 0 Then document.getelementbyid("spanXhrVB").innerText = "Yes" </script> </body> </html>
Internet Explorer 7 introduced a native version of the XMLHttpRequest
object. This object is a wrapper around the original XMLHttpRequest
ActiveX that allows JavaScript developers to write a single line of code to instantiate this object across all major browsers. It is only available to JavaScript running in Internet Explorer 7 and higher. Listing 3-3 demonstrates JavaScript that uses the native object instead of ActiveX to instantiate an XMLHttpRequest
object.
Example 3.3. Using the native window.XMLHttpRequest Object in JavaScript
<html> <head> <title>Native XMLHttpRequest</title> </head> <body> <h3>XMLHttpRequest object loaded? <span id="spanXhrJS">No</span></h3> <script type="text/javascript"> try { // Create a new XMLHttpRequest object and send a GET // request to http://examples.proiedev.com var xmlHttp = new XMLHttpRequest(); xmlHttp.open("get", "http://examples.proiedev.com", true); // Indicate success if the script made it this far setText(document.getElementById("spanXhrJS"), "Yes"); } catch(e) { } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Although IE7 and above offer a native, cross-browser implementation of XMLHttpRequest
, websites wishing to support IE6 users must load the ActiveX-based XMLHTTP
object instead. Listing 3-4 demonstrates JavaScript that attempts to use the native XMLHttpRequest
object and, if required, falls back to the ActiveX version. The example uses a cascade of conditionals and exception handling that loads either the native or the ActiveX version into the xhr
depending on which is available. Since the native object is simply a wrapper around the ActiveX version, the available properties, methods, and events are the same on each.
Example 3.4. Cross-Browser XMLHttpRequest object instantiation
<html> <head> <title>Cross-Browser AJAX Compatibility</title> </head> <body> <h3>Status of XmlHttpRequest object: <span id="status"></span></h3> <script type="text/javascript"> // Create a variable to hold the XMLHttpRequest object var xhr; // Check to see if the native object exists if(window.XMLHttpRequest){ xhr = new XMLHttpRequest(); setText(document.getElementById("status"), "Created Native XHR."); } // If no native object is found, check for the ActiveX object else if(window.ActiveXObject) { try { xhr = new ActiveXObject("Microsoft.XMLHTTP"); setText(document.getElementById("status"), "Created ActiveX XHR."); } catch(e) { setText(document.getElementById("status"), "XHR not supported"); } } else { // Indicate failure to find any XHR object setText(document.getElementById("status"), "XHR not supported."); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Most web developers will never need to write a script like this; frameworks like jQuery and Dojo abstract this logic and provide a single, cross-browser way of generating XMLHttpRequest
objects.
AJAX applications require a core set of JavaScript and DOM to functionality to deliver dynamic content and features. The next few sections highlight the most important updates to IE's JavaScript engine and object model and discuss how those changes may impact current and future websites.
JSON (JavaScript Object Notation) is a lightweight data exchange format, commonly used to pass data between web applications. Much like XML, it encapsulates and compartmentalizes data in a way that can be read, modified, and transferred. Many AJAX developers choose JSON because of its structure; it remains valid JavaScript notation in both serialized and deserialized forms.
Example 3.5. An customer record stored in JSON
{ "firstname" : "Nick", "lastname" : "Tierno", "address": { "street" : "123 Euclid Avenue", "city" : "Cleveland", "state" : "OH", "postalCode" : 44106 }, "phone": [ "+1 555 867 5309", "+1 555 TIERNO0" ] }
Listing 3-5 demonstrates some JSON (in this case, a customer record). It can be interpreted as a JavaScript object literal containing types including Array, String, Number
, and Boolean
, or serialized and transferred as structured markup.
Conversion of a JSON object from a String to JavaScript is trivial with eval()
; since serialized JSON remains valid JavaScript, this method can convert any valid JSON back into script. The simplicity of JSON conversion using eval()
hides the fact that eval()
can be extremely dangerous from a security standpoint; since the point of eval()
is to execute a string as script, its use potentially opens websites to a host of script injection vulnerabilities. Aside from the security issues surrounding eval()
, JavaScript itself does not provide sufficient built-in functionality to convert a JSON object back to a string (besides recursively walking the object).
eval() is dangerous—by definition, it is script injection. While it may be convenient to convert JSON strings into valid JavaScript with eval(), it will turn site security into Swiss cheese unless done properly. Do not use it unless you have to (or you're building a JSON library). Alternatively, use one of the safer options described below: IE's JSON object, JSON libraries, and general JavaScript frameworks that offer such functionality.
The security concerns around eval()
-based deserialization and lack of good mechanisms for JSON serialization lead to the creation of JSON libraries. The best known (and arguably most optimal) one is json2 (http://link.proiedev.com/json2
). Internet Explorer, in versions 8 and above, is host to a native JSON management; it is available to pages rendering in IE8 Standards Mode and above. The native JSON object allows web applications to convert objects to and from the JSON format just like JSON libraries, albeit in a faster and less memory-intensive way.
Objects
JSON
object - Top-level object that offers conversion methods for JSON objects and strings.
(supported objects) - Boolean, String, Number, and Date JavaScript objects are given a new toJSON() method to convert them into serialized JSON.
Methods
(supported object).toJSON()
method - Converts supported types (Boolean, String, Number, and Date) to serialize those objects into valid JSON.
JSON.parse(source [, reviver])
method - Deserializes a JSON object source into a JavaScript array or relevant object. The reviver parameter accepts a callback method, and this callback is raised for each member of the new JSON object as it is converted. This allows for further parsing.
reviver(key, value)
callback method - Returns a JavaScript object modified from an original key and value input. This object replaces the object normally returned by JSON.parse() for each member of the original string.
JSON.stringify(value [, replacer] [, space])
method - Seralizes an existing JavaScript object value into a string. The replacer parameter accepts a callback method, and this callback is raised for each member of the new JSON string as it is converted. This allows for further parsing. The space parameter specifies custom whitespace to be appended between serialization of each object member.
replacer(key, value)
callback method - Returns a JavaScript string modified from an original key and value input. This string replaces the string normally returned by JSON.stringify() for each member from thesource array or object.
The structure of IE8's native JSON object was designed to mimic that of the json2 library; applications already incorporating json2 can take advantage of IE's built-in support with minimal code changes.
Listing 3-6 demonstrates IE's native JSON object by deserializing a JSON-formatted string to a JSON object and then re-serializing it back to a string.
Example 3.6. Converting between JSON strings and objects
<html> <head> <title>Native JSON Support</title> </head> <body> <h3>Original String:</h3><span id="original"></span> <h3>Parsed JSON:</h3><span id="parsed"></span> <h3>Stringified JSON:</h3><span id="result"></span>
<script type="text/javascript"> // Define a new serialized JSON object var contactStr = "{ "firstname" : "Nick", "lastname" : " + ""Tierno", "address" : { "street" : "123 Euclid " + "Avenue", "city" : "Cleveland", "state" : " + ""OH", "postalCode" : 44106 }, "phone" : [ " + ""+1 555 867 5309", "+1 555 TIERNO0" ] }"; // Write that string to the page setText(document.getElementById('original'), contactStr); // Check if the JSON object exists if(window.JSON) { // Convert contactStr to a JSON JavaScript object var contactObjectJSON = JSON.parse(contactStr); var outputFromJSON = "Name: " + contactObjectJSON.firstname + " " + contactObjectJSON.lastname + " " + "Address: " + contactObjectJSON.address.street + ", " + contactObjectJSON.address.city + ", " + contactObjectJSON.address.state + " " + contactObjectJSON.address.postalCode + " " + "Phone: " + contactObjectJSON.phone[0] + " " + contactObjectJSON["phone"][1]; setText(document.getElementById('parsed'), outputFromJSON); // Convert contactJSON back to a string var contactStrRedux = JSON.stringify(contactObjectJSON); setText(document.getElementById('result'), contactStrRedux); // Display an error message if the JSON object doesn't exist } else { setText(document.getElementById("parsed"), "Error: window.JSON object does not exist."); setText(document.getElementById("result"), "Error: window.JSON object does not exist."); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
The contactStr
variable represents a string, perhaps received from an onmessage
handler which fires when an <iframe>
performs a postMessage
call (discussed later). The contactObjectJSON
variable represents a deserialized object output from JSON.parse(contactStr)
; it is used to display the customer record. Finally, the result of a JSON.stringify()
is set to the contactStrRedux
string, which functionally matches the original string contactStr
.
Figure 3-1 is the webpage from the sample code. The contactStr
and contactStrRedux
variables are displayed under the "Original String" and "Stringified JSON," respectively. "Parsed JSON" header is followed by data from the deserialized JSON object; this demonstrates that the deserialized contactStr
can be accessed as any other JavaScript object or array once serialized.
Websites that use JSON libraries or native JSON support are able to block the most trivial instances of malicious JavaScript from tainting potentially-untrusted JSON content even further. The security goodness that these features provide is lost once this content is used to write content to a webpage or transfer that content to another resource (for example, displaying content on a page using an element
object's innerHTML
property). IE8 includes a new object, window.toStaticHTML
, as a second line of defense, allowing scripts to sanitize content before it is used in the context of a page.
Methods
toStaticHTML(html)
object (constructor) - Returns a sanitized version of the input html
parameter by removing dynamic objects (certain elements and script) from it.
The code sample in Listing 3-7 uses the toStaticHTML
object to clean up input coming from an <input>
textbox. The page was constructed so that input from this object can inject script into the page (when the sanitizeCheck
checkbox is left unchecked).
Example 3.7. Sanitizing HTML content using toStaticHTML
<html> <head> <title>Using toStaticHTML</title> </head>
<body> <p>Type some input into the box below. If "safely handle input" is checked, the output will be run through toStaticHTML (if availalable). If it is not, the output will be set as the innerHTML of a div. <p><input type="text" id="userInput" name="userInput"> <input type="checkbox" id="sanitizeCheck" name="sanitizeCheck" checked> Safely handle input? <button name="displayOutput" id="displayOutput" onclick="processUserInput();"> Display Output</button> <h3>Output:</h3><div id="outputContainer"></div> <script type="text/javascript"> function processUserInput() { // Simulate some evil input, such as script injection var evilInput = document.getElementById("userInput").value; var sanitizeCheck = document.getElementById("sanitizeCheck").checked; var doSanitize = (sanitizeCheck == true) ? true : false; var sanitizedInput = ""; // Sanitize input text if box is checked if(doSanitize) { // If toStaticHTML is defined, use it (otherwise, escape) if(typeof toStaticHTML == "object") sanitizedInput = toStaticHTML(evilInput); else sanitizedInput = escape(evilInput); // Write sanitized input to the webpage setText(document.getElementById("outputContainer"), sanitizedInput); } // Otherwise, write raw HTML to document else document.getElementById("outputContainer").innerHTML = evilInput; } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
The state of the sanitizeInput
checkbox determines whether or not this script will clean user input. When sanitization is turned on, the script runs input through toStaticHTML
before writing it to the page through the innerText
property on the outputContainer
element. When this object is not present, it escapes content with escape
before writing it to innerText
. When this checkbox is disabled, content in the <input>
textbox is written to outputContainer
through the innerHTML
property; this simulates an unsafe use of input data.
Figure 3-2 shows the sample page running in IE8.
In this case, the text in Listing 3-8 was passed into the box with sanitization enabled. Even though the input text contains HTML and JavaScript, that data is not added to the DOM nor executed; toStaticHTML
escapes the content and removes script entries. It is shown as text on the page.
Example 3.8. Sanitizing HTML content using toStaticHTML()
</div></div><div style="position:absolute;width:8888px;height:8888px;z-index:99;padding:0;background-color:black;color:white;border:0;font-size:36px;font-weight:bold;" onclick="javascript:window.location.href='http://www.bankofamerica.com'">Bank error. Click here to fix bank error.</div>
The screenshots in Figure 3-3 show the effect of this same input when written to the page without any sanitization and through the innerHTML
property of outputContainer
. In this case, a <div>
with the text "Bank Error" is placed on top of other page elements. When clicked, this <div>
launches bankofamerica.com
; an attacker might do this to entice a user into entering their credentials.
Internet Explorer 8 introduces a number of changes geared towards conformance with ECMAScript, and W3C specifications. These changes may affect existing AJAX applications, especially those relying upon events and DOM objects such as element
.
Internet Explorer 8 and older versions do not support the addEventListener()
method outlined in the W3C DOM Level 2 Events specification. Developers wishing to attach an event to the window
object can alternatively use the attachEvent
. Although exposed events may differ between browsers, interoperability can be achieved by first locating window.addEventListener
; if this is not present, the window.attachEvent
object can be used as a fallback. Listing 3-9 offers an example of this.
Example 3.9. Cross-Browser eventing using either addEventListener or attachEvent
<html> <head> <title>Handling the addEventListener Method</title> </head> <body> <h3>addEventListenter Support? <span id="supportAEL">No.</span></h3> <h3>attachEvent Support? <span id="supportAE">No.</span></h3> <h3>Function used: <span id="fallback"></span></h3> <script type="text/javascript"> // Determine if the browser supports addEventListener() if (window.addEventListener) setText(document.getElementById("supportAEL"), "Yes!"); // Determine if the browser supports attachEvent() if (window.attachEvent) setText(document.getElementById("supportAE"), "Yes!"); // Simulate a fallback chain used by many web applications if (window.addEventListener) setText(document.getElementById("fallback"), "addEventListener()"); else { if (window.attachEvent) setText(document.getElementById("fallback"), "attachEvent()"); else setText(document.getElementById("fallback"), "—"); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Figure 3-4 demonstrates this script running in both IE8 and Chrome 3. In IE8, addEventListener
is not available, thus the script falls back to using attachEvent
. In Chrome 3, addEventListener
is available, allowing the script to operate without falling back to attachEvent
.
While IE does not support addEventListener
, it can be added to IE8 using DOM prototypes. This method and other DOM work (such as Accessors) are covered in the next chapter.
Prior to IE8, the document.getElementById()
method was case insensitive. Developers building pages in IE8 Standards Mode will discover scripts depending on this trait can no longer find matching elements. IE8 Standards Mode and rendering modes of other newer browsers require the id
parameter passed to getElementById()
match the case of a target element's id
.
Listing 3-10 contains a script that accesses a <div>
element with the id testDiv
. In IE7 Standards Mode and lower, the script can access this element by using the lowercase testdiv
or the camel-case form testDiv
. The script fails to access the element using the lowercase id testdiv
in IE8 mode since the case of that string does not match the case of the real id
.
Example 3.10. Example of getElementById case sensitivity
<html> <head> <title>Case Sensitivity in getElementById</title> </head> <body> <h3>Access element with id "testdiv": <span id="caseInsensitive"></span></h3> <h3>Access element with id "testDiv": <span id="caseSensitive"></span></h3> <div id="testDiv"></div> <script> // Attempt to access element testdiv try { setText(document.getElementById("testdiv"), " "); setText(document.getElementById("caseInsensitive"), "Success!"); } catch(e) { setText(document.getElementById("caseInsensitive"), "Error"); }
// Attempt to access element testDiv try { setText(document.getElementById("testDiv"), " "); setText(document.getElementById("caseSensitive"), "Success!"); } catch(e) { setText(document.getElementById("caseSensitive"), "Error"); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Figure 3-5 shows the output of the script. Again, the IE7 mode page successfully accesses the <div>
despite the case difference from the actual element's id
, indicating success in both cases. In the IE8 mode page, the script can only access the element when the case provided to getElementById()
matches that of the id
attribute.
A number of changes were made to the way Internet Explorer handles element attributes moving the browser towards more complete standards compliance.
Internet Explorer 8 no longer writes attribute information to an element
object's attribute collection when those attributes are not given an initial value. Prior to IE8, the browser initialized all attributes and set their value to a default value appropriate for their type.
This behavior affects pages running in all document modes; pages relying on IE7 Standards Mode to emulate IE7 behavior may see exceptions raised by pages accessing unset attributes.
Listing 3-11 is an example of a page that has two <input>
checkboxes, one without an initial checked state (checkBox
) and the other with (checkBoxChecked
).
Example 3.11. Code sample showing IE8 not placing uninitialized values into the attributes collection
<html> <head> <title>Uninitialized Values and the Attributes Collection</title> </head> <body> <h3>Attribute "checked" exists on "checkBox": <span id="attrCheckBox"></span></h3> <h3>Attribute "checked" exists on "checkBoxChecked": <span id="attrCheckBoxChecked"></span></h3> "checkBox" Object: <input type="checkbox" id="checkBox"><br> "checkBoxChecked" Object: <input type="checkbox" id="checkBoxChecked" checked> <script> // Attempt to access the "checked" attribute of checkbox "checkBox" var attrCheckBox = document.getElementById("checkBox").getAttribute("checked"); setText(document.getElementById("attrCheckBox"), (attrCheckBox) ? "True" : "False"); // Attempt to access the "checked" attribute of checkbox "checkBoxChecked" var attrCheckBoxChecked = document.getElementById("checkBoxChecked").getAttribute("checked"); setText(document.getElementById("attrCheckBoxChecked"), (attrCheckBoxChecked) ? "True" : "False"); // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Script in this example attempts to access the checked
attribute of both objects. The first <input>
object did not have an initial checked state, thus the a null value is applied to attrCheckBox
when the attempts to set the checked
attribute's value to it. The script creates the attrCheckBox
variable and assigns the checked attribute of the first <input>
box to it. The attrCheckBoxChecked
variable, on the other hand, successfully retrieves the value of that parameter on the checkBoxChecked
element since that object's checked attribute was initialized in markup.
Figure 3-6 shows the example running in IE7 mode of Internet Explorer 8, conveying the fact that the checked
attribute on the element checkBox
does not exist since it was not initially set, whereas it does exist on the checkBoxChecked
object.
Pages that rely on the order of Internet Explorer's attribute collection may require modifications in order to work properly in IE8 Standards Mode and above. Scripts falling into this category refer to attributes using array notation (e.g. element.attributes[1]
) instead of using property notation (e.g. element.style
) or attribute get and set methods (e.g. element.getAttribute("style")
). This is an effect of the browser's move to no longer pre-initialize unspecified attributes in IE8 Standards Mode and above (as described in the previous section).
The code in Listing 3-12 contains a for loop that looks at the i
th attribute of a <div id="testDiv">
from 0 to 4. This <div>
has three pre-defined attributes: class, style
, and id
. is the same code run in both IE7 and IE8 mode, yet the results differ. The results of this loop are written to the page.
Example 3.12. Script that demonstrates attribute ordering differences between IE7 and IE8
<html> <head> <title>Attributes Collection Ordering</title> </head> <body> <div id="resultsDiv"></div> <div class="someClass" style="display: none;" id="testDiv"></div> <script type="text/javascript"> // Return the names of the first 5 attributes on <div> var resultHTML = ""; for(var i = 0; i < 5; i++) { // Attempt to access the ith element of the <div id="testDiv"> attribute // collection var attrByArray = document.getElementById("testDiv").attributes[i]; resultHTML += "<h3>Element [" + i + "] on <div>: " + ((attrByArray) ? attrByArray.name : "—")
+ "</h3> "; } // Output results to the screen document.getElementById("resultsDiv").innerHTML = resultHTML; </script> </body> </html>
Figure 3-7 displays the output of this script in both IE7 and IE8 Standards Modes. In IE7 mode, the first five attributes in the attributes collection exist (onresizeend, onrowenter, aria-haspopup, ondragleave
, and onbeforepaste
), albeit with empty values. In IE8 mode, only the first three exist (class, id, style
), with coincide with the attributes explicitly set in the page markup; the last two accesses return null
objects instead of empty attributes.
An element's class information can be obtained in JavaScript via two methods: either through the className
property of an element object, or through the getAttribute()
method present on that same object. In IE7 and below (as well as in IE8's IE7 Standards Mode), the class attribute was accessible via getAttribute()
when the string "className"
into it. This did not conform to established standards.
Developers wishing to access the class attribute via getAttribute()
must use the string "class"
instead of "className"
to access an element object's class attribute. The use of "className"
will raise a TypeError
exception.
The JavaScript in Listing 3-13 attempts to access the class attribute information of <div id="classTest">
three ways: by using the .className
property, getAttribute("className")
, and getAttribute("class")
. The first two methods work properly in IE7 mode, whereas the last throws an exception. IE8 mode results in the same TypeError
exception, but when calling getAttribute("className")
; the call to getAttribute("class")
is successful.
Example 3.13. Script that accesses element information using the class property and getAttribute() method
<html> <head> <title>Accessing an Element's Class Information</title> </head> <body> <h3><div> "classTest", getAttribute("class"): <span id="attrClass"></span></h3> <h3><div> "classTest", getAttribute("className"): <span id="attrClassName"></span></h3> <h3><div> "classTest", property .className: <span id="propClassName"></span></h3> <div id="classTest" class="testClass"></div> <script type="text/javascript"> // Get the <div id="testClass"> element var divClassTest = document.getElementById("classTest"); // Attempt to access the attribute via getAttribute "class" try { var attrClass = divClassTest.getAttribute("class"); setText(document.getElementById("attrClass"), attrClass.toString()); } catch(e) { setText(document.getElementById("attrClass"), "—"); } // Attempt to access the attribute via getAttribute "className" try { var attrClassName = divClassTest.getAttribute("className"); setText(document.getElementById("attrClassName"), attrClassName.toString()); } catch(e) { setText(document.getElementById("attrClassName"), "—"); } // Attempt to access the attribute via property "className" try { var attrClass = divClassTest.className; setText(document.getElementById("propClassName"), attrClass.toString()); } catch(e) { setText(document.getElementById("propClassName"), "—"); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Figure 3-8 highlights this—the leftmost page, running in IE7 Standards Mode, displays the class
attribute value of a <div>
through the .className
property as well as through getAttribute("className")
. The page running in IE8 mode displays the class value through the .className
property as well, but must use "class"
to access the it via getAttribute()
.
Implementing interoperable data persistence in a web application is a difficult, especially since no well-adopted standard exists to support it. Traditionally, cookies have been used to achieve such persistent storage. Cookie storage has its drawbacks—there are size limitations for each cookie (4KB for IE7 and below, and 10KB in IE8), domains are limited to a fixed number of cookies, cookies are sent back and forth through every HTTP transaction, and each browser handles cookies in a slightly different way.
There has been a number of attempts to fill this void; Flash, Google Gears, and IE5's UserData feature were either created or used to circumvent the limitations of cookies. Each method has major drawbacks and no single one has solved the problem of interoperability.
The HTML 5 specification takes another crack at this problem with its DOM Storage features. Internet Explorer 8 implements these features to all document modes.
Objects
Storage object - Generic object that defines a storage mechanism. It complies with the HTML 5 DOM Storage specification.
sessionStorage object - Subclass of the Storage object that stores information for a single browser session.
localStorage object - Subclass of the Storage object that stores information across multiple browser sessions.
Methods
getItem(key)
method - Returns a value in a Storage
object identified by a key
.
key(index)
method - Returns a key located at the specified collection index
.
removeItem(key)
method - Removes an item from a Storage
object specified by key
.
setItem(key, value)
method - Sets a value into a Storage
object identified by a key
.
clear()
method - Clears all key/value pairs currently set in a Storage
object.
length
property - Returns the length of a Storage
object's key/value list.
remainingSpace
property - Returns the remaining space (in bytes) in a Storage
object.
(expando)
- Returns the value associated from a key, providing that the key name is not the same as a reserved name of a Storage
object.
Events
onstorage
event - Fired whenever a key/value pair is created, modified, or deleted.
The localStorage
and sessionStorage
objects both derive from the Storage object. They each contain the same methods, properties, and events. They differ only in their level of persistence; sessionStorage
only persists its contents for the lifetime of a "session." Session in this case does not mean a browser session (such as those used by cookies)—it refers to data stored by a page and frames contained within a tab whose persistence lasts only for the lifetime of that tab. sessionStorage
does not persist data between tabs and there is no reliable way to use this object to share data between frames.
Aside from the length of persistence, these objects are otherwise interchangeable; developers can switch between the two simply by changing the object reference during instantiation.
Most properties and methods on the Storage
object provide access to data through key/value pairs. Each key can be read from or written to through the get/set methods on a Storage
object or with an expando; for example, a value under a key "foo
" in storage object "myStorage
" can be accessed either by calling myStorage.getItem("foo")
or via the expando myStorage.foo
. The length
property returns the number of key/value pairs on the current subdomain, and the remainingSpace
property returns the free space remaining in a domain's storage quota (this includes data from subdomains as well). Whenever a key/value pair is added or removed from a Storage
object, the onstorage
event is fired.
Listing 3-14 represents a webpage that implements DOM storage to persist data.
Example 3.14. An example of cross-browser storage using HTML 5 DOM Storage
<html> <head> <title>Persisting Data with DOM Storage</title> </head> <body> <h3>Current Value: <span id="curVal"></span></h3> <h3>Current Value from <i>expando</i>: <span id="curValExpando"></span></h3> Set new text value: <input id="inputVal" size="20" type="text"> <p><input onclick="setStorageData();" type="submit" value="Save Data"> <button onclick="clearItems();">Clear Data</button> <h3>DOM Store information for <span id="infoDomain"></span></h3> <b>Length:</b> <span id="infoLength"></span> items<br> <b>Remaining Space:</b> <span id="infoRemaining"></span> KB<br> <script type="text/javascript"> // Create variables for the storage type var storageObject = localStorage; // Read DOM Storage data for this domain function getStorageData() {
// Get the storage data via getItem. Make sure this value // is string (IE Bug: empty values are returned as VT_NULL // instead of a blank BSTR ("") var getItemData = storageObject.getItem('DOMStorageExample'), if(getItemData == null) getItemData = ""; // Sanitize the data with toStaticHTML (if available) try { getItemData = toStaticHTML(getItemData); } catch(e) { escape(getItemData); } // Write getItem results to the screen document.getElementById("inputVal").value = getItemData; setText(document.getElementById("curVal"), (getItemData == "") ? "—" : getItemData); // Get the value via an expando, again compensating for // the VT_NULL bug var expandoData = storageObject.DOMStorageExample; if(expandoData == null) expandoData = "—"; // Sanitize the data with toStaticHTML (if available) try { expandoData = toStaticHTML(expandoData); } catch(e) { escape(expandoData); } // Write expando results to the screen setText(document.getElementById("curValExpando"), expandoData); // Write domain info setText(document.getElementById("infoDomain"), document.domain); // Display length if available var remainingSpace = "—"; try { if(storageObject.remainingSpace != null && typeof storageObject.remainingSpace != typeof undefined) remainingSpace = String(Math.round(storageObject.remainingSpace / 1024)); } catch(e) { } setText(document.getElementById("infoLength"), storageObject.length); // Display remainingSpace if available var remainingSpace = "—"; try { if(storageObject.remainingSpace != null && typeof storageObject.remainingSpace != typeof undefined) remainingSpace = String(Math.round(storageObject.remainingSpace / 1024)); } catch(e) { } setText(document.getElementById("infoRemaining"), remainingSpace); } // Write data into DOM Storage for this domain function setStorageData() { // Set the contents of the input box to the storage var newValue = String(document.getElementById("inputVal").value); storageObject.setItem('DOMStorageExample', newValue);
// Re-display the storage data getStorageData(); } // Clear DOM Storage for this domain function clearItems() { // Clear out data in the storage object storageObject.clear(); // Re-display the storage data getStorageData(); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
The following variables and methods are defined in the script:
storageObject
variable - References a Storage
object. In this example, that object is localStorage
; since this and sessionStorage
are interchangeable, session storage can be used by changing the reference.
storageKey
variable - Represents the key used by the page to save data into the storageObject
. While this example only uses one key, webpages can utilize more than one to save site settings.
getStorageData()
method - Called to retrieve data in the storage object and display it in the webpage.
setStorageData()
method - Called to write data in the page's input box into the storage object.
clearItems()
method - Used to clear all data out of the storage object.
When the page is loaded, the <body> onload
event triggers the getStorageData()
method to retrieve data already in storage. The "Save Data" button triggers the setStorageData()
method; it takes the contents of the <input>
textbox and saves it to the "DOMStorageExample
" key of the storageObject
. The "Clear Data" button calls the clearItems()
method, which deletes all entries currently in the storageObject
.
In IE8, Empty <input> elements return a VARIANT_NULL value rather than a blank BSTR that the storage object expects. When this value is placed directly into DOM Storage, IE crashes. To work around this, always save the value of an <input> box to a string variable or explicitly type it using String(...) before saving it to a storage object.
In addition to using getItem()
to access a value for a specific key, that value can be accessed via its expando on the Storage
object. The example demonstrates this in getStorageData(), setStorageData()
, and clearItems()
; the innerText
(via setText
) of curValExpando
is set using the storageObject.DOMStorageExample
expando for the key named DOMStorageExample
. The getStorageData()
method also grabs and displays information about the current storage object: the domain it's setting data to, the number of keys currently in the object, and the remaining space (if available).
Figure 3-9 displays a screenshot of the DOM storage example in both IE8 and Firefox 3.5. Contents of the storage object are displayed on the page and, when the localStorage
object is being used, that data remains available even after the browser is closed and reopened. Data typed into the input box can be saved to the storage object via the "Save Data" button. The storage object can be cleared by clicking on the "Clear Data" button.
Key/value pairs in Storage
objects cannot be shared between domains, nor can they be shared between domains and their subdomains. Domains and subdomains do share one storage area even though their respective data is inaccessible to each other; both fall under one limit of 10MB in IE8.
In Figure 3-10, pages hosted examples.proiedev.com
and subdomain.examples.proiedev.com
both write to matching keys in their respective Storage
objects. Even though they both reside on proiedev.com
, they cannot access each other's data even when using the same key. They are, however, restricted to the same amount of shared space; they each must share one 10MB storage area.
Persisted storage is a double-edged sword—while it enables offline scenarios for web applications, it also introduces a new class of security issues pertaining to data integrity.
Storage objects are isolated to the domains and subdomains accessing those objects. Data stored from one domain is not accessible from another, even if values are stored using the same key. For instance, a key/value pair saved from proiedev.com
is not accessible from ie-examples.com
. Domains and their subdomains share one 10MB block for data storage, but they cannot share data between each other. These domain restrictions also apply to the quota of Storage objects; only domains which have access to a certain Storage object can save data to that object.
Internet Explorer provides basic data security for storage objects based on context and origin policies. Developers, however, should not rely upon these mechanisms for complete data security; some attacks, such as cross-site scripting exploits, circumvent same-origin policy. Sensitive data (such as personally identifiable information) generally should not be kept in DOM storage; if there is no other option, strong encryption should be used. Data should be selectively removed with the removeItem()
method or completely deleted using clear()
if there is no need for it to be persisted. End-users can clear these items through Internet Explorer's Delete Browsing History feature.
HTML 5 DOM Storage finally offers web developers a great option for persistent, cross-browser storage. Unfortunately, older versions of IE and other major browsers don't support this model yet; until these are eventually phased out, developers will need to use older methods to persist data in these scenarios.
A number of libraries create an abstraction layer this and older methods of persistent storage. These allow pages to take advantage of HTML 5 Storage when available and fall back to older methods when it isn't. Here are some common frameworks that were available at the time of publication:
PersistJS - Standalone, dedicated persisted storage library. No prerequisites. (http://link.proiedev.com/persistjs
)
Dojo Offline - Plugin that works with the Dojo Library. Requires the Dojo library. (http://link.proiedev.com/dojooffline
)
jQuery jStore - Plugin for jQuery. Requires the jQuery library. (http://link.proiedev.com/jstore
)
The network and connectivity changes in Internet Explorer 8 and higher consist of new features and updates to old ones that ensure web applications can operate regardless of network connectivity.
The window.navigator.onLine
property was introduced in Internet Explorer 4 as a way for webpages to know when the browser was running "Offline Mode." This flag did not report the state of network connectivity. AJAX applications that provide offline features have worked around this limitation by "pinging" remote severs with XMLHttpRequest
, however this method isn't completely reliable. Internet Explorer 8 and higher now use this property to reflect both the state of network connectivity and whether or not the browser is running in Offline Mode.
onLine
property - Indicates whether or not the system is connected to the network with a Boolean value. When connected to a network and when IE is not running in "Offline Mode," true
is returned; false
is returned otherwise. In IE7 and below, this property indicates only if the browser is running normally (true
) or in "Offline Mode" (false
).
Events
onoffine
event - Fired whenever Internet Explorer detects a loss of network connectivity or the browser has entered "Offline Mode."
ononline
event - Fired whenever Internet Explorer detects that network connectivity has resumed or the browser has left "Offline Mode."
The events and property changes in the window.navigator
and window.clientInformation
objects are available to pages running in any document mode.
The code in Listing 3-15 uses three events to gather and act on connectivity information: onload, ononline
, and onoffline
. The onload
event calls the onLoad()
method; it gathers and displays the initial connectivity state from the window.navigator.onLine
property. The onoffline
event calls the onOffline()
method whenever connectivity is lost; the method in this sample writes "Offline" to the page. ononline
acts in the opposite manner, calling onOnline()
when connectivity is restored and, in this case, writing "Online" to the page.
Example 3.15. Sample code demonstrating online/offline property and events
<html> <head> <title>Online/Offline Events</title> </head> <body ononline="onOnline();" onoffline="onOffline();" onload="onLoad();"> <h3>Connectivity Status: <span id="status"></span></h3> <script type="text/javascript"> // When the browser goes online, write "Online" to the page function onOnline() { setText(document.getElementById("status"), "Online"); } // When the browser goes offline, write "Offline" to the page function onOffline() { setText(document.getElementById("status"), "Offline"); } // When the page loads, display the current connectivity status function onLoad() { setText(document.getElementById("status"), (window.navigator.onLine) ? "Online" : "Offline"); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body>
</html>
Figure 3-11 shows this sample code running in both IE7 and IE8 Standards Modes. The browser in this scenario initially connected to the internet and running the page in IE8 Standards Mode. The page is reloaded in IE7 mode, quickly followed by a good yank on the CAT-5 cable connected to the client machine. Once the connection is severed, the onoffline
event is fired and the page reads "Offline."
Internet Explorer 8 and higher include a timeout
property and ontimeout
event in the XMLHttpRequest
object. Scripts using this object can apply a hard limit on the time a request should take using this property, and receive an event when this limit is reached.
Properties
timeout
property - The amount of time in milliseconds that the XMLHttpRequest
object should wait for a response from a target. The ontimeout
event is raised when this limit is reached and the request is aborted.
Events
ontimeout
event – Fires an associated callback function when time elapsed between the a XMLHttpRequest.send()
call and the current time is reached.
Listing 3-16 is a webpage that uses server-side script (in this case, PHP) to simulate a webpage taking a long time to load. Whenever a request to this page is received by the server, the script pauses 5 seconds (with sleep(5)
) before responding with the text "Success!"
Example 3.16. Webpage that forces a server to wait for 5 seconds before responding
<?php // Simulate a page that takes 5 seconds to respond. Once finished, // return the string "Success!" sleep(5); echo "Success!"; ?>
Listing 3-17 demonstrates a script that requests the page in Listing 3-16 using two XMLHttpRequest
objects: xhrSmallTimeout
and xhrLargeTimeout
. The xhrSmallTimeout
object requests the above page, but limits the wait time for that request to 2 seconds using its timeout
property. The xhrLargeTimeout
object attempts to pull down the same page, but allots a maximum of 10 seconds for a response. Each object has two callback methods: one for ontimeout
(timeoutRaisedSmall()
and timeoutRaisedLarge()
), fired if a request's timeout is reached, and the other for onreadystate
(readyStateHandlerSmall()
and readyStateHandlerLarge()
), fired during steps of the request/response process. Exceptions thrown during execution of the onreadystate
callbacks are written to <div id="exceptions">
.
Example 3.17. Code sample using the XMLHttpRequest timeout event
<html> <head> <title>XMLHttpRequest Timeout</title> </head> <body> <h3>Request with 2s timeout: <span id="xhrSmallStatus"></span></h3> <h3>Request with 10s timeout: <span id="xhrLargeStatus"></span></h3> <h3>Exceptions:</h3> <div id="exceptions"><p></div> <script type="text/javascript"> // Define the XMLHttpRequest variables, the target page and // elements that will be written to var xhrSmallTimeout; var xhrLargeTimeout; var targetPage = "http://examples.proiedev.com/03/networking/timeout/result.php"; var spanXhrSmallStatus = document.getElementById("xhrSmallStatus"); var spanXhrLargeStatus = document.getElementById("xhrLargeStatus"); var spanExceptions = document.getElementById("exceptions"); function displayException(e, objectName) { spanExceptions.innerHTML += "<b>Object:</b> " + objectName + "<br>" + "<b>Name:</b> " + e.name + "<br>" + "<b>Message:</b> " + e.message + "<p>"; } // Timeout callback for request with 2s timeout function timeoutRaisedSmall(){ setText(spanXhrSmallStatus, "Timeout"); } // Timeout callback for request with 10s timeout function timeoutRaisedLarge() { setText(spanXhrLargeStatus, "Timeout"); } // readyState callback for request with 2s timeout. If an // exception is raised, write it to the screen. function readyStateHandlerSmall() { if(xhrSmallTimeout.readyState == 4) { try { setText(spanXhrSmallStatus, xhrSmallTimeout.responseText); } catch(e) { displayException(e, "xhrSmallTimeout"); } } } // readyState callback for request with 10s timeout. If an // exception is raised, write it to the screen. function readyStateHandlerLarge() { if(xhrLargeTimeout.readyState == 4) { try {
setText(spanXhrLargeStatus, xhrLargeTimeout.responseText); } catch(e) { displayException(e, "xhrLargeTimeout"); } } } // Create a XMLHttpRequest object with a small (2 second) // timeout period xhrSmallTimeout = new XMLHttpRequest(); xhrSmallTimeout.open("GET", targetPage, true); xhrSmallTimeout.timeout = 2000; xhrSmallTimeout.ontimeout = timeoutRaisedSmall; xhrSmallTimeout.onreadystatechange = readyStateHandlerSmall; // Create a XMLHttpRequest object with a large (10 second) // timeout period xhrLargeTimeout = new XMLHttpRequest(); xhrLargeTimeout.open("GET", targetPage, true); xhrLargeTimeout.timeout = 10000; xhrLargeTimeout.ontimeout = timeoutRaisedLarge; xhrLargeTimeout.onreadystatechange = readyStateHandlerLarge; // Sent the XMLHttpRequests xhrSmallTimeout.send(null); xhrLargeTimeout.send(null); // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Figure 3-12 shows the sample code being run. The timeoutRaisedSmall()
method is called and "Timeout" is shown on the page 2 seconds after the xhrSmallTimeout
object makes a request (since the server won't respond for at least 5 seconds). The onreadystate
event is raised after the timeout is hit, resulting in an exception—while the request technically completed (readyState == 4
), the responseText
returns a null
type rather than a String
. The second request made by xhrLargeTimeout
finishes after about 5 seconds, triggering readyStateHandlerLarge()
to write the associated responseText
to the page. The method timeoutRaisedLarge()
is never called since the second request completes before the 10 second limit.
This example raises an important point about XMLHttpRequest
timeouts: transactions that timeout before completion are considered "finished" by the browser, thus onreadystatechange
callbacks are fired with their readyState
indicates completion. The responseXML
and responseText
properties of those objects, however, are not available. Scripts using the timeout
property and ontimeout
event must use onreadystatechange
event handlers that fail gracefully when these properties are null.
Many AJAX applications use in-page navigations (often called hash or fragment navigations) to trigger events or change states. In-page navigations allow pages to update the address bar without making a full navigation request; they give users a way to save a page's state through bookmarks or email a that state to someone else. Mapping sites like Google Maps and MapQuest are great examples—they use in-page navigations to zoom-in on a place or display a window to get directions to a location.
In older browsers and versions of Internet Explorer prior to IE8, these navigations were never appended to the travel log unless an anchor tag with the same value as the hash existed on the page (for more information on the travel log, see Chapter 1) The back and forward buttons were rendered useless in these cases, and users needed to rely on the web application to provide similar functionality.
IE8 breaks from this old behavior by allowing developers to add in-page navigations to the travel log when necessary. This feature is opt-in—webpages are given a chance to append hash information to the navigation history whenever the onhashchanged
event is fired. Only pages running in IE8 Standards Mode and above may catch this event and persist navigations.
Properties
location.hash
property - Returns the fragment component of the current URL, including the leading hash (#) mark. This property is read/write (in IE8 Standards Mode and above).
onhashchange
event - Fired when an in-page navigation occurs either by a user (through the IE user interface) or script (through navigation methods). This event is not fired during page load; an onload
Event handler can examine the location.hash
property and adjust the application's state accordingly.
The sample code in Listing 3-16 is a "Search Provider Generator." Search Provider extensions, discussed in Chapter 10, add search engines into the search box in Internet Explorer, Firefox, and any other browsers that support the OpenSearch XML format. Figure 3-14 gives a bit more context; it shows the search boxes in both in IE and Firefox where these extensions live.
The printed sample here omits the finer details of Search Providers and focuses mainly on how this tool uses AJAX navigation.
Concepts presented here represent a "wizard"-style interface; there are a series of ordered steps represented as panels. These panels either present information or wait for user action, the same concept used by setup applications. In-page navigations are a great solution for wizards; the hash can be used to store a user's current position within the wizard. In this case, there are four steps:
Ask the user for the name of a Search Engine.
Have the user perform a search query using the word "TEST" and place the URL of the result into a textbox.
Ask the user for some metadata.
Provide the user with information—in this case, a new Search Provider extension that can be either installed or downloaded to the system.
Figure 3-15 shows each of these four panels.
Listing 3-18 highlights the code for this example that pertains to in-page navigations. The page begins by throwing the onload
event, which calls the onHashChange()
method. This method reads the current hash value; it represents the current position within the wizard (which can range from minStep
()1 to maxStep
(4). If the window.location.hash
value is empty or outside of this range, the number is set to minStep
. The loadPanelForStep()
method is called once the hash is normalized; this method displays the panel <div>
associated with the current step.
Example 3.18. Sample code for the Search Provider Generator using in-page navigation
<html> <head> <title>AJAX Navigation</title> </head> <body> <h2 id="title">Search Provider Wizard</h2> <div id="stepPanel1"> <!-- ... --> </div> <div id="stepPanel2"> <!-- ... --> </div> <div id="stepPanel3"> <!-- ... --> </div> <div id="stepPanel4"> <!-- ... --> </div>
<p> <button id="previousStep" onclick="previous()">Previous Step</button> <button id="nextStep" onclick="next()">Next Step</button> <script type="text/javascript"> // Define the current, min, and max steps. var currentStep = 1; var minStep = 1; var maxStep = 4; // ... function next() { // Stop if the current step is already at/above max if(currentStep >= maxStep) return; // Increase the current position, set that position to the // hash, and display the panel for this new step currentStep++; window.location.hash = currentStep; loadPanelForStep(); } function previous() { // Stop if the current step is already at/below min if(currentStep <= minStep) return; // Decrease the current position, set that position to the hash, and // display the panel for this new step currentStep--; window.location.hash = currentStep; loadPanelForStep(); } function onHashChange() { // Grab the step specified by the hash as an Integer var hashStep = parseInt(window.location.hash.substr(1)); // If the hash value isn't a valid number, start at the first step. If // it is out of bounds, snap to the closest limit. Otherwise, set the // current step to be the one specified in the hash if(isNaN(hashStep)) currentStep = minStep; else if(hashStep < minStep) currentStep = minStep; else if(hashStep > maxStep) currentStep = minStep; else currentStep = hashStep; // Display panel for the current step loadPanelForStep(); }
function loadPanelForStep() { // Disable the previous button if on the first panel and // disable the next button of on the last document.getElementById("previousStep").disabled = (currentStep <= minStep) ? true : false; document.getElementById("nextStep").disabled = (currentStep >= maxStep) ? true : false; // Show the current panel and hide all others for(var i = minStep; i <= maxStep; i++) { var display = ((i == currentStep) ? "block" : "none"); document.getElementById("stepPanel" + i).style.display = display; } // ... } // ... </script> </body> </html>
The "Previous Step" and "Next Step" buttons (previousStep
and nextStep
, respectively) move backwards and forward through the steps in the wizard. When previousStep
is clicked, its onclick
handler subtracts 1 from the currentStep
position if possible and calls loadPanelForStep()
to display the panel for this new value. The nextStep
button does the same, but instead increases currentStep
by 1. Since these panels are located on the same page, the data input into each remains while the user remains within the page context. Finally, each of these methods set the new hash value to window.location.hash
—this "registers" the new in-page navigation with the travel log, thus making it available to IE's back and forward buttons as well as navigation history.
The last major piece of AJAX navigation is integration with the browser's forward and back buttons. The onhashchange
event handler of the <body>
tag is raised whenever these buttons refer back to a location within the page; to grab these events, the same onHashChange()
method used for the onload
handler is used to catch these events. This method reads the current hash value, normalizes it, and navigates to the step provided by this value.
The HTTP 1.0 and 1.1 specifications suggest limits to the number of concurrent connections a requesting application (in this case, the browser) should make to each host server. These limits were based upon the infrastructure of the era (the 1990s). While the limits were wise at the time, improvements in network infrastructure have caused most browser developers to increase their connection limits.
Current versions of major browsers increased connection limits in order to speed page load and allow AJAX applications to utilize more concurrent connections. Internet Explorer 8 increases this number to 6 connections per domain in non-modem configurations. Comparative values are shown in Table 3-1.
IE8 also exposes a new property on the window
object called macConnectionsPerServer
. Scripts can query this property, potentially offering varying behavior and functionality based on the number of concurrent connections available.
Properties
maxConnectionsPerServer
property - Returns number of connections available per domain based on the server HTTP version and the connection type. This property is read-only.
The page in Listing 3-19 attempts to download eight images from a server located at examples.proiedev.com
.
Example 3.19. Code sample that highlights how concurrent connections operate
<html> <head> <title>Concurrent Connections</title> </head> <body> <h3>Maximum connections per host: <span id="maxCon"></span></h3> <img src="images/1.jpg"><br> <img src="images/2.jpg"><br> <img src="images/3.jpg"><br> <img src="images/4.jpg"><br> <img src="images/5.jpg"><br> <img src="images/6.jpg"><br> <img src="images/7.jpg"><br> <img src="images/8.jpg"> <script type="text/javascript"> // If the number of maxConnectionsPerServer is readable from // script, display that value to the screen var spanMaxCon = document.getElementById("maxCon"); if(window.maxConnectionsPerServer) setText(spanMaxCon, window.maxConnectionsPerServer); else setText(spanMaxCon, "—"); // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ }
</script> </body> </html>
The page, when running in IE8 and using a broadband connection, can download up to 6 items at a time from the server. In theory, this means the first six images in the sample will start downloading around the same time, and the seventh will begin when the first completes.
Figure 3-16 shows a network request timeline generated with Excel based on timing information captured by the Fiddler Web Debugger. When the example page is loaded, Internet Explorer checks the number of connections per host. Trident's parser subsystem (described in Chapter 1) issues download requests for embedded resources. The download subsystem begins issuing requests until the connections-per-host limit is reached, at which point subsequent requests are queued until a connection becomes free. In this case, six image files are downloading concurrently, and the seventh begins when an earlier request completes and frees a connection slot.
The 6 connection limit is directly tied to a specific hostname. Some websites use this definition to circumvent browser connection limits; by using multiple hostnames or CDNs (Content Delivery Networks), pages can increase the number of available concurrent connections. For example, a page served from the hostname examples.proiedev.com
could increase the number of images downloaded concurrently by a client browser by serving some content on another hostname like www.proiedev.com
.
The use of mixed content or mixed origins carries with it a risk of cross-site scripting vulnerabilities and request forgeries. Developers must take care to ensure that data transferred between origins is sanitized before it's use. The next few sections outline features of IE8 that help developers build secure cross-domain and cross-site communication channel which such mixing is necessary.
The XDomainRequest
object (and enhanced versions of XMLHttpRequest
found in Firefox 3.5 and Safari 4) provides an asynchronous communication channel for domains with a pre-established trust relationship.
XDomainRequest
is similar to XMLHttpRequest
in terms of core functionality—one page (the requestee) makes a request to a remote page (the requestor) and, upon receiving and validating the request, the requestor returns a response to the requestee. Unlike XMLHttpRequest, XDomainRequest
assumes that it's being used by two origins that have a trust relationship. The requestor confirms such trust by including the requestee's domain in a response header. It omits certain security features such as credential handling and cookie support as a result.
Objects
XDomainRequest object
– A lightweight, asynchronous request object that enforces cross-domain access controls defined by the W3C Access Control for Cross-Site Requests standard.
Properties
contentType
property – Same as XMLHttpRequest.contentType
.
responseText
property – Same as XMLHttpRequest.responseText
.
timeout
property – Same as XMLHttpRequest.timeout
.
Methods
abort
method – Same as XMLHttpRequest.abort()
.
open
method – Same as XMLHttpRequest.open()
.
send
method – Same as XMLHttpRequest.send()
.
Events
onload
event - Raises a callback method when the requested server returns a response. This is similar the onreadystatechange
event in XMLHttpRequest
, but it only fires when a transaction is complete (thus there is no readyState
property).
onerror
event – Same as XMLHttpRequest
's onerror
event.
onprogress
event – Same as XMLHttpRequest
's onprogress
event.
ontimeout
event – Same as XMLHttpRequest
's ontimeout
event.
The XDomainRequest
object does not have an onreadystatechange
event. Instead, the onload
event is fired when a transaction is finished. This coincides with the "completed" state (or readyState == 4
) of the XMLHttpRequestObject
's onreadystatechange
event.
Asynchronous, origin-restricted communication is not limited to IE8. Firefox 3.5 and Safari 4 have also added this functionality. Instead of using the XDomainRequest object, this functionality was placed directly on the XMLHttpRequest
object. This topic is discussed in-depth in the following sections.
When writing scripts that are expected to run in all browsers that support Cross-Domain Requests, use the XDomainRequest object in IE8 and XMLHttpRequest for other browsers such as Firefox 3.5+ and Safari4+.
The sample page in Listing 3-20 attempts to access origin-restricted resources found at both examples.proiedev.com
and www.ie-examples.com
. The origin of the requesting page is http://examples.proiedev.com
. Script in this sample uses either Internet Explorer 8's XDomainRequest
object or origin-controlled versions of XMLHttpRequest
(in browsers that support it) to attempt a trusted connection with the server. The proper object is chosen by detecting the browser with the Peter-Paul Koch's BrowserDetect.js script; it can be downloaded from http://link.proiedev.com/browserdetect
.
Example 3.20. Code sample using Cross-Domain Requests
<html> <head> <title>Cross-Domain Requests</title> </head> <body> <div id="resultDiv"> <h3>Request to examples.proiedev.com: <span id="xdrAllowed"></span></h3> <h3>Request to www.ie-examples.com: <span id="xdrBlocked"></span></h3> </div> <!-- Include PPK's BrowserDetect script from QuirksMode.org --> <script type="text/javascript" src="browserdetect.quirksmode.js"></script> <script type="text/javascript"> // Create variables to hold XDomainRequest or instances of // XMLHttpRequest that offer origin header support var xdrAllowed = null; var xdrBlocked = null; // Callback functions for onload and onerror events in // XDomainRequest/XMLHttpRequest function onLoadAllowed() { displayEvent("xdrAllowed", xdrAllowed.responseText); } function onLoadBlocked() { displayEvent("xdrBlocked", xdrBlocked.responseText); } function onErrorAllowed() { displayEvent("xdrAllowed", "Error") } function onErrorBlocked() { displayEvent("xdrBlocked", "Error") } // Create XDomainRequest objects (or XMLHttpRequest objects // if not IE and on a browser that offers origin header support) if(window.XDomainRequest) { xdrAllowed = new XDomainRequest(); xdrBlocked = new XDomainRequest(); } else if((BrowserDetect.browser = "Firefox" && BrowserDetect.version > 3.5) || (BrowserDetect.browser = "Safari" && BrowserDetect.version > 4)) { xdrAllowed = new XMLHttpRequest(); xdrBlocked = new XMLHttpRequest(); } // Only proceed if cross-domain protections are available
// otherwise write a "not-available" message to the page if(xdrAllowed != null && xdrBlocked != null) { // Point the xdrAllowed object to examples.proiedev.com xdrAllowed.onload = onLoadAllowed; xdrAllowed.onerror = onErrorAllowed; xdrAllowed.open("GET", "http://examples.proiedev.com/03/xcomm/xdr/alloworigin/allow.php", true ); // Point the xdrAllowed object to www.ie-examples.com xdrBlocked.onload = onLoadBlocked; xdrBlocked.onerror = onErrorBlocked; xdrBlocked.open("GET", "http://www.ie-examples.com/03/xcomm/xdr/alloworigin/block.php", true ); // Send requests to each domain xdrAllowed.send(null); xdrBlocked.send(null); } else displayNotAvailableMessage(); // Display a message on the page indicating origin header support // isn't available on the current browser function displayNotAvailableMessage() { var resultDiv = document.getElementById("resultDiv") resultDiv.innerHTML = "<h3>XDomainRequest and origin-restricted XMLHttpRequest " + "are not available in this browser.</h3>"; } // Generic function to display data in an element function displayEvent(id, text) { var element = document.getElementById(id); setText(element, text); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
The xdrAllowed
and xdrBlocked
objects are initialized to either an XDomainRequest
or an XMLHttpRequest
object. They each register onload
and onerror
event handlers and open a GET
request to http://examples.proiedev.com
and http://www.ie-examples.com
, respectively.
After the page loads, the xdrAllowed
and xdrBlocked
objects open a connection with their respective servers. If a given request is successful, the onload
event callback is executed; the onerror
callback is run if that request is denied.
Webpages that receive a request via XDomainRequest
or access-control-enabled XMLHttpRequest
objects may use the Access-Control-Allow-Origin
HTTP response header to indicate whether or not a resource is accessible. The header may contain one of two values pertaining to allowed origins: either an asterisk (*
), which indicates the response page is intended for all origins, or a specific origin containing a protocol and domain (e.g. http://examples.proiedev.com
). This header does not support multiple URLs, but a web application can simulate this functionality by serving different headers through logic switching using the HTTP request's Origin value.
Example 3.21. Response allowing requests from examples.proiedev.com
<?php header('Access-Control-Allow-Origin: http://examples.proiedev.com'), header('Content-Type: text/plain'), echo "Success!"; ?>
Listing 3-21 is a sample response page requested by the xdrAllowed
object in Listing 3-20. In this case, the server includes an Access-Control-Allow-Origin-Header
header in its response, pointing to http://examples.proiedev.com
. Since the requesting page matched this origin, IE displayed the response data after reading the server headers.
Listing 3-22 is the same exact response page that was used in the prior example. Unlike the other response, this page is hosted on the domain www.ie-examples.com
and requires the origin of any request derive from http://www.ie-examples.com
. The xdrBlocked
object throws an error once IE sees that the allowed origin doesn't match the page making the request.
Examples in Listings 3-20, 3-21, and 3-22 represent a sample a request/response sequence using the cross-domain request object.
The example page in Figure 3-17 is a screenshot of the request page in Listing 3-20. On load, the page makes two Cross-Domain Requests from the URL http://examples.proiedev.com
. The first request made is by the xdrAllowed
object to a page hosted on the same domain, http://examples.proiedev.com
. The response handler in Listing 3-21 allows for requests originating from http://examples.proiedev.com
, thus the transaction is successful and the transaction's responseText
value is displayed after xdrAllowed
's onload
even is raised.
The second request is made by xdrBlocked
to a response page hosted on a different domain, http://www.ie-examples.com
. The second response page from Listing 3-22 only allows for requests originating from http://www.ie-examples.com
, thus it denies the access request. The xdrBlocked
object raises its onerror
event upon rejection, and the callback displays the word "Error" on the screen.
The relatively recent introduction of the W3C Access Control for Cross-Site Requests
(the standard covering cross-domain request functionality) means that current cross-browser support is limited. At the time of IE8's release, Firefox 3.5 and Safari 4 supported this specification as an extension of the XMLHttpRequest
object, whereas IE uses a separate XDomainRequest
object. IE and other browsers also differ in the access control headers they support; IE, for instance, only recognizes the Access-Control-Allow-Origin
header, whereas Firefox and Safari implement most if not all headers outlined in the W3C's specification.
Listing 3-23 provides some sample code that demonstrates a cross-browser implementation of cross-domain access control in JavaScript.
Example 3.23. Cross-Browser compatible Cross-Domain Request sample
<html> <head> <title>Building Interoperable Cross-Domain Requests</title> </head> <body> <div id="resultDiv"> <h3>XDR over XDomainRequest? <span id="useXDR">No</span></h3> <h3>XDR over XMLHttpRequest? <span id="useXHR">No</span></h3> </div> <!-- Include PPK's BrowserDetect script from QuirksMode.org --> <script type="text/javascript" src="browserdetect.quirksmode.js"></script> <script type="text/javascript"> // Create variables to hold XDomainRequest or instances of // XMLHttpRequest that offer origin header support var xdrAllowed = null; var xdrBlocked = null;
// Create XDomainRequest objects (or XMLHttpRequest objects // if not IE and on a browser that offers origin header support) if(window.XDomainRequest) { xdrAllowed = new XDomainRequest(); xdrBlocked = new XDomainRequest(); setText(document.getElementById("useXDR"), "Yes"); } else if((BrowserDetect.browser = "Firefox" && BrowserDetect.version > 3.5) || (BrowserDetect.browser = "Safari" && BrowserDetect.version > 4)) { xdrAllowed = new XMLHttpRequest(); xdrBlocked = new XMLHttpRequest(); setText(document.getElementById("useXHR"), "Yes"); } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
The sample itself implements only those features of cross-domain access control that are interoperable: the existence of an object that supports access control, and the Access-Control-Allow-Origin
header.
In addition to a having a different object name versus other browsers (XDomainRequest versus XMLHttpRequest), Internet Explorer 8 only supports the Access-Control-Allow-Origin HTTP header.
Cross-frame communication (that between a page and <iframe>
) is been a pain point for developers building pages that contain multiple origins—commonly known as "mashups." IE and other browsers have put some strict security measures in place around what <iframe>
objects and their hosts can do and how they can communicate. This isolation, however necessary, neuters communication between them. Developers have worked around this limitation by treating the document.location.hash
as a mutual data store.
The HTML5 spec addresses establishes postMessage()
and the onmessage
event as a way for parents and their child <iframe>
object to communicate. They can send and receive data between each other using this method as well as force the host browser to enforce origin-based restrictions on request.
Internet Explorer 8 implements this feature—the postMessage()
method and onmessage
event— for all document modes.
Objects
postMessage(msg [, targetOrigin])
method - Sends a message specified in the string msg
. Further restrictions are placed through targetOrigin
, which restricts the origin of content permitted to receive the message.
Events
onmessage
event - Fired when a target window object receives a message sent using the postMessage()
method. The message itself is stored in a property on the event object.
origin
property - Returns the Origin of the document that sent the message.
data
property - Returns the message sent by the origin
document.
type
property - The type of event, in this case message
.
The code sample in Listing 3-24 shows a parent document that hosts an <iframe>
. In IE8, these two documents can send messages to each other with postMessage
. This page contains an <input>
textbox and a submit button; the onclick
event on the submit button sends the content of the <input>
textbox to the iframe through postMessage
located on the <iframe>
's contentWindow
instance.
Example 3.24. Code for a document parent hosting a frame object and communicating with postMessage
<html> <head> <title>Cross Frame Messaging with postMessage</title> </head> <body> <h3>Parent Document (examples.proiedev.com)</h3> Post message to remote document (www.ie-examples.com): <input id="postDataInput" size="25" type="text"> <input onclick="postToRemote();" type="submit" value="Post Message"><br><br> <iframe id="remoteFrame" src="http://www.ie-examples.com/03/xcomm/xdm/remote" width="400px" height="200px" class="highlightBorder" frameborder="no"></iframe> <script type="text/javascript"> // Post contents of an input box to a remote page function postToRemote() { // Grab the input value from the box (explicitly type as a string to // avoid pulling in a VT_NULL (IE Bug) and quit if empty var postData = String(document.getElementById("postDataInput").value); if (postData == "") return; // Use postMessage to send the string message over to the remote page var remote = document.getElementById("remoteFrame"); remote.contentWindow.postMessage(postData, "http://www.ie-examples.com"); } </script> </body> </html>
The parent page uses postMessage
's targetOrigin
parameter to restrict what page can receive the message; even if an injected script manages to change the location of the parent's <iframe>
, IE will only transport the message to a target at http://www.ie-examples.com
.
Listing 3-25 is the code for the "receiver" document hosted in the parent's <iframe>
. The script opts-in to messages from the parent by attaching itself to the onmessage
event handler; using attachEvent()
in IE and with addEventListener()
in other browsers. When script on the parent page calls postMessage()
, the onmessage
event is fired in this page and the receiveData()
callback is executed. Script writes data from the event to the screen whenever a message is received.
Example 3.25. Code sample for child frame using the onmessage event
<html> <head> <title>Cross Frame Messaging with postMessage (Receiver)</title> </head> <body> <h3>Remote Page (www.ie-examples.com)</h3> Message Origin (e.origin): <span id="receivedDataOrigin"></span><br> Message Contents (e.data): <span id="receivedDataContents"></span><br> Message Type (e.type): <span id="receivedDataType"></span><br> <script type="text/javascript"> // Point the onmessage event callback to receiveData, either by // using addEventListener or attachEvent if(window.addEventListener) window.addEventListener("message", receiveData, false); else window.attachEvent("onmessage", receiveData); // Grab messages from the parent through this callback/event e function receiveData(e) { // Make sure that the origin server is examples.proiedev.com if (e.data != "" && e.origin == "http://examples.proiedev.com") { // Write message data to the webpage setText(document.getElementById("receivedDataOrigin"), e.origin); setText(document.getElementById("receivedDataContents"), e.data); setText(document.getElementById("receivedDataType"), e.type); } } // Set element text (cross-browser innerText/textContent) function setText(element, text) { /*...*/ } </script> </body> </html>
Figure 3-18 shows the pages described in the prior two code samples. The parent document, located at examples.proiedev.com
, hosts an <iframe>
that loads a page from www.ie-examples.com
.
A The parent calls postMessage()
method to send a message (the contents of an <input>
box) to the child frame when the "Post Message" <input>
is clicked. The child frame receives notification of the message from the onmessage
event, and promptly displays that message to the user.
IE does not permit webpages to communicate with popup windows or tabs through postMessage(), even those created by a webpage itself; Firefox and other browsers are more lenient.
The following tips and tricks provide insight into building websites that communicate across documents and domains securely.
Don't mix protocols. Ensure that HTTPS pages do not rely on content served over HTTP. Mixed content is more than just an annoying dialog; trusted connections could be compromised if insecure content is added to a secure document.
Sanitize incoming data. Web applications should sanitize all input being used by the web server. Data should be sanitized using server-side code and client-side methods such as toStaticHTML()
.
Use postMessage() to communicate between documents. It's reliable, broadly supported, and allows for mutually-suspicious cross-origin communication..
Protect against threats on both sides. Insecure connections are subject to man-in-the-middle attacks, and servers must not assume that the browser is a trustworthy client.
IE8's changes with respect to AJAX and JSON allow developers to take advantage of some more advanced scenarios that simplify development of dynamic web applications. I used the samples to provide ways of using these features in an interoperable way and have highlighted some of the pitfalls and quirks that you could encounter when testing these features in IE. In the next chapter, I'll focus on some more interoperability scenarios regarding the Document Object Model and IE's JavaScript engine.
18.220.136.165