Section 19.5. Cookie Alternatives


19.5. Cookie Alternatives

There are a couple of drawbacks to using cookies for client-side persistence:

  • They are limited to 4 KB of data.

  • Even when cookies are used only for client-side scripting, they are still uploaded to the web server in the request for any web page with which they are associated. When the cookies are not used on the server, it's a waste of bandwidth.

Two cookie alternatives exist. Microsoft Internet Explorer and the Adobe Flash plug-in both define proprietary mechanisms for client-side persistence. Although neither is standard, both IE and Flash are widely deployed, which means that at least one of these mechanisms is available in a large majority of browsers. The IE and Flash persistence mechanisms are briefly described in the sections that follow, and the chapter concludes with an advanced example that provides persistent storage with IE, Flash, or cookies.

19.5.1. IE userData Persistence

Internet Explorer enables client-side persistence with a DHTML behavior. To access this mechanism, you apply a special behavior to an element (such as <div>) of your document. One way to do this is with CSS:

 <!-- This stylesheet defines a class named "persistent" --> <style>.persistent { behavior:url(#default#userData);}</style> <!-- This <div> element is a member of that class --> <div  ></div> 

Since the behavior attribute is not standard CSS, other web browsers simply ignore it. You can also set the behavior style attribute on an element with JavaScript:

 var memory = document.getElementById("memory"); memory.style.behavior = "url('#default#userData')"; 

When an HTML element has this "userData" behavior associated with it, new methods (defined by the behavior) become available for that element.[*] To store data persistently, set attributes of the element with setAttribute( ) and then save those attributes with save( ):

[*] The userData behavior is just one of four persistence-related behaviors available to Internet Explorer. See http://msdn.microsoft.com/workshop/author/persistence/overview.aspfor further details on persistence in IE.

 var memory = document.getElementById("memory");       // Get persistent element memory.setAttribute("username", username);            // Set data as attributes memory.setAttribute("favoriteColor", favoriteColor); memory.save("myPersistentData");                      // Save the data 

Note that the save( ) method takes a string argument: this is the (arbitrary) name under which the data is to be stored. You'll need to use the same name when you retrieve the data.

Data saved using the IE persistence mechanism can be given an expiration date, just as cookie data can. To do this, simply set the expires property before calling the save( ) method. This property should be set to a string in the form returned by Date.toUTCString( ). For example, you might add the following lines to the previous code to specify an expiration date 10 days in the future:

 var now = (new Date( )).getTime( );                     // now, in milliseconds var expires = now + 10 * 24 * 60 * 60 * 1000;             // 10 days from now in ms memory.expires = (new Date(expires)).toUTCString( );     // convert to a string 

To retrieve persistent data, reverse these steps, calling load( ) to load saved attributes and calling getAttribute( ) to query attribute values:

 var memory = document.getElementById("memory");   // Get persistent element memory.load("myPersistentData");                  // Retrieve saved data by name var user = memory.getAttribute("username");       // Query attributes var color = memory.getAttribute("favoriteColor"); 

19.5.1.1. Storing hierarchical data

The userData persistence behavior is not limited to storing and retrieving the values of attributes. Any element to which this behavior is applied has a complete XML document associated with it. Applying the behavior to an HTML element creates an XMLDocument property on the element, and the value of this property is a DOM Document object. You can use DOM methods (see Chapter 15) to add content to this document before calling save( ) or to extract content after calling load( ). Here's an example:

 var memory = document.getElementById("memory");    // Get persistent element var doc = memory.XMLDocument;                      // Get its document var root = doc.documentElement;                    // Root element of document root.appendChild(doc.createTextNode("data here")); // Store text in document 

The use of an XML document enables the storage of hierarchical data; you might convert a tree of JavaScript objects to a tree of XML elements, for example.

19.5.1.2. Storage limits

The IE persistence mechanism allows much more data to be stored than cookies do. Each page may store up to 64 KB, and each web server is allowed a total of 640 KB. Sites on a trusted intranet are allowed even more storage. There is no documented way for an end user to alter these storage limits or to disable the persistence mechanism altogether.

19.5.1.3. Sharing persistent data

Like cookies, data stored with the IE persistence mechanism is available to all web pages in the same directory. Unlike cookies, however, a web page cannot access persistent data saved by pages in its ancestor directories using IE. Also, the IE persistence mechanism has no equivalent to the path and domain attributes of cookies, so there is no way to share persistent data more widely among pages. Finally, persistent data in IE is shared only between pages in the same directory, loaded via the same protocols. That is, data stored by a page loaded with the https: protocol cannot be accessed by a page loaded with regular http:.

19.5.2. Flash SharedObject Persistence

The Flash plug-in, versions 6 and later, enables client-side persistence with the SharedObject class, which can be scripted with ActionScript code in a Flash movie.[*] To do this, create a SharedObject with ActionScript code like the following. Note that you must specify a name (like a cookie name) for your persistent data:

[*] For complete details on the SharedObject class and Flash-based persistence, see the Adobe web site at http://www.adobe.com/support/flash/action_scripts/local_shared_object/. I learned about Flash-based persistence from Brad Neuberg, who pioneered its use from client-side JavaScript with his AMASS project (http://codinginparadise.org/projects/storage/README.html). This project is evolving at the time of this writing, and you may find further information on client-side persistence at Brad's blog (http://codinginparadise.org).

 var so = SharedObject.getLocal("myPersistentData"); 

The SharedObject class does not define a load( ) method as the IE persistence mechanism does. When you create a SharedObject, any data previously saved under the specified name is automatically loaded. Each SharedObject has a data property. This data property refers to a regular ActionScript object, and the persistent data is available as properties of that object. To read or write persistent data, simply read or write the properties of the data object:

 var name = so.data.username;    // Get some persistent data so.data.favoriteColor = "red";  // Set a persistent field 

The properties you set on the data object are not limited to primitive types such as numbers and strings. Arrays, for example, are also allowed.

SharedObject does not have a save( ) method, either. It does have a flush( ) method that immediately stores the current state of the SharedObject. Calling this method is not necessary, however: properties set in the data object of a SharedObject are automatically saved when the Flash movie is unloaded. Note also that SharedObject does not support any way to specify an expiration date or lifetime for the persistent data.

Keep in mind that all the code shown in this section is ActionScript code run by the Flash plug-in, not JavaScript code running in the browser. If you want to use the Flash persistence mechanism in your JavaScript code, you need a way for JavaScript to communicate with Flash. Techniques for doing this are covered in Chapter 23. Example 22-12 demonstrates the use of the ExternalInterface class (available in Flash 8 and later), which makes it trivial to invoke ActionScript methods from JavaScript. Examples 19-3 and 19-4, later in this chapter, use a lower-level communication mechanism to connect JavaScript and ActionScript. The GetVariable( ) and SetVariable( ) methods of the Flash plug-in object enable JavaScript to query and set ActionScript variables, and the ActionScript fscommand( ) function sends data to JavaScript.

19.5.2.1. Storage limits

By default, the Flash player allows up to 100 KB of persistent data per web site. The user can adjust this limit down to 10 KB or up to 10 MB. Alternatively, the user can allow unlimited storage or disallow any persistent storage whatsoever. If a web site attempts to exceed the limit, Flash asks the user to allow or deny more storage for your web site.

19.5.2.2. Persistent data sharing

By default, persistent data in Flash is accessible only to the movie that created it. It is possible, however, to loosen this restriction so that two different movies in the same directory or anywhere on the same server can share access to persistent data. The way this is done is very similar to how it's done in the path attribute of a cookie. When you create a SharedObject with SharedObject.getLocal( ), you can pass a path as the second argument. This path must be a prefix of the actual path in the movie's URL. Any other movie that uses the same path can access the persistent data stored by this movie. For example, the following code creates a SharedObject that can be shared by any Flash movie that originates from the same web server:

 var so = SharedObject.getLocal("My/Shared/Persistent/Data",  // Object name                                "/");                         // Path 

When scripting the SharedObject class from JavaScript, you probably are not interested in sharing persistent data between Flash movies. Instead, you most likely care about sharing data between web pages that script the same movie (see Example 19-3 in the next section).

19.5.3. Example: Persistent Objects

This section concludes with an extended example that defines a unified API for the three persistence mechanisms you've studied in this chapter. Example 19-3 defines a PObject class for persistent objects. The PObject class works much like the Cookie class of Example 19-2. You create a persistent object with the PObject( ) constructor, to which you pass a name, a set of default values, and an onload handler function. The constructor creates a new JavaScript object and attempts to load persistent data previously stored under the name you specify. If it finds this data, it parses it into a set of name/value pairs and sets these pairs as properties of the newly created object. If it does not find any previously stored data, it uses the properties of the defaults object you specify. In either case, the onload handler function you specify is invoked asynchronously when your persistent data is ready for use.

Once your onload handler has been called, you can use the persistent data simply by reading the properties of the PObject as you would do with any regular JavaScript object. To store new persistent data, first set that data as properties (of type boolean, number, or string) of the PObject. Then call the save( ) method of the PObject, optionally specifying a lifetime (in days) for the data. To delete the persistent data, call the forget( ) method of the PObject.

The PObject class defined here uses IE-based persistence if it is running in IE. Otherwise, it checks for a suitable version of the Flash plug-in and uses Flash-based persistence if that is available. If neither of these options are available, it falls back on cookies.[*]

[*] It is also possible to define a persistence class that uses cookies whenever possible and falls back on IE or Flash-based persistence if cookies have been disabled.

Note that the PObject class allows only primitive values to be saved and converts numbers and booleans to strings when they are retrieved. It is possible to serialize array and object values as strings and parse those strings back into arrays and objects (see http://www.json.org, for example), but this example does not do that.

Example 19-3 is long but well commented and should be easy to follow. Be sure to read the long introductory comment that documents the PObject class and its API.

Example 19-3. PObject.js: persistent objects for JavaScript

 /**  * PObject.js: JavaScript objects that persist across browser sessions and may  *   be shared by web pages within the same directory on the same host.  *  * This module defines a PObject( ) constructor to create a persistent object.  * PObject objects have two public methods. save( ) saves, or "persists," the  * current properties of the object, and forget( ) deletes the persistent  * properties of the object. To define a persistent property in a PObject,  * simply set the property on the object as if it were a regular JavaScript  * object and then call the save( ) method to save the current state of  * the object. You may not use "save" or "forget" as a property name, nor  * any property whose name begins with $. PObject is intended for use with  * property values of type string. You may also save properties of type  * boolean and number, but these will be converted to strings when retrieved.  *  * When a PObject is created, the persistent data is read and stored in the  * newly created object as regular JavaScript properties, and you can use the  * PObject just as you would use a regular JavaScript object. Note, however,  * that persistent properties may not be ready when the PObject( ) constructor  * returns, and you should wait for asynchronous notification using an onload  * handler function that you pass to the constructor.  *  * Constructor:  *    PObject(name, defaults, onload):  *  * Arguments:  *  *    name       A name that identifies this persistent object. A single pages  *               can have more than one PObject, and PObjects are accessible  *               to all pages within the same directory, so this name should  *               be unique within the directory. If this argument is null or  *               is not specified, the filename (but not directory) of the  *               containing web page is used.  *  *    defaults   An optional JavaScript object. When no saved value for the  *               persistent object can be found (which happens when a PObject  *               is created for the first time), the properties of this object  *               are copied into the newly created PObject.  *  *    onload     The function to call (asynchronously) when persistent values  *               have been loaded into the PObject and are ready for use.  *               This function is invoked with two arguments: a reference  *               to the PObject and the PObject name. This function is  *               called *after* the PObject( ) constructor returns. PObject  *               properties should not be used before this.  *  * Method PObject.save(lifetimeInDays):  *   Persist the properties of a PObject. This method saves the properties of  *   the PObject, ensuring that they persist for at least the specified  *   number of days.  *  * Method PObject.forget( ):  *   Delete the properties of the PObject. Then save this "empty" PObject to  *   persistent storage and, if possible, cause the persistent store to expire.  *  * Implementation Notes:  *  * This module defines a single PObject API but provides three distinct  * implementations of that API. In Internet Explorer, the IE-specific  * "UserData" persistence mechanism is used. On any other browser that has an  * Adobe Flash plug-in, the Flash SharedObject persistence mechanism is  * used. Browsers that are not IE and do not have Flash available fall back on  * a cookie-based implementation. Note that the Flash implementation does not  * support expiration dates for saved data, so data stored with that  * implementation persists until deleted.  *  * Sharing of PObjects:  *  * Data stored with a PObject on one page is also available to other pages  * within the same directory of the same web server. When the cookie  * implementation is used, pages in subdirectories can read (but not write)  * the properties of PObjects created in parent directories. When the Flash  * implementation is used, any page on the web server can access the shared  * data if it cheats and uses a modified version of this module.  *  * Distinct web browser applications store their cookies separately and  * persistent data stored using cookies in one browser is not accessible using  * a different browser. If two browsers both use the same installation of  * the Flash plug-in, however, these browsers may share persistent data stored  * with the Flash implementation.  *  * Security Notes:  *  * Data saved through a PObject is stored unencrypted on the user's hard disk.  * Applications running on the computer can access the data, so PObject is  * not suitable for storing sensitive information such as credit card numbers,  * passwords, or financial account numbers.  */ // This is the constructor function PObject(name, defaults, onload) {     if (!name) { // If no name was specified, use the last component of the URL         name = window.location.pathname;         var pos = name.lastIndexOf("/");         if (pos != -1) name = name.substring(pos+1);     }     this.$name = name;  // Remember our name     // Just delegate to a private, implementation-defined $init( ) method.     this.$init(name, defaults, onload); } // Save the current state of this PObject for at least the specified # of days. PObject.prototype.save = function(lifetimeInDays) {     // First serialize the properties of the object into a single string     var s = "";                               // Start with empty string     for(var name in this) {                   // Loop through properties         if (name.charAt(0) == "$") continue;  // Skip private $ properties         var value = this[name];               // Get property value         var type = typeof value;              // Get property type         // Skip properties whose type is object or function         if (type == "object" || type == "function") continue;         if (s.length > 0) s += "&";           // Separate properties with &         // Add property name and encoded value         s += name + ':' + encodeURIComponent(value);     }     // Then delegate to a private implementation-defined method to actually     // save that serialized string.     this.$save(s, lifetimeInDays); }; PObject.prototype.forget = function( ) {     // First, delete the serializable properties of this object using the     // same property-selection criteria as the save( ) method.     for(var name in this) {         if (name.charAt(0) == '$') continue;         var value = this[name];         var type = typeof value;         if (type == "function" || type == "object") continue;         delete this[name];  // Delete the property     }     // Then erase and expire any previously saved data by saving the     // empty string and setting its lifetime to 0.     this.$save("", 0); }; // Parse the string s into name/value pairs and set them as properties of this. // If the string is null or empty, copy properties from defaults instead. // This private utility method is used by the implementations of $init( ) below. PObject.prototype.$parse = function(s, defaults) {     if (!s) {  // If there is no string, use default properties instead         if (defaults) for(var name in defaults) this[name] = defaults[name];         return;     }     // The name/value pairs are separated from each other by ampersands, and     // the individual names and values are separated from each other by colons.     // We use the split( ) method to parse everything.     var props = s.split('&'); // Break it into an array of name/value pairs     for(var i = 0; i < props.length; i++) { // Loop through name/value pairs         var p = props[i];         var a = p.split(':');     // Break each name/value pair at the colon         this[a[0]] = decodeURIComponent(a[1]); // Decode and store property     } }; /*  * The implementation-specific portion of the module is below.  * For each implementation, we define an $init( ) method that loads  * persistent data and a $save( ) method that saves it.  */ // Determine if we're in IE and, if not, whether we've got a Flash // plug-in installed and whether it has a high-enough version number var isIE = navigator.appName == "Microsoft Internet Explorer"; var hasFlash7 = false; if (!isIE && navigator.plugins) { // If we use the Netscape plug-in architecture    var flashplayer = navigator.plugins["Shockwave Flash"];    if (flashplayer) {    // If we've got a Flash plug-in        // Extract the version number        var flashversion = flashplayer.description;        var flashversion = flashversion.substring(flashversion.search("\\d"));        if (parseInt(flashversion) >= 7) hasFlash7 = true;    } } if (isIE) {  // If we're in IE     // The PObject( ) constructor delegates to this initialization function     PObject.prototype.$init = function(name, defaults, onload) {         // Create a hidden element with the userData behavior to persist data         var div = document.createElement("div");  // Create a <div> tag         this.$div = div;                          // Remember it         div.id = "PObject" + name;                // Name it         div.style.display = "none";               // Make it invisible         // This is the IE-specific magic that makes persistence work.         // The "userData" behavior adds the getAttribute( ), setAttribute( ),         // load( ), and save( ) methods to this <div> element. We use them below.         div.style.behavior = "url('#default#userData')";         document.body.appendChild(div);  // Add the element to the document         // Now we retrieve any previously saved persistent data.         div.load(name);  // Load data stored under our name         // The data is a set of attributes. We only care about one of these         // attributes. We've arbitrarily chosen the name "data" for it.         var data = div.getAttribute("data");         // Parse the data we retrieved, breaking it into object properties         this.$parse(data, defaults);         // If there is an onload callback, arrange to call it asynchronously         // once the PObject( ) constructor has returned.         if (onload) {             var pobj = this;  // Can't use "this" in the nested function             setTimeout(function( ) { onload(pobj, name);}, 0);         }     }     // Persist the current state of the persistent object     PObject.prototype.$save = function(s, lifetimeInDays) {         if (lifetimeInDays) { // If lifetime specified, convert to expiration             var now = (new Date( )).getTime( );             var expires = now + lifetimeInDays * 24 * 60 * 60 * 1000;             // Set the expiration date as a string property of the <div>             this.$div.expires = (new Date(expires)).toUTCString( );         }         // Now save the data persistently         this.$div.setAttribute("data", s); // Set text as attribute of the <div>         this.$div.save(this.$name);        // And make that attribute persistent     }; } else if (hasFlash7) { // This is the Flash-based implementation     PObject.prototype.$init = function(name, defaults, onload) {         var moviename = "PObject_" + name;    // id of the <embed> tag         var url = "PObject.swf?name=" + name; // URL of the movie file         // When the Flash player has started up and has our data ready,         // it notifies us with an FSCommand. We must define a         // handler that is called when that happens.         var pobj = this;  // for use by the nested function         // Flash requires that we name our function with this global symbol         window[moviename + "_DoFSCommand"] = function(command, args) {             // We know Flash is ready now, so query it for our persistent data             var data =  pobj.$flash.GetVariable("data")             pobj.$parse(data, defaults);    // Parse data or copy defaults             if (onload) onload(pobj, name); // Call onload handler, if any         };         // Create an <embed> tag to hold our Flash movie. Using an <object>         // tag is more standards-compliant, but it seems to cause problems         // receiving the FSCommand. Note that we'll never be using Flash with         // IE, which simplifies things quite a bit.         var movie = document.createElement("embed");  // element to hold movie         movie.setAttribute("id", moviename);          // element id         movie.setAttribute("name", moviename);        // and name         movie.setAttribute("type", "application/x-shockwave-flash");         movie.setAttribute("src", url);  // This is the URL of the movie         // Make the movie inconspicuous at the upper-right corner         movie.setAttribute("width", 1);  // If this is 0, it doesn't work         movie.setAttribute("height", 1);         movie.setAttribute("style", "position:absolute; left:0px; top:0px;");         document.body.appendChild(movie);  // Add the movie to the document         this.$flash = movie;               // And remember it for later     };     PObject.prototype.$save = function(s, lifetimeInDays) {         // To make the data persistent, we simply set it as a variable on         // the Flash movie. The ActionScript code in the movie persists it.         // Note that Flash persistence does not support lifetimes.         this.$flash.SetVariable("data", s); // Ask Flash to save the text     }; } else { /* If we're not IE and don't have Flash 7, fall back on cookies */     PObject.prototype.$init = function(name, defaults, onload) {         var allcookies = document.cookie;             // Get all cookies         var data = null;                              // Assume no cookie data         var start = allcookies.indexOf(name + '=');   // Look for cookie start         if (start != -1) {                            // Found it             start += name.length + 1;                 // Skip cookie name             var end = allcookies.indexOf(';', start); // Find end of cookie             if (end == -1) end = allcookies.length;             data = allcookies.substring(start, end);  // Extract cookie data         }         this.$parse(data, defaults);  // Parse the cookie value to properties         if (onload) {                 // Invoke onload handler asynchronously             var pobj = this;             setTimeout(function( ) { onload(pobj, name); }, 0);         }     };     PObject.prototype.$save = function(s, lifetimeInDays) {         var cookie = this.$name + '=' + s;          // Cookie name and value         if (lifetimeInDays != null)                 // Add expiration             cookie += "; max-age=" + (lifetimeInDays*24*60*60);         document.cookie = cookie;                   // Save the cookie     }; } 

19.5.3.1. ActionScript code for Flash persistence

The code in Example 19-3 is not complete as it stands. The Flash-based persistence implementation relies on a Flash movie named PObject.swf. This movie is nothing more than a compiled ActionScript file. Example 19-4 shows the ActionScript code.

Example 19-4. ActionScript code for Flash-based persistence

 class PObject {     static function main( ) {         // SharedObject exists in Flash 6 but isn't protected against         // cross-domain scripting until Flash 7, so make sure we've got         // that version of the Flash player.         var version = getVersion( );         version = parseInt(version.substring(version.lastIndexOf(" ")));         if (isNaN(version) || version < 7) return;         // Create a SharedObject to hold our persistent data.         // The name of the object is passed in the movie URL like this:         // PObject.swf?name=name         _root.so = SharedObject.getLocal(_root.name);         // Retrieve the initial data and store it on _root.data.         _root.data = _root.so.data.data;         // Watch the data variable. When it changes, persist its new value.         _root.watch("data", function(propName, oldValue, newValue) {                      _root.so.data.data = newValue;                      _root.so.flush( );                  });         // Notify JavaScript that it can retrieve the persistent data   now.         fscommand("init");     } } 

The ActionScript code is quite simple. It starts by creating a SharedObject, using a name specified (by JavaScript) in the query portion of the URL of the movie object. Creating this SharedObject loads the persistent data, which in this case is simply a single string. This data string is passed back to JavaScript with the fscommand( ) function that invokes the doFSCommand handler defined in JavaScript. The ActionScript code also sets up a handler function to be invoked whenever the data property of the root object changes. The JavaScript code uses SetVariable( ) to set the data property, and this ActionScript handler function is invoked in response, causing the data to be made persistent.

The ActionScript code shown in the PObject.as file of Example 19-4 must be compiled into a PObject.swf file before it can be used with the Flash player. You can do this with the open source ActionScript compiler mtasc (available from http://www.mtasc.org). Invoke the compiler like this:

 mtasc -swf PObject.swf -main -header 1:1:1 PObject.as 

mtasc produces a SWF file that invokes the PObject.main( ) method from the first frame of the movie. If you use the Flash IDE instead, you must explicitly call PObject.main( ) from the first frame. Alternatively, you can simply copy code from the main( ) method and insert it into the first frame.




JavaScript. The Definitive Guide
JavaScript: The Definitive Guide
ISBN: 0596101996
EAN: 2147483647
Year: 2004
Pages: 767

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net