// Copyright 2011 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Provides data persistence using IE userData mechanism. * UserData uses proprietary Element.addBehavior(), Element.load(), * Element.save(), and Element.XMLDocument() methods, see: * http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx. * */ goog.provide('goog.storage.mechanism.IEUserData'); goog.require('goog.asserts'); goog.require('goog.iter.Iterator'); goog.require('goog.iter.StopIteration'); goog.require('goog.storage.mechanism.ErrorCode'); goog.require('goog.storage.mechanism.IterableMechanism'); goog.require('goog.structs.Map'); goog.require('goog.userAgent'); /** * Provides a storage mechanism using IE userData. * * @param {string} storageKey The key (store name) to store the data under. * @param {string=} opt_storageNodeId The ID of the associated HTML element, * one will be created if not provided. * @constructor * @extends {goog.storage.mechanism.IterableMechanism} * @final */ goog.storage.mechanism.IEUserData = function(storageKey, opt_storageNodeId) { /** * The key to store the data under. * * @private {?string} */ this.storageKey_ = storageKey; /** * The document element used for storing data. * * @private {Element} */ this.storageNode_ = null; goog.storage.mechanism.IEUserData.base(this, 'constructor'); // Tested on IE6, IE7 and IE8. It seems that IE9 introduces some security // features which make persistent (loaded) node attributes invisible from // JavaScript. if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) { if (!goog.storage.mechanism.IEUserData.storageMap_) { goog.storage.mechanism.IEUserData.storageMap_ = new goog.structs.Map(); } this.storageNode_ = /** @type {Element} */ ( goog.storage.mechanism.IEUserData.storageMap_.get(storageKey)); if (!this.storageNode_) { if (opt_storageNodeId) { this.storageNode_ = document.getElementById(opt_storageNodeId); } else { this.storageNode_ = document.createElement('userdata'); // This is a special IE-only method letting us persist data. this.storageNode_['addBehavior']('#default#userData'); document.body.appendChild(this.storageNode_); } goog.storage.mechanism.IEUserData.storageMap_.set( storageKey, this.storageNode_); } try { // Availability check. this.loadNode_(); } catch (e) { this.storageNode_ = null; } } }; goog.inherits( goog.storage.mechanism.IEUserData, goog.storage.mechanism.IterableMechanism); /** * Encoding map for characters which are not encoded by encodeURIComponent(). * See encodeKey_ documentation for encoding details. * * @type {!Object} * @const */ goog.storage.mechanism.IEUserData.ENCODE_MAP = { '.': '.2E', '!': '.21', '~': '.7E', '*': '.2A', '\'': '.27', '(': '.28', ')': '.29', '%': '.' }; /** * Global storageKey to storageNode map, so we save on reloading the storage. * * @type {goog.structs.Map} * @private */ goog.storage.mechanism.IEUserData.storageMap_ = null; /** * Encodes anything other than [-a-zA-Z0-9_] using a dot followed by hex, * and prefixes with underscore to form a valid and safe HTML attribute name. * * We use URI encoding to do the initial heavy lifting, then escape the * remaining characters that we can't use. Since a valid attribute name can't * contain the percent sign (%), we use a dot (.) as an escape character. * * @param {string} key The key to be encoded. * @return {string} The encoded key. * @private */ goog.storage.mechanism.IEUserData.encodeKey_ = function(key) { // encodeURIComponent leaves - _ . ! ~ * ' ( ) unencoded. return '_' + encodeURIComponent(key).replace(/[.!~*'()%]/g, function(c) { return goog.storage.mechanism.IEUserData.ENCODE_MAP[c]; }); }; /** * Decodes a dot-encoded and character-prefixed key. * See encodeKey_ documentation for encoding details. * * @param {string} key The key to be decoded. * @return {string} The decoded key. * @private */ goog.storage.mechanism.IEUserData.decodeKey_ = function(key) { return decodeURIComponent(key.replace(/\./g, '%')).substr(1); }; /** * Determines whether or not the mechanism is available. * * @return {boolean} True if the mechanism is available. */ goog.storage.mechanism.IEUserData.prototype.isAvailable = function() { return !!this.storageNode_; }; /** @override */ goog.storage.mechanism.IEUserData.prototype.set = function(key, value) { this.storageNode_.setAttribute( goog.storage.mechanism.IEUserData.encodeKey_(key), value); this.saveNode_(); }; /** @override */ goog.storage.mechanism.IEUserData.prototype.get = function(key) { // According to Microsoft, values can be strings, numbers or booleans. Since // we only save strings, any other type is a storage error. If we returned // nulls for such keys, i.e., treated them as non-existent, this would lead // to a paradox where a key exists, but it does not when it is retrieved. // http://msdn.microsoft.com/en-us/library/ms531348(v=vs.85).aspx var value = this.storageNode_.getAttribute( goog.storage.mechanism.IEUserData.encodeKey_(key)); if (!goog.isString(value) && !goog.isNull(value)) { throw goog.storage.mechanism.ErrorCode.INVALID_VALUE; } return value; }; /** @override */ goog.storage.mechanism.IEUserData.prototype.remove = function(key) { this.storageNode_.removeAttribute( goog.storage.mechanism.IEUserData.encodeKey_(key)); this.saveNode_(); }; /** @override */ goog.storage.mechanism.IEUserData.prototype.getCount = function() { return this.getNode_().attributes.length; }; /** @override */ goog.storage.mechanism.IEUserData.prototype.__iterator__ = function(opt_keys) { var i = 0; var attributes = this.getNode_().attributes; var newIter = new goog.iter.Iterator(); newIter.next = function() { if (i >= attributes.length) { throw goog.iter.StopIteration; } var item = goog.asserts.assert(attributes[i++]); if (opt_keys) { return goog.storage.mechanism.IEUserData.decodeKey_(item.nodeName); } var value = item.nodeValue; // The value must exist and be a string, otherwise it is a storage error. if (!goog.isString(value)) { throw goog.storage.mechanism.ErrorCode.INVALID_VALUE; } return value; }; return newIter; }; /** @override */ goog.storage.mechanism.IEUserData.prototype.clear = function() { var node = this.getNode_(); for (var left = node.attributes.length; left > 0; left--) { node.removeAttribute(node.attributes[left - 1].nodeName); } this.saveNode_(); }; /** * Loads the underlying storage node to the state we saved it to before. * * @private */ goog.storage.mechanism.IEUserData.prototype.loadNode_ = function() { // This is a special IE-only method on Elements letting us persist data. this.storageNode_['load'](this.storageKey_); }; /** * Saves the underlying storage node. * * @private */ goog.storage.mechanism.IEUserData.prototype.saveNode_ = function() { try { // This is a special IE-only method on Elements letting us persist data. // Do not try to assign this.storageNode_['save'] to a variable, it does // not work. May throw an exception when the quota is exceeded. this.storageNode_['save'](this.storageKey_); } catch (e) { throw goog.storage.mechanism.ErrorCode.QUOTA_EXCEEDED; } }; /** * Returns the storage node. * * @return {!Element} Storage DOM Element. * @private */ goog.storage.mechanism.IEUserData.prototype.getNode_ = function() { // This is a special IE-only property letting us browse persistent data. var doc = /** @type {Document} */ (this.storageNode_['XMLDocument']); return doc.documentElement; };