/* Copyright (c) 2004-2008, The Dojo Foundation All Rights Reserved. Licensed under the Academic Free License version 2.1 or above OR the modified BSD license. For more information on Dojo licensing, see: http://dojotoolkit.org/book/dojo-book-0-9/introduction/licensing */ /* This is a compiled version of Dojo, built for deployment and not for development. To get an editable version, please visit: http://dojotoolkit.org for documentation and information on getting the source. */ if(!dojo._hasResource["dojox.storage.Provider"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.storage.Provider"] = true; dojo.provide("dojox.storage.Provider"); dojo.declare("dojox.storage.Provider", null, { // summary: A singleton for working with dojox.storage. // description: // dojox.storage exposes the current available storage provider on this // platform. It gives you methods such as dojox.storage.put(), // dojox.storage.get(), etc. // // For more details on dojox.storage, see the primary documentation // page at // http://manual.dojotoolkit.org/storage.html // // Note for storage provider developers who are creating subclasses- // This is the base class for all storage providers Specific kinds of // Storage Providers should subclass this and implement these methods. // You should avoid initialization in storage provider subclass's // constructor; instead, perform initialization in your initialize() // method. constructor: function(){ }, // SUCCESS: String // Flag that indicates a put() call to a // storage provider was succesful. SUCCESS: "success", // FAILED: String // Flag that indicates a put() call to // a storage provider failed. FAILED: "failed", // PENDING: String // Flag that indicates a put() call to a // storage provider is pending user approval. PENDING: "pending", // SIZE_NOT_AVAILABLE: String // Returned by getMaximumSize() if this storage provider can not determine // the maximum amount of data it can support. SIZE_NOT_AVAILABLE: "Size not available", // SIZE_NO_LIMIT: String // Returned by getMaximumSize() if this storage provider has no theoretical // limit on the amount of data it can store. SIZE_NO_LIMIT: "No size limit", // DEFAULT_NAMESPACE: String // The namespace for all storage operations. This is useful if several // applications want access to the storage system from the same domain but // want different storage silos. DEFAULT_NAMESPACE: "default", // onHideSettingsUI: Function // If a function is assigned to this property, then when the settings // provider's UI is closed this function is called. Useful, for example, // if the user has just cleared out all storage for this provider using // the settings UI, and you want to update your UI. onHideSettingsUI: null, initialize: function(){ // summary: // Allows this storage provider to initialize itself. This is // called after the page has finished loading, so you can not do // document.writes(). Storage Provider subclasses should initialize // themselves inside of here rather than in their function // constructor. console.warn("dojox.storage.initialize not implemented"); }, isAvailable: function(){ /*Boolean*/ // summary: // Returns whether this storage provider is available on this // platform. console.warn("dojox.storage.isAvailable not implemented"); }, put: function( /*string*/ key, /*object*/ value, /*function*/ resultsHandler, /*string?*/ namespace){ // summary: // Puts a key and value into this storage system. // description: // Example- // var resultsHandler = function(status, key, message){ // alert("status="+status+", key="+key+", message="+message); // }; // dojox.storage.put("test", "hello world", resultsHandler); // // Important note: if you are using Dojo Storage in conjunction with // Dojo Offline, then you don't need to provide // a resultsHandler; this is because for Dojo Offline we // use Google Gears to persist data, which has unlimited data // once the user has given permission. If you are using Dojo // Storage apart from Dojo Offline, then under the covers hidden // Flash might be used, which is both asychronous and which might // get denied; in this case you must provide a resultsHandler. // key: // A string key to use when retrieving this value in the future. // value: // A value to store; this can be any JavaScript type. // resultsHandler: // A callback function that will receive three arguments. The // first argument is one of three values: dojox.storage.SUCCESS, // dojox.storage.FAILED, or dojox.storage.PENDING; these values // determine how the put request went. In some storage systems // users can deny a storage request, resulting in a // dojox.storage.FAILED, while in other storage systems a storage // request must wait for user approval, resulting in a // dojox.storage.PENDING status until the request is either // approved or denied, resulting in another call back with // dojox.storage.SUCCESS. // The second argument in the call back is the key name that was being stored. // The third argument in the call back is an optional message that // details possible error messages that might have occurred during // the storage process. // namespace: // Optional string namespace that this value will be placed into; // if left off, the value will be placed into dojox.storage.DEFAULT_NAMESPACE console.warn("dojox.storage.put not implemented"); }, get: function(/*string*/ key, /*string?*/ namespace){ /*Object*/ // summary: // Gets the value with the given key. Returns null if this key is // not in the storage system. // key: // A string key to get the value of. // namespace: // Optional string namespace that this value will be retrieved from; // if left off, the value will be retrieved from dojox.storage.DEFAULT_NAMESPACE // return: Returns any JavaScript object type; null if the key is not present console.warn("dojox.storage.get not implemented"); }, hasKey: function(/*string*/ key, /*string?*/ namespace){ // summary: Determines whether the storage has the given key. return !!this.get(key, namespace); // Boolean }, getKeys: function(/*string?*/ namespace){ /*Array*/ // summary: Enumerates all of the available keys in this storage system. // return: Array of available keys console.warn("dojox.storage.getKeys not implemented"); }, clear: function(/*string?*/ namespace){ // summary: // Completely clears this storage system of all of it's values and // keys. If 'namespace' is provided just clears the keys in that // namespace. console.warn("dojox.storage.clear not implemented"); }, remove: function(/*string*/ key, /*string?*/ namespace){ // summary: Removes the given key from this storage system. console.warn("dojox.storage.remove not implemented"); }, getNamespaces: function(){ /*string[]*/ console.warn("dojox.storage.getNamespaces not implemented"); }, isPermanent: function(){ /*Boolean*/ // summary: // Returns whether this storage provider's values are persisted // when this platform is shutdown. console.warn("dojox.storage.isPermanent not implemented"); }, getMaximumSize: function(){ /* mixed */ // summary: The maximum storage allowed by this provider // returns: // Returns the maximum storage size // supported by this provider, in // thousands of bytes (i.e., if it // returns 60 then this means that 60K // of storage is supported). // // If this provider can not determine // it's maximum size, then // dojox.storage.SIZE_NOT_AVAILABLE is // returned; if there is no theoretical // limit on the amount of storage // this provider can return, then // dojox.storage.SIZE_NO_LIMIT is // returned console.warn("dojox.storage.getMaximumSize not implemented"); }, putMultiple: function( /*array*/ keys, /*array*/ values, /*function*/ resultsHandler, /*string?*/ namespace){ // summary: // Puts multiple keys and values into this storage system. // description: // Example- // var resultsHandler = function(status, key, message){ // alert("status="+status+", key="+key+", message="+message); // }; // dojox.storage.put(["test"], ["hello world"], resultsHandler); // // Important note: if you are using Dojo Storage in conjunction with // Dojo Offline, then you don't need to provide // a resultsHandler; this is because for Dojo Offline we // use Google Gears to persist data, which has unlimited data // once the user has given permission. If you are using Dojo // Storage apart from Dojo Offline, then under the covers hidden // Flash might be used, which is both asychronous and which might // get denied; in this case you must provide a resultsHandler. // keys: // An array of string keys to use when retrieving this value in the future, // one per value to be stored // values: // An array of values to store; this can be any JavaScript type, though the // performance of plain strings is considerably better // resultsHandler: // A callback function that will receive three arguments. The // first argument is one of three values: dojox.storage.SUCCESS, // dojox.storage.FAILED, or dojox.storage.PENDING; these values // determine how the put request went. In some storage systems // users can deny a storage request, resulting in a // dojox.storage.FAILED, while in other storage systems a storage // request must wait for user approval, resulting in a // dojox.storage.PENDING status until the request is either // approved or denied, resulting in another call back with // dojox.storage.SUCCESS. // The second argument in the call back is the key name that was being stored. // The third argument in the call back is an optional message that // details possible error messages that might have occurred during // the storage process. // namespace: // Optional string namespace that this value will be placed into; // if left off, the value will be placed into dojox.storage.DEFAULT_NAMESPACE console.warn("dojox.storage.putMultiple not implemented"); // JAC: We could implement a 'default' puMultiple here by just doing // each put individually }, getMultiple: function(/*array*/ keys, /*string?*/ namespace){ /*Object*/ // summary: // Gets the valuse corresponding to each of the given keys. // Returns a null array element for each given key that is // not in the storage system. // keys: // An array of string keys to get the value of. // namespace: // Optional string namespace that this value will be retrieved from; // if left off, the value will be retrieved from dojox.storage.DEFAULT_NAMESPACE // return: Returns any JavaScript object type; null if the key is not present console.warn("dojox.storage.getMultiple not implemented"); // JAC: We could implement a 'default' getMultiple here by just // doing each get individually }, removeMultiple: function(/*array*/ keys, /*string?*/ namespace) { // summary: Removes the given keys from this storage system. // JAC: We could implement a 'default' removeMultiple here by just // doing each remove individually console.warn("dojox.storage.remove not implemented"); }, isValidKeyArray: function( keys) { if(keys === null || keys === undefined || !dojo.isArray(keys)){ return false; } // JAC: This could be optimized by running the key validity test // directly over a joined string return !dojo.some(keys, function(key){ return !this.isValidKey(key); }); // Boolean }, hasSettingsUI: function(){ /*Boolean*/ // summary: Determines whether this provider has a settings UI. return false; }, showSettingsUI: function(){ // summary: If this provider has a settings UI, determined // by calling hasSettingsUI(), it is shown. console.warn("dojox.storage.showSettingsUI not implemented"); }, hideSettingsUI: function(){ // summary: If this provider has a settings UI, hides it. console.warn("dojox.storage.hideSettingsUI not implemented"); }, isValidKey: function(/*string*/ keyName){ /*Boolean*/ // summary: // Subclasses can call this to ensure that the key given is valid // in a consistent way across different storage providers. We use // the lowest common denominator for key values allowed: only // letters, numbers, and underscores are allowed. No spaces. if(keyName === null || keyName === undefined){ return false; } return /^[0-9A-Za-z_]*$/.test(keyName); }, getResourceList: function(){ /* Array[] */ // summary: // Returns a list of URLs that this // storage provider might depend on. // description: // This method returns a list of URLs that this // storage provider depends on to do its work. // This list is used by the Dojo Offline Toolkit // to cache these resources to ensure the machinery // used by this storage provider is available offline. // What is returned is an array of URLs. // Note that Dojo Offline uses Gears as its native // storage provider, and does not support using other // kinds of storage providers while offline anymore. return []; } }); } if(!dojo._hasResource["dojox.storage.manager"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.storage.manager"] = true; dojo.provide("dojox.storage.manager"); //dojo.require("dojo.AdapterRegistry"); // FIXME: refactor this to use an AdapterRegistry dojox.storage.manager = new function(){ // summary: A singleton class in charge of the dojox.storage system // description: // Initializes the storage systems and figures out the best available // storage options on this platform. // currentProvider: Object // The storage provider that was automagically chosen to do storage // on this platform, such as dojox.storage.FlashStorageProvider. this.currentProvider = null; // available: Boolean // Whether storage of some kind is available. this.available = false; // providers: Array // Array of all the static provider instances, useful if you want to // loop through and see what providers have been registered. this.providers = []; this._initialized = false; this._onLoadListeners = []; this.initialize = function(){ // summary: // Initializes the storage system and autodetects the best storage // provider we can provide on this platform this.autodetect(); }; this.register = function(/*string*/ name, /*Object*/ instance){ // summary: // Registers the existence of a new storage provider; used by // subclasses to inform the manager of their existence. The // storage manager will select storage providers based on // their ordering, so the order in which you call this method // matters. // name: // The full class name of this provider, such as // "dojox.storage.FlashStorageProvider". // instance: // An instance of this provider, which we will use to call // isAvailable() on. // keep list of providers as a list so that we can know what order // storage providers are preferred; also, store the providers hashed // by name in case someone wants to get a provider that uses // a particular storage backend this.providers.push(instance); this.providers[name] = instance; }; this.setProvider = function(storageClass){ // summary: // Instructs the storageManager to use the given storage class for // all storage requests. // description: // Example- // dojox.storage.setProvider( // dojox.storage.IEStorageProvider) }; this.autodetect = function(){ // summary: // Autodetects the best possible persistent storage provider // available on this platform. //console.debug("dojox.storage.manager.autodetect"); if(this._initialized){ // already finished return; } // a flag to force the storage manager to use a particular // storage provider type, such as // djConfig = {forceStorageProvider: "dojox.storage.WhatWGStorageProvider"}; var forceProvider = dojo.config["forceStorageProvider"] || false; // go through each provider, seeing if it can be used var providerToUse; //FIXME: use dojo.some for(var i = 0; i < this.providers.length; i++){ providerToUse = this.providers[i]; if(forceProvider && forceProvider == providerToUse.declaredClass){ // still call isAvailable for this provider, since this helps some // providers internally figure out if they are available // FIXME: This should be refactored since it is non-intuitive // that isAvailable() would initialize some state providerToUse.isAvailable(); break; }else if(!forceProvider && providerToUse.isAvailable()){ break; } } if(!providerToUse){ // no provider available this._initialized = true; this.available = false; this.currentProvider = null; console.warn("No storage provider found for this platform"); this.loaded(); return; } // create this provider and mix in it's properties // so that developers can do dojox.storage.put rather // than dojox.storage.currentProvider.put, for example this.currentProvider = providerToUse; dojo.mixin(dojox.storage, this.currentProvider); // have the provider initialize itself dojox.storage.initialize(); this._initialized = true; this.available = true; }; this.isAvailable = function(){ /*Boolean*/ // summary: Returns whether any storage options are available. return this.available; }; this.addOnLoad = function(func){ /* void */ // summary: // Adds an onload listener to know when Dojo Offline can be used. // description: // Adds a listener to know when Dojo Offline can be used. This // ensures that the Dojo Offline framework is loaded and that the // local dojox.storage system is ready to be used. This method is // useful if you don't want to have a dependency on Dojo Events // when using dojox.storage. // func: Function // A function to call when Dojo Offline is ready to go this._onLoadListeners.push(func); if(this.isInitialized()){ this._fireLoaded(); } }; this.removeOnLoad = function(func){ /* void */ // summary: Removes the given onLoad listener for(var i = 0; i < this._onLoadListeners.length; i++){ if(func == this._onLoadListeners[i]){ this._onLoadListeners = this._onLoadListeners.splice(i, 1); break; } } }; this.isInitialized = function(){ /*Boolean*/ // summary: // Returns whether the storage system is initialized and ready to // be used. // FIXME: This should REALLY not be in here, but it fixes a tricky // Flash timing bug. // Confirm that this is still needed with the newly refactored Dojo // Flash. Used to be for Internet Explorer. -- Brad Neuberg if(this.currentProvider != null && this.currentProvider.declaredClass == "dojox.storage.FlashStorageProvider" && dojox.flash.ready == false){ return false; }else{ return this._initialized; } }; this.supportsProvider = function(/*string*/ storageClass){ /* Boolean */ // summary: Determines if this platform supports the given storage provider. // description: // Example- // dojox.storage.manager.supportsProvider( // "dojox.storage.InternetExplorerStorageProvider"); // construct this class dynamically try{ // dynamically call the given providers class level isAvailable() // method var provider = eval("new " + storageClass + "()"); var results = provider.isAvailable(); if(!results){ return false; } return results; }catch(e){ return false; } }; this.getProvider = function(){ /* Object */ // summary: Gets the current provider return this.currentProvider; }; this.loaded = function(){ // summary: // The storage provider should call this method when it is loaded // and ready to be used. Clients who will use the provider will // connect to this method to know when they can use the storage // system. You can either use dojo.connect to connect to this // function, or can use dojox.storage.manager.addOnLoad() to add // a listener that does not depend on the dojo.event package. // description: // Example 1- // if(dojox.storage.manager.isInitialized() == false){ // dojo.connect(dojox.storage.manager, "loaded", TestStorage, "initialize"); // }else{ // dojo.connect(dojo, "loaded", TestStorage, "initialize"); // } // Example 2- // dojox.storage.manager.addOnLoad(someFunction); // FIXME: we should just provide a Deferred for this. That way you // don't care when this happens or has happened. Deferreds are in Base this._fireLoaded(); }; this._fireLoaded = function(){ //console.debug("dojox.storage.manager._fireLoaded"); dojo.forEach(this._onLoadListeners, function(i){ try{ i(); }catch(e){ console.debug(e); } }); }; this.getResourceList = function(){ // summary: // Returns a list of whatever resources are necessary for storage // providers to work. // description: // This will return all files needed by all storage providers for // this particular environment type. For example, if we are in the // browser environment, then this will return the hidden SWF files // needed by the FlashStorageProvider, even if we don't need them // for the particular browser we are working within. This is meant // to faciliate Dojo Offline, which must retrieve all resources we // need offline into the offline cache -- we retrieve everything // needed, in case another browser that requires different storage // mechanisms hits the local offline cache. For example, if we // were to sync against Dojo Offline on Firefox 2, then we would // not grab the FlashStorageProvider resources needed for Safari. var results = []; dojo.forEach(dojox.storage.manager.providers, function(currentProvider){ results = results.concat(currentProvider.getResourceList()); }); return results; } }; } if(!dojo._hasResource["dojox._sql._crypto"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox._sql._crypto"] = true; // Taken from http://www.movable-type.co.uk/scripts/aes.html by // Chris Veness (CLA signed); adapted for Dojo and Google Gears Worker Pool // by Brad Neuberg, bkn3@columbia.edu dojo.provide("dojox._sql._crypto"); dojo.mixin(dojox._sql._crypto,{ // _POOL_SIZE: // Size of worker pool to create to help with crypto _POOL_SIZE: 100, encrypt: function(plaintext, password, callback){ // summary: // Use Corrected Block TEA to encrypt plaintext using password // (note plaintext & password must be strings not string objects). // Results will be returned to the 'callback' asychronously. this._initWorkerPool(); var msg ={plaintext: plaintext, password: password}; msg = dojo.toJson(msg); msg = "encr:" + String(msg); this._assignWork(msg, callback); }, decrypt: function(ciphertext, password, callback){ // summary: // Use Corrected Block TEA to decrypt ciphertext using password // (note ciphertext & password must be strings not string objects). // Results will be returned to the 'callback' asychronously. this._initWorkerPool(); var msg ={ciphertext: ciphertext, password: password}; msg = dojo.toJson(msg); msg = "decr:" + String(msg); this._assignWork(msg, callback); }, _initWorkerPool: function(){ // bugs in Google Gears prevents us from dynamically creating // and destroying workers as we need them -- the worker // pool functionality stops working after a number of crypto // cycles (probably related to a memory leak in Google Gears). // this is too bad, since it results in much simpler code. // instead, we have to create a pool of workers and reuse them. we // keep a stack of 'unemployed' Worker IDs that are currently not working. // if a work request comes in, we pop off the 'unemployed' stack // and put them to work, storing them in an 'employed' hashtable, // keyed by their Worker ID with the value being the callback function // that wants the result. when an employed worker is done, we get // a message in our 'manager' which adds this worker back to the // unemployed stack and routes the result to the callback that // wanted it. if all the workers were employed in the past but // more work needed to be done (i.e. it's a tight labor pool ;) // then the work messages are pushed onto // a 'handleMessage' queue as an object tuple{msg: msg, callback: callback} if(!this._manager){ try{ this._manager = google.gears.factory.create("beta.workerpool", "1.0"); this._unemployed = []; this._employed ={}; this._handleMessage = []; var self = this; this._manager.onmessage = function(msg, sender){ // get the callback necessary to serve this result var callback = self._employed["_" + sender]; // make this worker unemployed self._employed["_" + sender] = undefined; self._unemployed.push("_" + sender); // see if we need to assign new work // that was queued up needing to be done if(self._handleMessage.length){ var handleMe = self._handleMessage.shift(); self._assignWork(handleMe.msg, handleMe.callback); } // return results callback(msg); } var workerInit = "function _workerInit(){" + "gearsWorkerPool.onmessage = " + String(this._workerHandler) + ";" + "}"; var code = workerInit + " _workerInit();"; // create our worker pool for(var i = 0; i < this._POOL_SIZE; i++){ this._unemployed.push("_" + this._manager.createWorker(code)); } }catch(exp){ throw exp.message||exp; } } }, _assignWork: function(msg, callback){ // can we immediately assign this work? if(!this._handleMessage.length && this._unemployed.length){ // get an unemployed worker var workerID = this._unemployed.shift().substring(1); // remove _ // list this worker as employed this._employed["_" + workerID] = callback; // do the worke this._manager.sendMessage(msg, workerID); }else{ // we have to queue it up this._handleMessage ={msg: msg, callback: callback}; } }, _workerHandler: function(msg, sender){ /* Begin AES Implementation */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [§5.1.1] var Sbox = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16]; // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [§5.2] var Rcon = [ [0x00, 0x00, 0x00, 0x00], [0x01, 0x00, 0x00, 0x00], [0x02, 0x00, 0x00, 0x00], [0x04, 0x00, 0x00, 0x00], [0x08, 0x00, 0x00, 0x00], [0x10, 0x00, 0x00, 0x00], [0x20, 0x00, 0x00, 0x00], [0x40, 0x00, 0x00, 0x00], [0x80, 0x00, 0x00, 0x00], [0x1b, 0x00, 0x00, 0x00], [0x36, 0x00, 0x00, 0x00] ]; /* * AES Cipher function: encrypt 'input' with Rijndael algorithm * * takes byte-array 'input' (16 bytes) * 2D byte-array key schedule 'w' (Nr+1 x Nb bytes) * * applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage * * returns byte-array encrypted value (16 bytes) */ function Cipher(input, w) { // main Cipher function [§5.1] var Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) var Nr = w.length/Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys var state = [[],[],[],[]]; // initialise 4xNb byte-array 'state' with input [§3.4] for (var i=0; i<4*Nb; i++) state[i%4][Math.floor(i/4)] = input[i]; state = AddRoundKey(state, w, 0, Nb); for (var round=1; round 6 && i%Nk == 4) { temp = SubWord(temp); } for (var t=0; t<4; t++) w[i][t] = w[i-Nk][t] ^ temp[t]; } return w; } function SubWord(w) { // apply SBox to 4-byte word w for (var i=0; i<4; i++) w[i] = Sbox[w[i]]; return w; } function RotWord(w) { // rotate 4-byte word w left by one byte w[4] = w[0]; for (var i=0; i<4; i++) w[i] = w[i+1]; return w; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* * Use AES to encrypt 'plaintext' with 'password' using 'nBits' key, in 'Counter' mode of operation * - see http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf * for each block * - outputblock = cipher(counter, key) * - cipherblock = plaintext xor outputblock */ function AESEncryptCtr(plaintext, password, nBits) { if (!(nBits==128 || nBits==192 || nBits==256)) return ''; // standard allows 128/192/256 bit keys // for this example script, generate the key by applying Cipher to 1st 16/24/32 chars of password; // for real-world applications, a more secure approach would be to hash the password e.g. with SHA-1 var nBytes = nBits/8; // no bytes in key var pwBytes = new Array(nBytes); for (var i=0; i>> i*8) & 0xff; for (var i=0; i<4; i++) counterBlock[i+4] = (nonce/0x100000000 >>> i*8) & 0xff; // generate key schedule - an expansion of the key into distinct Key Rounds for each round var keySchedule = KeyExpansion(key); var blockCount = Math.ceil(plaintext.length/blockSize); var ciphertext = new Array(blockCount); // ciphertext as array of strings for (var b=0; b>> c*8) & 0xff; for (var c=0; c<4; c++) counterBlock[15-c-4] = (b/0x100000000 >>> c*8) var cipherCntr = Cipher(counterBlock, keySchedule); // -- encrypt counter block -- // calculate length of final block: var blockLength = b>> c*8) & 0xff; for (var c=0; c<4; c++) counterBlock[15-c-4] = ((b/0x100000000-1) >>> c*8) & 0xff; var cipherCntr = Cipher(counterBlock, keySchedule); // encrypt counter block ciphertext[b] = unescCtrlChars(ciphertext[b]); var pt = ''; for (var i=0; i 63){ this.dbName = this.dbName.substring(0, 63); } } if(!dbName){ dbName = this.dbName; } try{ this._initDb(); this.db.open(dbName); this._dbOpen = true; }catch(exp){ throw exp.message||exp; } }, close: function(dbName){ // on Internet Explorer, Google Gears throws an exception // "Object not a collection", when we try to close the // database -- just don't close it on this platform // since we are running into a Gears bug; the Gears team // said it's ok to not close a database connection if(dojo.isIE){ return; } if(!this._dbOpen && (!dbName || dbName == this.dbName)){ return; } if(!dbName){ dbName = this.dbName; } try{ this.db.close(dbName); this._dbOpen = false; }catch(exp){ throw exp.message||exp; } }, _exec: function(params){ try{ // get the Gears Database object this._initDb(); // see if we need to open the db; if programmer // manually called dojox.sql.open() let them handle // it; otherwise we open and close automatically on // each SQL execution if(!this._dbOpen){ this.open(); this._autoClose = true; } // determine our parameters var sql = null; var callback = null; var password = null; var args = dojo._toArray(params); sql = args.splice(0, 1)[0]; // does this SQL statement use the ENCRYPT or DECRYPT // keywords? if so, extract our callback and crypto // password if(this._needsEncrypt(sql) || this._needsDecrypt(sql)){ callback = args.splice(args.length - 1, 1)[0]; password = args.splice(args.length - 1, 1)[0]; } // 'args' now just has the SQL parameters // print out debug SQL output if the developer wants that if(this.debug){ this._printDebugSQL(sql, args); } // handle SQL that needs encryption/decryption differently // do we have an ENCRYPT SQL statement? if so, handle that first if(this._needsEncrypt(sql)){ var crypto = new dojox.sql._SQLCrypto("encrypt", sql, password, args, callback); return; // encrypted results will arrive asynchronously }else if(this._needsDecrypt(sql)){ // otherwise we have a DECRYPT statement var crypto = new dojox.sql._SQLCrypto("decrypt", sql, password, args, callback); return; // decrypted results will arrive asynchronously } // execute the SQL and get the results var rs = this.db.execute(sql, args); // Gears ResultSet object's are ugly -- normalize // these into something JavaScript programmers know // how to work with, basically an array of // JavaScript objects where each property name is // simply the field name for a column of data rs = this._normalizeResults(rs); if(this._autoClose){ this.close(); } return rs; }catch(exp){ exp = exp.message||exp; console.debug("SQL Exception: " + exp); if(this._autoClose){ try{ this.close(); }catch(e){ console.debug("Error closing database: " + e.message||e); } } throw exp; } }, _initDb: function(){ if(!this.db){ try{ this.db = google.gears.factory.create('beta.database', '1.0'); }catch(exp){ dojo.setObject("google.gears.denied", true); dojox.off.onFrameworkEvent("coreOperationFailed"); throw "Google Gears must be allowed to run"; } } }, _printDebugSQL: function(sql, args){ var msg = "dojox.sql(\"" + sql + "\""; for(var i = 0; i < args.length; i++){ if(typeof args[i] == "string"){ msg += ", \"" + args[i] + "\""; }else{ msg += ", " + args[i]; } } msg += ")"; console.debug(msg); }, _normalizeResults: function(rs){ var results = []; if(!rs){ return []; } while(rs.isValidRow()){ var row = {}; for(var i = 0; i < rs.fieldCount(); i++){ var fieldName = rs.fieldName(i); var fieldValue = rs.field(i); row[fieldName] = fieldValue; } results.push(row); rs.next(); } rs.close(); return results; }, _needsEncrypt: function(sql){ return /encrypt\([^\)]*\)/i.test(sql); }, _needsDecrypt: function(sql){ return /decrypt\([^\)]*\)/i.test(sql); } }); // summary: // A private class encapsulating any cryptography that must be done // on a SQL statement. We instantiate this class and have it hold // it's state so that we can potentially have several encryption // operations happening at the same time by different SQL statements. dojo.declare("dojox.sql._SQLCrypto", null, { constructor: function(action, sql, password, args, callback){ if(action == "encrypt"){ this._execEncryptSQL(sql, password, args, callback); }else{ this._execDecryptSQL(sql, password, args, callback); } }, _execEncryptSQL: function(sql, password, args, callback){ // strip the ENCRYPT/DECRYPT keywords from the SQL var strippedSQL = this._stripCryptoSQL(sql); // determine what arguments need encryption var encryptColumns = this._flagEncryptedArgs(sql, args); // asynchronously encrypt each argument that needs it var self = this; this._encrypt(strippedSQL, password, args, encryptColumns, function(finalArgs){ // execute the SQL var error = false; var resultSet = []; var exp = null; try{ resultSet = dojox.sql.db.execute(strippedSQL, finalArgs); }catch(execError){ error = true; exp = execError.message||execError; } // was there an error during SQL execution? if(exp != null){ if(dojox.sql._autoClose){ try{ dojox.sql.close(); }catch(e){} } callback(null, true, exp.toString()); return; } // normalize SQL results into a JavaScript object // we can work with resultSet = dojox.sql._normalizeResults(resultSet); if(dojox.sql._autoClose){ dojox.sql.close(); } // are any decryptions necessary on the result set? if(dojox.sql._needsDecrypt(sql)){ // determine which of the result set columns needs decryption var needsDecrypt = self._determineDecryptedColumns(sql); // now decrypt columns asynchronously // decrypt columns that need it self._decrypt(resultSet, needsDecrypt, password, function(finalResultSet){ callback(finalResultSet, false, null); }); }else{ callback(resultSet, false, null); } }); }, _execDecryptSQL: function(sql, password, args, callback){ // strip the ENCRYPT/DECRYPT keywords from the SQL var strippedSQL = this._stripCryptoSQL(sql); // determine which columns needs decryption; this either // returns the value *, which means all result set columns will // be decrypted, or it will return the column names that need // decryption set on a hashtable so we can quickly test a given // column name; the key is the column name that needs // decryption and the value is 'true' (i.e. needsDecrypt["someColumn"] // would return 'true' if it needs decryption, and would be 'undefined' // or false otherwise) var needsDecrypt = this._determineDecryptedColumns(sql); // execute the SQL var error = false; var resultSet = []; var exp = null; try{ resultSet = dojox.sql.db.execute(strippedSQL, args); }catch(execError){ error = true; exp = execError.message||execError; } // was there an error during SQL execution? if(exp != null){ if(dojox.sql._autoClose){ try{ dojox.sql.close(); }catch(e){} } callback(resultSet, true, exp.toString()); return; } // normalize SQL results into a JavaScript object // we can work with resultSet = dojox.sql._normalizeResults(resultSet); if(dojox.sql._autoClose){ dojox.sql.close(); } // decrypt columns that need it this._decrypt(resultSet, needsDecrypt, password, function(finalResultSet){ callback(finalResultSet, false, null); }); }, _encrypt: function(sql, password, args, encryptColumns, callback){ //console.debug("_encrypt, sql="+sql+", password="+password+", encryptColumns="+encryptColumns+", args="+args); this._totalCrypto = 0; this._finishedCrypto = 0; this._finishedSpawningCrypto = false; this._finalArgs = args; for(var i = 0; i < args.length; i++){ if(encryptColumns[i]){ // we have an encrypt() keyword -- get just the value inside // the encrypt() parantheses -- for now this must be a ? var sqlParam = args[i]; var paramIndex = i; // update the total number of encryptions we know must be done asynchronously this._totalCrypto++; // FIXME: This currently uses DES as a proof-of-concept since the // DES code used is quite fast and was easy to work with. Modify dojox.sql // to be able to specify a different encryption provider through a // a SQL-like syntax, such as dojox.sql("SET ENCRYPTION BLOWFISH"), // and modify the dojox.crypto.Blowfish code to be able to work using // a Google Gears Worker Pool // do the actual encryption now, asychronously on a Gears worker thread dojox._sql._crypto.encrypt(sqlParam, password, dojo.hitch(this, function(results){ // set the new encrypted value this._finalArgs[paramIndex] = results; this._finishedCrypto++; // are we done with all encryption? if(this._finishedCrypto >= this._totalCrypto && this._finishedSpawningCrypto){ callback(this._finalArgs); } })); } } this._finishedSpawningCrypto = true; }, _decrypt: function(resultSet, needsDecrypt, password, callback){ //console.debug("decrypt, resultSet="+resultSet+", needsDecrypt="+needsDecrypt+", password="+password); this._totalCrypto = 0; this._finishedCrypto = 0; this._finishedSpawningCrypto = false; this._finalResultSet = resultSet; for(var i = 0; i < resultSet.length; i++){ var row = resultSet[i]; // go through each of the column names in row, // seeing if they need decryption for(var columnName in row){ if(needsDecrypt == "*" || needsDecrypt[columnName]){ this._totalCrypto++; var columnValue = row[columnName]; // forming a closure here can cause issues, with values not cleanly // saved on Firefox/Mac OS X for some of the values above that // are needed in the callback below; call a subroutine that will form // a closure inside of itself instead this._decryptSingleColumn(columnName, columnValue, password, i, function(finalResultSet){ callback(finalResultSet); }); } } } this._finishedSpawningCrypto = true; }, _stripCryptoSQL: function(sql){ // replace all DECRYPT(*) occurrences with a * sql = sql.replace(/DECRYPT\(\*\)/ig, "*"); // match any ENCRYPT(?, ?, ?, etc) occurrences, // then replace with just the question marks in the // middle var matches = sql.match(/ENCRYPT\([^\)]*\)/ig); if(matches != null){ for(var i = 0; i < matches.length; i++){ var encryptStatement = matches[i]; var encryptValue = encryptStatement.match(/ENCRYPT\(([^\)]*)\)/i)[1]; sql = sql.replace(encryptStatement, encryptValue); } } // match any DECRYPT(COL1, COL2, etc) occurrences, // then replace with just the column names // in the middle matches = sql.match(/DECRYPT\([^\)]*\)/ig); if(matches != null){ for(var i = 0; i < matches.length; i++){ var decryptStatement = matches[i]; var decryptValue = decryptStatement.match(/DECRYPT\(([^\)]*)\)/i)[1]; sql = sql.replace(decryptStatement, decryptValue); } } return sql; }, _flagEncryptedArgs: function(sql, args){ // capture literal strings that have question marks in them, // and also capture question marks that stand alone var tester = new RegExp(/([\"][^\"]*\?[^\"]*[\"])|([\'][^\']*\?[^\']*[\'])|(\?)/ig); var matches; var currentParam = 0; var results = []; while((matches = tester.exec(sql)) != null){ var currentMatch = RegExp.lastMatch+""; // are we a literal string? then ignore it if(/^[\"\']/.test(currentMatch)){ continue; } // do we have an encrypt keyword to our left? var needsEncrypt = false; if(/ENCRYPT\([^\)]*$/i.test(RegExp.leftContext)){ needsEncrypt = true; } // set the encrypted flag results[currentParam] = needsEncrypt; currentParam++; } return results; }, _determineDecryptedColumns: function(sql){ var results = {}; if(/DECRYPT\(\*\)/i.test(sql)){ results = "*"; }else{ var tester = /DECRYPT\((?:\s*\w*\s*\,?)*\)/ig; var matches; while(matches = tester.exec(sql)){ var lastMatch = new String(RegExp.lastMatch); var columnNames = lastMatch.replace(/DECRYPT\(/i, ""); columnNames = columnNames.replace(/\)/, ""); columnNames = columnNames.split(/\s*,\s*/); dojo.forEach(columnNames, function(column){ if(/\s*\w* AS (\w*)/i.test(column)){ column = column.match(/\s*\w* AS (\w*)/i)[1]; } results[column] = true; }); } } return results; }, _decryptSingleColumn: function(columnName, columnValue, password, currentRowIndex, callback){ //console.debug("decryptSingleColumn, columnName="+columnName+", columnValue="+columnValue+", currentRowIndex="+currentRowIndex) dojox._sql._crypto.decrypt(columnValue, password, dojo.hitch(this, function(results){ // set the new decrypted value this._finalResultSet[currentRowIndex][columnName] = results; this._finishedCrypto++; // are we done with all encryption? if(this._finishedCrypto >= this._totalCrypto && this._finishedSpawningCrypto){ //console.debug("done with all decrypts"); callback(this._finalResultSet); } })); } }); } if(!dojo._hasResource["dojox.sql"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.sql"] = true; dojo.provide("dojox.sql"); } if(!dojo._hasResource["dojox.storage.GearsStorageProvider"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.storage.GearsStorageProvider"] = true; dojo.provide("dojox.storage.GearsStorageProvider"); if(dojo.isGears){ (function(){ // make sure we don't define the gears provider if we're not gears // enabled dojo.declare("dojox.storage.GearsStorageProvider", dojox.storage.Provider, { // summary: // Storage provider that uses the features of Google Gears // to store data (it is saved into the local SQL database // provided by Gears, using dojox.sql) // description: // You can disable this storage provider with the following djConfig // variable: // var djConfig = { disableGearsStorage: true }; // // Authors of this storage provider- // Brad Neuberg, bkn3@columbia.edu constructor: function(){ }, // instance methods and properties TABLE_NAME: "__DOJO_STORAGE", initialized: false, _available: null, initialize: function(){ //console.debug("dojox.storage.GearsStorageProvider.initialize"); if(dojo.config["disableGearsStorage"] == true){ return; } // partition our storage data so that multiple apps // on the same host won't collide this.TABLE_NAME = "__DOJO_STORAGE"; // create the table that holds our data try{ dojox.sql("CREATE TABLE IF NOT EXISTS " + this.TABLE_NAME + "( " + " namespace TEXT, " + " key TEXT, " + " value TEXT " + ")" ); dojox.sql("CREATE UNIQUE INDEX IF NOT EXISTS namespace_key_index" + " ON " + this.TABLE_NAME + " (namespace, key)"); }catch(e){ console.debug("dojox.storage.GearsStorageProvider.initialize:", e); this.initialized = false; // we were unable to initialize dojox.storage.manager.loaded(); return; } // indicate that this storage provider is now loaded this.initialized = true; dojox.storage.manager.loaded(); }, isAvailable: function(){ // is Google Gears available and defined? return this._available = dojo.isGears; }, put: function(key, value, resultsHandler, namespace){ if(this.isValidKey(key) == false){ throw new Error("Invalid key given: " + key); } namespace = namespace||this.DEFAULT_NAMESPACE; // serialize the value; // handle strings differently so they have better performance if(dojo.isString(value)){ value = "string:" + value; }else{ value = dojo.toJson(value); } // try to store the value try{ dojox.sql("DELETE FROM " + this.TABLE_NAME + " WHERE namespace = ? AND key = ?", namespace, key); dojox.sql("INSERT INTO " + this.TABLE_NAME + " VALUES (?, ?, ?)", namespace, key, value); }catch(e){ // indicate we failed console.debug("dojox.storage.GearsStorageProvider.put:", e); resultsHandler(this.FAILED, key, e.toString()); return; } if(resultsHandler){ resultsHandler(dojox.storage.SUCCESS, key, null); } }, get: function(key, namespace){ if(this.isValidKey(key) == false){ throw new Error("Invalid key given: " + key); } namespace = namespace||this.DEFAULT_NAMESPACE; // try to find this key in the database var results = dojox.sql("SELECT * FROM " + this.TABLE_NAME + " WHERE namespace = ? AND " + " key = ?", namespace, key); if(!results.length){ return null; }else{ results = results[0].value; } // destringify the content back into a // real JavaScript object; // handle strings differently so they have better performance if(dojo.isString(results) && (/^string:/.test(results))){ results = results.substring("string:".length); }else{ results = dojo.fromJson(results); } return results; }, getNamespaces: function(){ var results = [ dojox.storage.DEFAULT_NAMESPACE ]; var rs = dojox.sql("SELECT namespace FROM " + this.TABLE_NAME + " DESC GROUP BY namespace"); for(var i = 0; i < rs.length; i++){ if(rs[i].namespace != dojox.storage.DEFAULT_NAMESPACE){ results.push(rs[i].namespace); } } return results; }, getKeys: function(namespace){ namespace = namespace||this.DEFAULT_NAMESPACE; if(this.isValidKey(namespace) == false){ throw new Error("Invalid namespace given: " + namespace); } var rs = dojox.sql("SELECT key FROM " + this.TABLE_NAME + " WHERE namespace = ?", namespace); var results = []; for(var i = 0; i < rs.length; i++){ results.push(rs[i].key); } return results; }, clear: function(namespace){ if(this.isValidKey(namespace) == false){ throw new Error("Invalid namespace given: " + namespace); } namespace = namespace||this.DEFAULT_NAMESPACE; dojox.sql("DELETE FROM " + this.TABLE_NAME + " WHERE namespace = ?", namespace); }, remove: function(key, namespace){ namespace = namespace||this.DEFAULT_NAMESPACE; dojox.sql("DELETE FROM " + this.TABLE_NAME + " WHERE namespace = ? AND" + " key = ?", namespace, key); }, putMultiple: function(keys, values, resultsHandler, namespace) { if(this.isValidKeyArray(keys) === false || ! values instanceof Array || keys.length != values.length){ throw new Error("Invalid arguments: keys = [" + keys + "], values = [" + values + "]"); } if(namespace == null || typeof namespace == "undefined"){ namespace = dojox.storage.DEFAULT_NAMESPACE; } if(this.isValidKey(namespace) == false){ throw new Error("Invalid namespace given: " + namespace); } this._statusHandler = resultsHandler; // try to store the value try{ dojox.sql.open(); dojox.sql.db.execute("BEGIN TRANSACTION"); var _stmt = "REPLACE INTO " + this.TABLE_NAME + " VALUES (?, ?, ?)"; for(var i=0;i dew){ minw = dew; maxw = dbw; }else{ maxw = dew; minw = dbw; } var dbh = _document.body.clientHeight; if(dbh > deh){ minh = deh; maxh = dbh; }else{ maxh = deh; minh = dbh; } w = (maxw > _window.innerWidth) ? minw : maxw; h = (maxh > _window.innerHeight) ? minh : maxh; }else if(!dojo.isOpera && _window.innerWidth){ //in opera9, dojo.body().clientWidth should be used, instead //of window.innerWidth/document.documentElement.clientWidth //so we have to check whether it is opera w = _window.innerWidth; h = _window.innerHeight; }else if(dojo.isIE && de && deh){ w = dew; h = deh; }else if(dojo.body().clientWidth){ // IE5, Opera w = dojo.body().clientWidth; h = dojo.body().clientHeight; } // get scroll position var scroll = dojo._docScroll(); return { w: w, h: h, l: scroll.x, t: scroll.y }; // object }; dijit.placeOnScreen = function( /* DomNode */ node, /* Object */ pos, /* Object */ corners, /* boolean? */ tryOnly){ // summary: // Keeps 'node' in the visible area of the screen while trying to // place closest to pos.x, pos.y. The input coordinates are // expected to be the desired document position. // // Set which corner(s) you want to bind to, such as // // placeOnScreen(node, {x: 10, y: 20}, ["TR", "BL"]) // // The desired x/y will be treated as the topleft(TL)/topright(TR) or // BottomLeft(BL)/BottomRight(BR) corner of the node. Each corner is tested // and if a perfect match is found, it will be used. Otherwise, it goes through // all of the specified corners, and choose the most appropriate one. // // NOTE: node is assumed to be absolutely or relatively positioned. var choices = dojo.map(corners, function(corner){ return { corner: corner, pos: pos }; }); return dijit._place(node, choices); } dijit._place = function(/*DomNode*/ node, /* Array */ choices, /* Function */ layoutNode){ // summary: // Given a list of spots to put node, put it at the first spot where it fits, // of if it doesn't fit anywhere then the place with the least overflow // choices: Array // Array of elements like: {corner: 'TL', pos: {x: 10, y: 20} } // Above example says to put the top-left corner of the node at (10,20) // layoutNode: Function(node, aroundNodeCorner, nodeCorner) // for things like tooltip, they are displayed differently (and have different dimensions) // based on their orientation relative to the parent. This adjusts the popup based on orientation. // get {x: 10, y: 10, w: 100, h:100} type obj representing position of // viewport over document var view = dijit.getViewport(); // This won't work if the node is inside a
, // so reattach it to dojo.doc.body. (Otherwise, the positioning will be wrong // and also it might get cutoff) if(!node.parentNode || String(node.parentNode.tagName).toLowerCase() != "body"){ dojo.body().appendChild(node); } var best = null; dojo.some(choices, function(choice){ var corner = choice.corner; var pos = choice.pos; // configure node to be displayed in given position relative to button // (need to do this in order to get an accurate size for the node, because // a tooltips size changes based on position, due to triangle) if(layoutNode){ layoutNode(node, choice.aroundCorner, corner); } // get node's size var style = node.style; var oldDisplay = style.display; var oldVis = style.visibility; style.visibility = "hidden"; style.display = ""; var mb = dojo.marginBox(node); style.display = oldDisplay; style.visibility = oldVis; // coordinates and size of node with specified corner placed at pos, // and clipped by viewport var startX = (corner.charAt(1) == 'L' ? pos.x : Math.max(view.l, pos.x - mb.w)), startY = (corner.charAt(0) == 'T' ? pos.y : Math.max(view.t, pos.y - mb.h)), endX = (corner.charAt(1) == 'L' ? Math.min(view.l + view.w, startX + mb.w) : pos.x), endY = (corner.charAt(0) == 'T' ? Math.min(view.t + view.h, startY + mb.h) : pos.y), width = endX - startX, height = endY - startY, overflow = (mb.w - width) + (mb.h - height); if(best == null || overflow < best.overflow){ best = { corner: corner, aroundCorner: choice.aroundCorner, x: startX, y: startY, w: width, h: height, overflow: overflow }; } return !overflow; }); node.style.left = best.x + "px"; node.style.top = best.y + "px"; if(best.overflow && layoutNode){ layoutNode(node, best.aroundCorner, best.corner); } return best; } dijit.placeOnScreenAroundElement = function( /* DomNode */ node, /* DomNode */ aroundNode, /* Object */ aroundCorners, /* Function */ layoutNode){ // summary // Like placeOnScreen, except it accepts aroundNode instead of x,y // and attempts to place node around it. Uses margin box dimensions. // // aroundCorners // specify Which corner of aroundNode should be // used to place the node => which corner(s) of node to use (see the // corners parameter in dijit.placeOnScreen) // e.g. {'TL': 'BL', 'BL': 'TL'} // // layoutNode: Function(node, aroundNodeCorner, nodeCorner) // for things like tooltip, they are displayed differently (and have different dimensions) // based on their orientation relative to the parent. This adjusts the popup based on orientation. // get coordinates of aroundNode aroundNode = dojo.byId(aroundNode); var oldDisplay = aroundNode.style.display; aroundNode.style.display=""; // #3172: use the slightly tighter border box instead of marginBox var aroundNodeW = aroundNode.offsetWidth; //mb.w; var aroundNodeH = aroundNode.offsetHeight; //mb.h; var aroundNodePos = dojo.coords(aroundNode, true); aroundNode.style.display=oldDisplay; // Generate list of possible positions for node var choices = []; for(var nodeCorner in aroundCorners){ choices.push( { aroundCorner: nodeCorner, corner: aroundCorners[nodeCorner], pos: { x: aroundNodePos.x + (nodeCorner.charAt(1) == 'L' ? 0 : aroundNodeW), y: aroundNodePos.y + (nodeCorner.charAt(0) == 'T' ? 0 : aroundNodeH) } }); } return dijit._place(node, choices, layoutNode); } } if(!dojo._hasResource["dojox.flash._base"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.flash._base"] = true; dojo.provide("dojox.flash._base"); // for dijit.getViewport(), needed by dojox.flash.Embed.center() dojox.flash = function(){ // summary: // The goal of dojox.flash is to make it easy to extend Flash's capabilities // into an Ajax/DHTML environment. // // dojox.flash provides an easy object for interacting with the Flash plugin. // This object provides methods to determine the current version of the Flash // plugin (dojox.flash.info); write out the necessary markup to // dynamically insert a Flash object into the page (dojox.flash.Embed; and // do dynamic installation and upgrading of the current Flash plugin in // use (dojox.flash.Install). If you want to call methods on the Flash object // embedded into the page it is your responsibility to use Flash's ExternalInterface // API and get a reference to the Flash object yourself. // // To use dojox.flash, you must first wait until Flash is finished loading // and initializing before you attempt communication or interaction. // To know when Flash is finished use dojo.connect: // // dojo.connect(dojox.flash, "loaded", myInstance, "myCallback"); // // Then, while the page is still loading provide the file name: // // dojox.flash.setSwf(dojo.moduleUrl("dojox", "_storage/storage.swf")); // // If no SWF files are specified, then Flash is not initialized. // // Your Flash must use Flash's ExternalInterface to expose Flash methods and // to call JavaScript. // // setSwf can take an optional 'visible' attribute to control whether // the Flash object is visible or not on the page; the default is visible: // // dojox.flash.setSwf(dojo.moduleUrl("dojox", "_storage/storage.swf"), // false); // // Once finished, you can query Flash version information: // // dojox.flash.info.version // // Or can communicate with Flash methods that were exposed: // // var f = dojox.flash.get(); // var results = f.sayHello("Some Message"); // // Your Flash files should use DojoExternalInterface.as to register methods; // this file wraps Flash's normal ExternalInterface but correct various // serialization bugs that ExternalInterface has. // // Note that dojox.flash is not meant to be a generic Flash embedding // mechanism; it is as generic as necessary to make Dojo Storage's // Flash Storage Provider as clean and modular as possible. If you want // a generic Flash embed mechanism see SWFObject // (http://blog.deconcept.com/swfobject/). // // Notes: // Note that dojox.flash can currently only work with one Flash object // on the page; it does not yet support multiple Flash objects on // the same page. // // Your code can detect whether the Flash player is installing or having // its version revved in two ways. First, if dojox.flash detects that // Flash installation needs to occur, it sets dojox.flash.info.installing // to true. Second, you can detect if installation is necessary with the // following callback: // // dojo.connect(dojox.flash, "installing", myInstance, "myCallback"); // // You can use this callback to delay further actions that might need Flash; // when installation is finished the full page will be refreshed and the // user will be placed back on your page with Flash installed. // // ------------------- // Todo/Known Issues // ------------------- // * On Internet Explorer, after doing a basic install, the page is // not refreshed or does not detect that Flash is now available. The way // to fix this is to create a custom small Flash file that is pointed to // during installation; when it is finished loading, it does a callback // that says that Flash installation is complete on IE, and we can proceed // to initialize the dojox.flash subsystem. // * Things aren't super tested for sending complex objects to Flash // methods, since Dojo Storage only needs strings // // Author- Brad Neuberg, http://codinginparadise.org } dojox.flash = { ready: false, url: null, _visible: true, _loadedListeners: new Array(), _installingListeners: new Array(), setSwf: function(/* String */ url, /* boolean? */ visible){ // summary: Sets the SWF files and versions we are using. // url: String // The URL to this Flash file. // visible: boolean? // Whether the Flash file is visible or not. If it is not visible we hide it off the // screen. This defaults to true (i.e. the Flash file is visible). this.url = url; if(typeof visible != "undefined"){ this._visible = visible; } // initialize ourselves this._initialize(); }, addLoadedListener: function(/* Function */ listener){ // summary: // Adds a listener to know when Flash is finished loading. // Useful if you don't want a dependency on dojo.event. // listener: Function // A function that will be called when Flash is done loading. this._loadedListeners.push(listener); }, addInstallingListener: function(/* Function */ listener){ // summary: // Adds a listener to know if Flash is being installed. // Useful if you don't want a dependency on dojo.event. // listener: Function // A function that will be called if Flash is being // installed this._installingListeners.push(listener); }, loaded: function(){ // summary: Called back when the Flash subsystem is finished loading. // description: // A callback when the Flash subsystem is finished loading and can be // worked with. To be notified when Flash is finished loading, add a // loaded listener: // // dojox.flash.addLoadedListener(loadedListener); dojox.flash.ready = true; if(dojox.flash._loadedListeners.length > 0){ for(var i = 0;i < dojox.flash._loadedListeners.length; i++){ dojox.flash._loadedListeners[i].call(null); } } }, installing: function(){ // summary: Called if Flash is being installed. // description: // A callback to know if Flash is currently being installed or // having its version revved. To be notified if Flash is installing, connect // your callback to this method using the following: // // dojo.event.connect(dojox.flash, "installing", myInstance, "myCallback"); if(dojox.flash._installingListeners.length > 0){ for(var i = 0; i < dojox.flash._installingListeners.length; i++){ dojox.flash._installingListeners[i].call(null); } } }, // Initializes dojox.flash. _initialize: function(){ //console.debug("dojox.flash._initialize"); // see if we need to rev or install Flash on this platform var installer = new dojox.flash.Install(); dojox.flash.installer = installer; if(installer.needed() == true){ installer.install(); }else{ // write the flash object into the page dojox.flash.obj = new dojox.flash.Embed(this._visible); dojox.flash.obj.write(); // setup the communicator dojox.flash.comm = new dojox.flash.Communicator(); } } }; dojox.flash.Info = function(){ // summary: A class that helps us determine whether Flash is available. // description: // A class that helps us determine whether Flash is available, // it's major and minor versions, and what Flash version features should // be used for Flash/JavaScript communication. Parts of this code // are adapted from the automatic Flash plugin detection code autogenerated // by the Macromedia Flash 8 authoring environment. // // An instance of this class can be accessed on dojox.flash.info after // the page is finished loading. // // This constructor must be called before the page is finished loading. // Visual basic helper required to detect Flash Player ActiveX control // version information on Internet Explorer if(dojo.isIE){ document.write([ ' // // Note that inline styles on elements are not handled (i.e. // if you somehow have an inline style that uses a URL); // object and embed tags are not scanned since their format // differs based on type; and elements created by JavaScript // after page load are not found. For these you must manually // add them with a dojox.off.files.cache() method call. // just schedule the slurp once the page is loaded and // Dojo Offline is ready to slurp; dojox.off will call // our _slurp() method before indicating it is finished // loading this._doSlurp = true; }, cache: function(urlOrList){ /* void */ // summary: // Caches a file or list of files to be available offline. This // can either be a full URL, such as http://foobar.com/index.html, // or a relative URL, such as ../index.html. This URL is not // actually cached until dojox.off.sync.synchronize() is called. // urlOrList: String or Array[] // A URL of a file to cache or an Array of Strings of files to // cache //console.debug("dojox.off.files.cache, urlOrList="+urlOrList); if(dojo.isString(urlOrList)){ var url = this._trimAnchor(urlOrList+""); if(!this.isAvailable(url)){ this.listOfURLs.push(url); } }else if(urlOrList instanceof dojo._Url){ var url = this._trimAnchor(urlOrList.uri); if(!this.isAvailable(url)){ this.listOfURLs.push(url); } }else{ dojo.forEach(urlOrList, function(url){ url = this._trimAnchor(url); if(!this.isAvailable(url)){ this.listOfURLs.push(url); } }, this); } }, printURLs: function(){ // summary: // A helper function that will dump and print out // all of the URLs that are cached for offline // availability. This can help with debugging if you // are trying to make sure that all of your URLs are // available offline console.debug("The following URLs are cached for offline use:"); dojo.forEach(this.listOfURLs, function(i){ console.debug(i); }); }, remove: function(url){ /* void */ // summary: // Removes a URL from the list of files to cache. // description: // Removes a URL from the list of URLs to cache. Note that this // does not actually remove the file from the offline cache; // instead, it just prevents us from refreshing this file at a // later time, so that it will naturally time out and be removed // from the offline cache // url: String // The URL to remove for(var i = 0; i < this.listOfURLs.length; i++){ if(this.listOfURLs[i] == url){ this.listOfURLs = this.listOfURLs.splice(i, 1); break; } } }, isAvailable: function(url){ /* boolean */ // summary: // Determines whether the given resource is available offline. // url: String // The URL to check for(var i = 0; i < this.listOfURLs.length; i++){ if(this.listOfURLs[i] == url){ return true; } } return false; }, refresh: function(callback){ /* void */ //console.debug("dojox.off.files.refresh"); // summary: // For advanced usage; most developers can ignore this. // Refreshes our list of offline resources, // making them available offline. // callback: Function // A callback that receives two arguments: whether an error // occurred, which is a boolean; and an array of error message strings // with details on errors encountered. If no error occured then message is // empty array with length 0. try{ if(dojo.config.isDebug){ this.printURLs(); } this.refreshing = true; if(this.versionURL){ this._getVersionInfo(function(oldVersion, newVersion, justDebugged){ //console.warn("getVersionInfo, oldVersion="+oldVersion+", newVersion="+newVersion // + ", justDebugged="+justDebugged+", isDebug="+dojo.config.isDebug); if(dojo.config.isDebug || !newVersion || justDebugged || !oldVersion || oldVersion != newVersion){ console.warn("Refreshing offline file list"); this._doRefresh(callback, newVersion); }else{ console.warn("No need to refresh offline file list"); callback(false, []); } }); }else{ console.warn("Refreshing offline file list"); this._doRefresh(callback); } }catch(e){ this.refreshing = false; // can't refresh files -- core operation -- // fail fast dojox.off.coreOpFailed = true; dojox.off.enabled = false; dojox.off.onFrameworkEvent("coreOperationFailed"); } }, abortRefresh: function(){ // summary: // For advanced usage; most developers can ignore this. // Aborts and cancels a refresh. if(!this.refreshing){ return; } this._store.abortCapture(this._cancelID); this.refreshing = false; }, _slurp: function(){ if(!this._doSlurp){ return; } var handleUrl = dojo.hitch(this, function(url){ if(this._sameLocation(url)){ this.cache(url); } }); handleUrl(window.location.href); dojo.query("script").forEach(function(i){ try{ handleUrl(i.getAttribute("src")); }catch(exp){ //console.debug("dojox.off.files.slurp 'script' error: " // + exp.message||exp); } }); dojo.query("link").forEach(function(i){ try{ if(!i.getAttribute("rel") || i.getAttribute("rel").toLowerCase() != "stylesheet"){ return; } handleUrl(i.getAttribute("href")); }catch(exp){ //console.debug("dojox.off.files.slurp 'link' error: " // + exp.message||exp); } }); dojo.query("img").forEach(function(i){ try{ handleUrl(i.getAttribute("src")); }catch(exp){ //console.debug("dojox.off.files.slurp 'img' error: " // + exp.message||exp); } }); dojo.query("a").forEach(function(i){ try{ handleUrl(i.getAttribute("href")); }catch(exp){ //console.debug("dojox.off.files.slurp 'a' error: " // + exp.message||exp); } }); // FIXME: handle 'object' and 'embed' tag // parse our style sheets for inline URLs and imports dojo.forEach(document.styleSheets, function(sheet){ try{ if(sheet.cssRules){ // Firefox dojo.forEach(sheet.cssRules, function(rule){ var text = rule.cssText; if(text){ var matches = text.match(/url\(\s*([^\) ]*)\s*\)/i); if(!matches){ return; } for(var i = 1; i < matches.length; i++){ handleUrl(matches[i]) } } }); }else if(sheet.cssText){ // IE var matches; var text = sheet.cssText.toString(); // unfortunately, using RegExp.exec seems to be flakey // for looping across multiple lines on IE using the // global flag, so we have to simulate it var lines = text.split(/\f|\r|\n/); for(var i = 0; i < lines.length; i++){ matches = lines[i].match(/url\(\s*([^\) ]*)\s*\)/i); if(matches && matches.length){ handleUrl(matches[1]); } } } }catch(exp){ //console.debug("dojox.off.files.slurp stylesheet parse error: " // + exp.message||exp); } }); //this.printURLs(); }, _sameLocation: function(url){ if(!url){ return false; } // filter out anchors if(url.length && url.charAt(0) == "#"){ return false; } // FIXME: dojo._Url should be made public; // it's functionality is very useful for // parsing URLs correctly, which is hard to // do right url = new dojo._Url(url); // totally relative -- ../../someFile.html if(!url.scheme && !url.port && !url.host){ return true; } // scheme relative with port specified -- brad.com:8080 if(!url.scheme && url.host && url.port && window.location.hostname == url.host && window.location.port == url.port){ return true; } // scheme relative with no-port specified -- brad.com if(!url.scheme && url.host && !url.port && window.location.hostname == url.host && window.location.port == 80){ return true; } // else we have everything return window.location.protocol == (url.scheme + ":") && window.location.hostname == url.host && (window.location.port == url.port || !window.location.port && !url.port); }, _trimAnchor: function(url){ return url.replace(/\#.*$/, ""); }, _doRefresh: function(callback, newVersion){ // get our local server var localServer; try{ localServer = google.gears.factory.create("beta.localserver", "1.0"); }catch(exp){ dojo.setObject("google.gears.denied", true); dojox.off.onFrameworkEvent("coreOperationFailed"); throw "Google Gears must be allowed to run"; } var storeName = "dot_store_" + window.location.href.replace(/[^0-9A-Za-z_]/g, "_"); // clip at 64 characters, the max length of a resource store name if(storeName.length >= 64){ storeName = storeName.substring(0, 63); } // refresh everything by simply removing // any older stores localServer.removeStore(storeName); // open/create the resource store localServer.openStore(storeName); var store = localServer.createStore(storeName); this._store = store; // add our list of files to capture var self = this; this._currentFileIndex = 0; this._cancelID = store.capture(this.listOfURLs, function(url, success, captureId){ //console.debug("store.capture, url="+url+", success="+success); if(!success && self.refreshing){ self._cancelID = null; self.refreshing = false; var errorMsgs = []; errorMsgs.push("Unable to capture: " + url); callback(true, errorMsgs); return; }else if(success){ self._currentFileIndex++; } if(success && self._currentFileIndex >= self.listOfURLs.length){ self._cancelID = null; self.refreshing = false; if(newVersion){ dojox.storage.put("oldVersion", newVersion, null, dojox.off.STORAGE_NAMESPACE); } dojox.storage.put("justDebugged", dojo.config.isDebug, null, dojox.off.STORAGE_NAMESPACE); callback(false, []); } }); }, _getVersionInfo: function(callback){ var justDebugged = dojox.storage.get("justDebugged", dojox.off.STORAGE_NAMESPACE); var oldVersion = dojox.storage.get("oldVersion", dojox.off.STORAGE_NAMESPACE); var newVersion = null; callback = dojo.hitch(this, callback); dojo.xhrGet({ url: this.versionURL + "?browserbust=" + new Date().getTime(), timeout: 5 * 1000, handleAs: "javascript", error: function(err){ //console.warn("dojox.off.files._getVersionInfo, err=",err); dojox.storage.remove("oldVersion", dojox.off.STORAGE_NAMESPACE); dojox.storage.remove("justDebugged", dojox.off.STORAGE_NAMESPACE); callback(oldVersion, newVersion, justDebugged); }, load: function(data){ //console.warn("dojox.off.files._getVersionInfo, load=",data); // some servers incorrectly return 404's // as a real page if(data){ newVersion = data; } callback(oldVersion, newVersion, justDebugged); } }); } } } if(!dojo._hasResource["dojox.off.sync"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.off.sync"] = true; dojo.provide("dojox.off.sync"); // Author: Brad Neuberg, bkn3@columbia.edu, http://codinginparadise.org // summary: // Exposes syncing functionality to offline applications dojo.mixin(dojox.off.sync, { // isSyncing: boolean // Whether we are in the middle of a syncing session. isSyncing: false, // cancelled: boolean // Whether we were cancelled during our last sync request or not. If // we are cancelled, then successful will be false. cancelled: false, // successful: boolean // Whether the last sync was successful or not. If false, an error // occurred. successful: true, // details: String[] // Details on the sync. If the sync was successful, this will carry // any conflict or merging messages that might be available; if the // sync was unsuccessful, this will have an error message. For both // of these, this should be an array of Strings, where each string // carries details on the sync. // Example: // dojox.off.sync.details = ["The document 'foobar' had conflicts - yours one", // "The document 'hello world' was automatically merged"]; details: [], // error: boolean // Whether an error occurred during the syncing process. error: false, // actions: dojox.off.sync.ActionLog // Our ActionLog that we store offline actions into for later // replaying when we go online actions: null, // autoSync: boolean // For advanced usage; most developers can ignore this. // Whether we do automatically sync on page load or when we go online. // If true we do, if false syncing must be manually initiated. // Defaults to true. autoSync: true, // summary: // An event handler that is called during the syncing process with // the state of syncing. It is important that you connect to this // method and respond to certain sync events, especially the // "download" event. // description: // This event handler is called during the syncing process. You can // do a dojo.connect to receive sync feedback: // // dojo.connect(dojox.off.sync, "onSync", someFunc); // // You will receive one argument, which is the type of the event // and which can have the following values. // // The most common two types that you need to care about are "download" // and "finished", especially if you are using the default // Dojo Offline UI widget that does the hard work of informing // the user through the UI about what is occuring during syncing. // // If you receive the "download" event, you should make a network call // to retrieve and store your data somehow for offline access. The // "finished" event indicates that syncing is done. An example: // // dojo.connect(dojox.off.sync, "onSync", function(type){ // if(type == "download"){ // // make a network call to download some data // // for use offline // dojo.xhrGet({ // url: "downloadData.php", // handleAs: "javascript", // error: function(err){ // dojox.off.sync.finishedDownloading(false, "Can't download data"); // }, // load: function(data){ // // store our data // dojox.storage.put("myData", data); // // // indicate we are finished downloading // dojox.off.sync.finishedDownloading(true); // } // }); // }else if(type == "finished"){ // // update UI somehow to indicate we are finished, // // such as using the download data to change the // // available data // } // }) // // Here is the full list of event types if you want to do deep // customization, such as updating your UI to display the progress // of syncing (note that the default Dojo Offline UI widget does // this for you if you choose to pull that in). Most of these // are only appropriate for advanced usage and can be safely // ignored: // // * "start" // syncing has started // * "refreshFiles" // syncing will begin refreshing // our offline file cache // * "upload" // syncing will begin uploading // any local data changes we have on the client. // This event is fired before we fire // the dojox.off.sync.actions.onReplay event for // each action to replay; use it to completely // over-ride the replaying behavior and prevent // it entirely, perhaps rolling your own sync // protocol if needed. // * "download" // syncing will begin downloading any new data that is // needed into persistent storage. Applications are required to // implement this themselves, storing the required data into // persistent local storage using Dojo Storage. // * "finished" // syncing is finished; this // will be called whether an error ocurred or not; check // dojox.off.sync.successful and dojox.off.sync.error for sync details // * "cancel" // Fired when canceling has been initiated; canceling will be // attempted, followed by the sync event "finished". onSync: function(/* String */ type){}, synchronize: function(){ /* void */ // summary: Starts synchronizing //dojo.debug("synchronize"); if(this.isSyncing || dojox.off.goingOnline || (!dojox.off.isOnline)){ return; } this.isSyncing = true; this.successful = false; this.details = []; this.cancelled = false; this.start(); }, cancel: function(){ /* void */ // summary: // Attempts to cancel this sync session if(!this.isSyncing){ return; } this.cancelled = true; if(dojox.off.files.refreshing){ dojox.off.files.abortRefresh(); } this.onSync("cancel"); }, finishedDownloading: function(successful /* boolean? */, errorMessage /* String? */){ // summary: // Applications call this method from their // after getting a "download" event in // dojox.off.sync.onSync to signal that // they are finished downloading any data // that should be available offline // successful: boolean? // Whether our downloading was successful or not. // If not present, defaults to true. // errorMessage: String? // If unsuccessful, a message explaining why if(typeof successful == "undefined"){ successful = true; } if(!successful){ this.successful = false; this.details.push(errorMessage); this.error = true; } this.finished(); }, start: function(){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Called at the start of the syncing process. Advanced // developers can over-ride this method to use their // own sync mechanism to start syncing. if(this.cancelled){ this.finished(); return; } this.onSync("start"); this.refreshFiles(); }, refreshFiles: function(){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Called when we are going to refresh our list // of offline files during syncing. Advanced developers // can over-ride this method to do some advanced magic related to // refreshing files. //dojo.debug("refreshFiles"); if(this.cancelled){ this.finished(); return; } this.onSync("refreshFiles"); dojox.off.files.refresh(dojo.hitch(this, function(error, errorMessages){ if(error){ this.error = true; this.successful = false; for(var i = 0; i < errorMessages.length; i++){ this.details.push(errorMessages[i]); } // even if we get an error while syncing files, // keep syncing so we can upload and download // data } this.upload(); })); }, upload: function(){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Called when syncing wants to upload data. Advanced // developers can over-ride this method to completely // throw away the Action Log and replaying system // and roll their own advanced sync mechanism if needed. if(this.cancelled){ this.finished(); return; } this.onSync("upload"); // when we are done uploading start downloading dojo.connect(this.actions, "onReplayFinished", this, this.download); // replay the actions log this.actions.replay(); }, download: function(){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Called when syncing wants to download data. Advanced // developers can over-ride this method to use their // own sync mechanism. if(this.cancelled){ this.finished(); return; } // apps should respond to the "download" // event to download their data; when done // they must call dojox.off.sync.finishedDownloading() this.onSync("download"); }, finished: function(){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Called when syncing is finished. Advanced // developers can over-ride this method to clean // up after finishing their own sync // mechanism they might have rolled. this.isSyncing = false; this.successful = (!this.cancelled && !this.error); this.onSync("finished"); }, _save: function(callback){ this.actions._save(function(){ callback(); }); }, _load: function(callback){ this.actions._load(function(){ callback(); }); } }); // summary: // A class that records actions taken by a user when they are offline, // suitable for replaying when the network reappears. // description: // The basic idea behind this method is to record user actions that would // normally have to contact a server into an action log when we are // offline, so that later when we are online we can simply replay this log // in the order user actions happened so that they can be executed against // the server, causing synchronization to happen. // // When we replay, for each of the actions that were added, we call a // method named onReplay that applications should connect to and // which will be called over and over for each of our actions -- // applications should take the offline action // information and use it to talk to a server to have this action // actually happen online, 'syncing' themselves with the server. // // For example, if the action was "update" with the item that was updated, we // might call some RESTian server API that exists for updating an item in // our application. The server could either then do sophisticated merging // and conflict resolution on the server side, for example, allowing you // to pop up a custom merge UI, or could do automatic merging or nothing // of the sort. When you are finished with this particular action, your // application is then required to call continueReplay() on the actionLog object // passed to onReplay() to continue replaying the action log, or haltReplay() // with the reason for halting to completely stop the syncing/replaying // process. // // For example, imagine that we have a web application that allows us to add // contacts. If we are offline, and we update a contact, we would add an action; // imagine that the user has to click an Update button after changing the values // for a given contact: // // dojox.off.whenOffline(dojo.byId("updateButton"), "onclick", function(evt){ // // get the updated customer values // var customer = getCustomerValues(); // // // we are offline -- just record this action // var action = {name: "update", customer: customer}; // dojox.off.sync.actions.add(action) // // // persist this customer data into local storage as well // dojox.storage.put(customer.name, customer); // }) // // Then, when we go back online, the dojox.off.sync.actions.onReplay event // will fire over and over, once for each action that was recorded while offline: // // dojo.connect(dojox.off.sync.actions, "onReplay", function(action, actionLog){ // // called once for each action we added while offline, in the order // // they were added // if(action.name == "update"){ // var customer = action.customer; // // // call some network service to update this customer // dojo.xhrPost({ // url: "updateCustomer.php", // content: {customer: dojo.toJson(customer)}, // error: function(err){ // actionLog.haltReplay(err); // }, // load: function(data){ // actionLog.continueReplay(); // } // }) // } // }) // // Note that the actions log is always automatically persisted locally while using it, so // that if the user closes the browser or it crashes the actions will safely be stored // for later replaying. dojo.declare("dojox.off.sync.ActionLog", null, { // entries: Array // An array of our action entries, where each one is simply a custom // object literal that were passed to add() when this action entry // was added. entries: [], // reasonHalted: String // If we halted, the reason why reasonHalted: null, // isReplaying: boolean // If true, we are in the middle of replaying a command log; if false, // then we are not isReplaying: false, // autoSave: boolean // Whether we automatically save the action log after each call to // add(); defaults to true. For applications that are rapidly adding // many action log entries in a short period of time, it can be // useful to set this to false and simply call save() yourself when // you are ready to persist your command log -- otherwise performance // could be slow as the default action is to attempt to persist the // actions log constantly with calls to add(). autoSave: true, add: function(action /* Object */){ /* void */ // summary: // Adds an action to our action log // description: // This method will add an action to our // action log, later to be replayed when we // go from offline to online. 'action' // will be available when this action is // replayed and will be passed to onReplay. // // Example usage: // // dojox.off.sync.log.add({actionName: "create", itemType: "document", // {title: "Message", content: "Hello World"}}); // // The object literal is simply a custom object appropriate // for our application -- it can be anything that preserves the state // of a user action that will be executed when we go back online // and replay this log. In the above example, // "create" is the name of this action; "documents" is the // type of item this command is operating on, such as documents, contacts, // tasks, etc.; and the final argument is the document that was created. if(this.isReplaying){ throw "Programming error: you can not call " + "dojox.off.sync.actions.add() while " + "we are replaying an action log"; } this.entries.push(action); // save our updated state into persistent // storage if(this.autoSave){ this._save(); } }, onReplay: function(action /* Object */, actionLog /* dojox.off.sync.ActionLog */){ /* void */ // summary: // Called when we replay our log, for each of our action // entries. // action: Object // A custom object literal representing an action for this // application, such as // {actionName: "create", item: {title: "message", content: "hello world"}} // actionLog: dojox.off.sync.ActionLog // A reference to the dojox.off.sync.actions log so that developers // can easily call actionLog.continueReplay() or actionLog.haltReplay(). // description: // This callback should be connected to by applications so that // they can sync themselves when we go back online: // // dojo.connect(dojox.off.sync.actions, "onReplay", function(action, actionLog){ // // do something // }) // // When we replay our action log, this callback is called for each // of our action entries in the order they were added. The // 'action' entry that was passed to add() for this action will // also be passed in to onReplay, so that applications can use this information // to do their syncing, such as contacting a server web-service // to create a new item, for example. // // Inside the method you connected to onReplay, you should either call // actionLog.haltReplay(reason) if an error occurred and you would like to halt // action replaying or actionLog.continueReplay() to have the action log // continue replaying its log and proceed to the next action; // the reason you must call these is the action you execute inside of // onAction will probably be asynchronous, since it will be talking on // the network, and you should call one of these two methods based on // the result of your network call. }, length: function(){ /* Number */ // summary: // Returns the length of this // action log return this.entries.length; }, haltReplay: function(reason /* String */){ /* void */ // summary: Halts replaying this command log. // reason: String // The reason we halted. // description: // This method is called as we are replaying an action log; it // can be called from dojox.off.sync.actions.onReplay, for // example, for an application to indicate an error occurred // while replaying this action, halting further processing of // the action log. Note that any action log entries that // were processed before have their effects retained (i.e. // they are not rolled back), while the action entry that was // halted stays in our list of actions to later be replayed. if(!this.isReplaying){ return; } if(reason){ this.reasonHalted = reason.toString(); } // save the state of our action log, then // tell anyone who is interested that we are // done when we are finished saving if(this.autoSave){ var self = this; this._save(function(){ self.isReplaying = false; self.onReplayFinished(); }); }else{ this.isReplaying = false; this.onReplayFinished(); } }, continueReplay: function(){ /* void */ // summary: // Indicates that we should continue processing out list of // actions. // description: // This method is called by applications that have overridden // dojox.off.sync.actions.onReplay() to continue replaying our // action log after the application has finished handling the // current action. if(!this.isReplaying){ return; } // shift off the old action we just ran this.entries.shift(); // are we done? if(!this.entries.length){ // save the state of our action log, then // tell anyone who is interested that we are // done when we are finished saving if(this.autoSave){ var self = this; this._save(function(){ self.isReplaying = false; self.onReplayFinished(); }); return; }else{ this.isReplaying = false; this.onReplayFinished(); return; } } // get the next action var nextAction = this.entries[0]; this.onReplay(nextAction, this); }, clear: function(){ /* void */ // summary: // Completely clears this action log of its entries if(this.isReplaying){ return; } this.entries = []; // save our updated state into persistent // storage if(this.autoSave){ this._save(); } }, replay: function(){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Replays all of the commands that have been // cached in this command log when we go back online; // onCommand will be called for each command we have if(this.isReplaying){ return; } this.reasonHalted = null; if(!this.entries.length){ this.onReplayFinished(); return; } this.isReplaying = true; var nextAction = this.entries[0]; this.onReplay(nextAction, this); }, // onReplayFinished: Function // For advanced usage; most developers can ignore this. // Called when we are finished replaying our commands; // called if we have successfully exhausted all of our // commands, or if an error occurred during replaying. // The default implementation simply continues the // synchronization process. Connect to this to register // for the event: // // dojo.connect(dojox.off.sync.actions, "onReplayFinished", // someFunc) onReplayFinished: function(){ }, toString: function(){ var results = ""; results += "["; for(var i = 0; i < this.entries.length; i++){ results += "{"; for(var j in this.entries[i]){ results += j + ": \"" + this.entries[i][j] + "\""; results += ", "; } results += "}, "; } results += "]"; return results; }, _save: function(callback){ if(!callback){ callback = function(){}; } try{ var self = this; var resultsHandler = function(status, key, message){ //console.debug("resultsHandler, status="+status+", key="+key+", message="+message); if(status == dojox.storage.FAILED){ dojox.off.onFrameworkEvent("save", {status: dojox.storage.FAILED, isCoreSave: true, key: key, value: message, namespace: dojox.off.STORAGE_NAMESPACE}); callback(); }else if(status == dojox.storage.SUCCESS){ callback(); } }; dojox.storage.put("actionlog", this.entries, resultsHandler, dojox.off.STORAGE_NAMESPACE); }catch(exp){ console.debug("dojox.off.sync._save: " + exp.message||exp); dojox.off.onFrameworkEvent("save", {status: dojox.storage.FAILED, isCoreSave: true, key: "actionlog", value: this.entries, namespace: dojox.off.STORAGE_NAMESPACE}); callback(); } }, _load: function(callback){ var entries = dojox.storage.get("actionlog", dojox.off.STORAGE_NAMESPACE); if(!entries){ entries = []; } this.entries = entries; callback(); } } ); dojox.off.sync.actions = new dojox.off.sync.ActionLog(); } if(!dojo._hasResource["dojox.off._common"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.off._common"] = true; dojo.provide("dojox.off._common"); // Author: Brad Neuberg, bkn3@columbia.edu, http://codinginparadise.org // summary: // dojox.off is the main object for offline applications. dojo.mixin(dojox.off, { // isOnline: boolean // true if we are online, false if not isOnline: false, // NET_CHECK: int // For advanced usage; most developers can ignore this. // Time in seconds on how often we should check the status of the // network with an automatic background timer. The current default // is 5 seconds. NET_CHECK: 5, // STORAGE_NAMESPACE: String // For advanced usage; most developers can ignore this. // The namespace we use to save core data into Dojo Storage. STORAGE_NAMESPACE: "_dot", // enabled: boolean // For advanced usage; most developers can ignore this. // Whether offline ability is enabled or not. Defaults to true. enabled: true, // availabilityURL: String // For advanced usage; most developers can ignore this. // The URL to check for site availability. We do a GET request on // this URL to check for site availability. By default we check for a // simple text file in src/off/network_check.txt that has one value // it, the value '1'. availabilityURL: dojo.moduleUrl("dojox", "off/network_check.txt"), // goingOnline: boolean // For advanced usage; most developers can ignore this. // True if we are attempting to go online, false otherwise goingOnline: false, // coreOpFailed: boolean // For advanced usage; most developers can ignore this. // A flag set by the Dojo Offline framework that indicates that the // user denied some operation that required the offline cache or an // operation failed in some critical way that was unrecoverable. For // example, if the offline cache is Google Gears and we try to get a // Gears database, a popup window appears asking the user whether they // will approve or deny this request. If the user denies the request, // and we are doing some operation that is core to Dojo Offline, then // we set this flag to 'true'. This flag causes a 'fail fast' // condition, turning off offline ability. coreOpFailed: false, // doNetChecking: boolean // For advanced usage; most developers can ignore this. // Whether to have a timing interval in the background doing automatic // network checks at regular intervals; the length of time between // checks is controlled by dojox.off.NET_CHECK. Defaults to true. doNetChecking: true, // hasOfflineCache: boolean // For advanced usage; most developers can ignore this. // Determines if an offline cache is available or installed; an // offline cache is a facility that can truely cache offline // resources, such as JavaScript, HTML, etc. in such a way that they // won't be removed from the cache inappropriately like a browser // cache would. If this is false then an offline cache will be // installed. Only Google Gears is currently supported as an offline // cache. Future possible offline caches include Firefox 3. hasOfflineCache: null, // browserRestart: boolean // For advanced usage; most developers can ignore this. // If true, the browser must be restarted to register the existence of // a new host added offline (from a call to addHostOffline); if false, // then nothing is needed. browserRestart: false, _STORAGE_APP_NAME: window.location.href.replace(/[^0-9A-Za-z_]/g, "_"), _initializeCalled: false, _storageLoaded: false, _pageLoaded: false, onLoad: function(){ // summary: // Called when Dojo Offline can be used. // description: // Do a dojo.connect to this to know when you can // start using Dojo Offline: // dojo.connect(dojox.off, "onLoad", myFunc); }, onNetwork: function(type){ // summary: // Called when our on- or offline- status changes. // description: // If we move online, then this method is called with the // value "online". If we move offline, then this method is // called with the value "offline". You can connect to this // method to do add your own behavior: // // dojo.connect(dojox.off, "onNetwork", someFunc) // // Note that if you are using the default Dojo Offline UI // widget that most of the on- and off-line notification // and syncing is automatically handled and provided to the // user. // type: String // Either "online" or "offline". }, initialize: function(){ /* void */ // summary: // Called when a Dojo Offline-enabled application is finished // configuring Dojo Offline, and is ready for Dojo Offline to // initialize itself. // description: // When an application has finished filling out the variables Dojo // Offline needs to work, such as dojox.off.ui.appName, it must // this method to tell Dojo Offline to initialize itself. // Note: // This method is needed for a rare edge case. In some conditions, // especially if we are dealing with a compressed Dojo build, the // entire Dojo Offline subsystem might initialize itself and be // running even before the JavaScript for an application has had a // chance to run and configure Dojo Offline, causing Dojo Offline // to have incorrect initialization parameters for a given app, // such as no value for dojox.off.ui.appName. This method is // provided to prevent this scenario, to slightly 'slow down' Dojo // Offline so it can be configured before running off and doing // its thing. //console.debug("dojox.off.initialize"); this._initializeCalled = true; if(this._storageLoaded && this._pageLoaded){ this._onLoad(); } }, goOffline: function(){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Manually goes offline, away from the network. if((dojox.off.sync.isSyncing)||(this.goingOnline)){ return; } this.goingOnline = false; this.isOnline = false; }, goOnline: function(callback){ /* void */ // summary: // For advanced usage; most developers can ignore this. // Attempts to go online. // description: // Attempts to go online, making sure this web application's web // site is available. 'callback' is called asychronously with the // result of whether we were able to go online or not. // callback: Function // An optional callback function that will receive one argument: // whether the site is available or not and is boolean. If this // function is not present we call dojo.xoff.onOnline instead if // we are able to go online. //console.debug("goOnline"); if(dojox.off.sync.isSyncing || dojox.off.goingOnline){ return; } this.goingOnline = true; this.isOnline = false; // see if can reach our web application's web site this._isSiteAvailable(callback); }, onFrameworkEvent: function(type /* String */, saveData /* Object? */){ // summary: // For advanced usage; most developers can ignore this. // A standard event handler that can be attached to to find out // about low-level framework events. Most developers will not need to // attach to this method; it is meant for low-level information // that can be useful for updating offline user-interfaces in // exceptional circumstances. The default Dojo Offline UI // widget takes care of most of these situations. // type: String // The type of the event: // // * "offlineCacheInstalled" // An event that is fired when a user // has installed an offline cache after the page has been loaded. // If a user didn't have an offline cache when the page loaded, a // UI of some kind might have prompted them to download one. This // method is called if they have downloaded and installed an // offline cache so a UI can reinitialize itself to begin using // this offline cache. // * "coreOperationFailed" // Fired when a core operation during interaction with the // offline cache is denied by the user. Some offline caches, such // as Google Gears, prompts the user to approve or deny caching // files, using the database, and more. If the user denies a // request that is core to Dojo Offline's operation, we set // dojox.off.coreOpFailed to true and call this method for // listeners that would like to respond some how to Dojo Offline // 'failing fast'. // * "save" // Called whenever the framework saves data into persistent // storage. This could be useful for providing save feedback // or providing appropriate error feedback if saving fails // due to a user not allowing the save to occur // saveData: Object? // If the type was 'save', then a saveData object is provided with // further save information. This object has the following properties: // // * status - dojox.storage.SUCCESS, dojox.storage.PENDING, dojox.storage.FAILED // Whether the save succeeded, whether it is pending based on a UI // dialog asking the user for permission, or whether it failed. // // * isCoreSave - boolean // If true, then this save was for a core piece of data necessary // for the functioning of Dojo Offline. If false, then it is a // piece of normal data being saved for offline access. Dojo // Offline will 'fail fast' if some core piece of data could not // be saved, automatically setting dojox.off.coreOpFailed to // 'true' and dojox.off.enabled to 'false'. // // * key - String // The key that we are attempting to persist // // * value - Object // The object we are trying to persist // // * namespace - String // The Dojo Storage namespace we are saving this key/value pair // into, such as "default", "Documents", "Contacts", etc. // Optional. if(type == "save"){ if(saveData.isCoreSave && (saveData.status == dojox.storage.FAILED)){ dojox.off.coreOpFailed = true; dojox.off.enabled = false; // FIXME: Stop the background network thread dojox.off.onFrameworkEvent("coreOperationFailed"); } }else if(type == "coreOperationFailed"){ dojox.off.coreOpFailed = true; dojox.off.enabled = false; // FIXME: Stop the background network thread } }, _checkOfflineCacheAvailable: function(callback){ // is a true, offline cache running on this machine? this.hasOfflineCache = dojo.isGears; callback(); }, _onLoad: function(){ //console.debug("dojox.off._onLoad"); // both local storage and the page are finished loading // cache the Dojo JavaScript -- just use the default dojo.js // name for the most common scenario // FIXME: TEST: Make sure syncing doesn't break if dojo.js // can't be found, or report an error to developer dojox.off.files.cache(dojo.moduleUrl("dojo", "dojo.js")); // pull in the files needed by Dojo this._cacheDojoResources(); // FIXME: need to pull in the firebug lite files here! // workaround or else we will get an error on page load // from Dojo that it can't find 'console.debug' for optimized builds // dojox.off.files.cache(dojo.config.baseRelativePath + "src/debug.js"); // make sure that resources needed by all of our underlying // Dojo Storage storage providers will be available // offline dojox.off.files.cache(dojox.storage.manager.getResourceList()); // slurp the page if the end-developer wants that dojox.off.files._slurp(); // see if we have an offline cache; when done, move // on to the rest of our startup tasks this._checkOfflineCacheAvailable(dojo.hitch(this, "_onOfflineCacheChecked")); }, _onOfflineCacheChecked: function(){ // this method is part of our _onLoad series of startup tasks // if we have an offline cache, see if we have been added to the // list of available offline web apps yet if(this.hasOfflineCache && this.enabled){ // load framework data; when we are finished, continue // initializing ourselves this._load(dojo.hitch(this, "_finishStartingUp")); }else if(this.hasOfflineCache && !this.enabled){ // we have an offline cache, but it is disabled for some reason // perhaps due to the user denying a core operation this._finishStartingUp(); }else{ this._keepCheckingUntilInstalled(); } }, _keepCheckingUntilInstalled: function(){ // this method is part of our _onLoad series of startup tasks // kick off a background interval that keeps // checking to see if an offline cache has been // installed since this page loaded // FIXME: Gears: See if we are installed somehow after the // page has been loaded // now continue starting up this._finishStartingUp(); }, _finishStartingUp: function(){ //console.debug("dojox.off._finishStartingUp"); // this method is part of our _onLoad series of startup tasks if(!this.hasOfflineCache){ this.onLoad(); }else if(this.enabled){ // kick off a thread to check network status on // a regular basis this._startNetworkThread(); // try to go online this.goOnline(dojo.hitch(this, function(){ //console.debug("Finished trying to go online"); // indicate we are ready to be used dojox.off.onLoad(); })); }else{ // we are disabled or a core operation failed if(this.coreOpFailed){ this.onFrameworkEvent("coreOperationFailed"); }else{ this.onLoad(); } } }, _onPageLoad: function(){ //console.debug("dojox.off._onPageLoad"); this._pageLoaded = true; if(this._storageLoaded && this._initializeCalled){ this._onLoad(); } }, _onStorageLoad: function(){ //console.debug("dojox.off._onStorageLoad"); this._storageLoaded = true; // were we able to initialize storage? if // not, then this is a core operation, and // let's indicate we will need to fail fast if(!dojox.storage.manager.isAvailable() && dojox.storage.manager.isInitialized()){ this.coreOpFailed = true; this.enabled = false; } if(this._pageLoaded && this._initializeCalled){ this._onLoad(); } }, _isSiteAvailable: function(callback){ // summary: // Determines if our web application's website is available. // description: // This method will asychronously determine if our web // application's web site is available, which is a good proxy for // network availability. The URL dojox.off.availabilityURL is // used, which defaults to this site's domain name (ex: // foobar.com). We check for dojox.off.AVAILABILITY_TIMEOUT (in // seconds) and abort after that // callback: Function // An optional callback function that will receive one argument: // whether the site is available or not and is boolean. If this // function is not present we call dojox.off.onNetwork instead if we // are able to go online. dojo.xhrGet({ url: this._getAvailabilityURL(), handleAs: "text", timeout: this.NET_CHECK * 1000, error: dojo.hitch(this, function(err){ //console.debug("dojox.off._isSiteAvailable.error: " + err); this.goingOnline = false; this.isOnline = false; if(callback){ callback(false); } }), load: dojo.hitch(this, function(data){ //console.debug("dojox.off._isSiteAvailable.load, data="+data); this.goingOnline = false; this.isOnline = true; if(callback){ callback(true); }else{ this.onNetwork("online"); } }) }); }, _startNetworkThread: function(){ //console.debug("startNetworkThread"); // kick off a thread that does periodic // checks on the status of the network if(!this.doNetChecking){ return; } window.setInterval(dojo.hitch(this, function(){ var d = dojo.xhrGet({ url: this._getAvailabilityURL(), handleAs: "text", timeout: this.NET_CHECK * 1000, error: dojo.hitch(this, function(err){ if(this.isOnline){ this.isOnline = false; // FIXME: xhrGet() is not // correctly calling abort // on the XHR object when // it times out; fix inside // there instead of externally // here try{ if(typeof d.ioArgs.xhr.abort == "function"){ d.ioArgs.xhr.abort(); } }catch(e){} // if things fell in the middle of syncing, // stop syncing dojox.off.sync.isSyncing = false; this.onNetwork("offline"); } } ), load: dojo.hitch(this, function(data){ if(!this.isOnline){ this.isOnline = true; this.onNetwork("online"); } } ) }); }), this.NET_CHECK * 1000); }, _getAvailabilityURL: function(){ var url = this.availabilityURL.toString(); // bust the browser's cache to make sure we are really talking to // the server if(url.indexOf("?") == -1){ url += "?"; }else{ url += "&"; } url += "browserbust=" + new Date().getTime(); return url; }, _onOfflineCacheInstalled: function(){ this.onFrameworkEvent("offlineCacheInstalled"); }, _cacheDojoResources: function(){ // if we are a non-optimized build, then the core Dojo bootstrap // system was loaded as separate JavaScript files; // add these to our offline cache list. these are // loaded before the dojo.require() system exists // FIXME: create a better mechanism in the Dojo core to // expose whether you are dealing with an optimized build; // right now we just scan the SCRIPT tags attached to this // page and see if there is one for _base/_loader/bootstrap.js var isOptimizedBuild = true; dojo.forEach(dojo.query("script"), function(i){ var src = i.getAttribute("src"); if(!src){ return; } if(src.indexOf("_base/_loader/bootstrap.js") != -1){ isOptimizedBuild = false; } }); if(!isOptimizedBuild){ dojox.off.files.cache(dojo.moduleUrl("dojo", "_base.js").uri); dojox.off.files.cache(dojo.moduleUrl("dojo", "_base/_loader/loader.js").uri); dojox.off.files.cache(dojo.moduleUrl("dojo", "_base/_loader/bootstrap.js").uri); // FIXME: pull in the host environment file in a more generic way // for other host environments dojox.off.files.cache(dojo.moduleUrl("dojo", "_base/_loader/hostenv_browser.js").uri); } // add anything that was brought in with a // dojo.require() that resulted in a JavaScript // URL being fetched // FIXME: modify dojo/_base/_loader/loader.js to // expose a public API to get this information for(var i = 0; i < dojo._loadedUrls.length; i++){ dojox.off.files.cache(dojo._loadedUrls[i]); } // FIXME: add the standard Dojo CSS file }, _save: function(){ // summary: // Causes the Dojo Offline framework to save its configuration // data into local storage. }, _load: function(callback){ // summary: // Causes the Dojo Offline framework to load its configuration // data from local storage dojox.off.sync._load(callback); } }); // wait until the storage system is finished loading dojox.storage.manager.addOnLoad(dojo.hitch(dojox.off, "_onStorageLoad")); // wait until the page is finished loading dojo.addOnLoad(dojox.off, "_onPageLoad"); } if(!dojo._hasResource["dojox.off"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.off"] = true; dojo.provide("dojox.off"); } if(!dojo._hasResource["dojox.off.ui"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.off.ui"] = true; dojo.provide("dojox.off.ui"); // Author: Brad Neuberg, bkn3@columbia.edu, http://codinginparadise.org // summary: // dojox.off.ui provides a standard, // default user-interface for a // Dojo Offline Widget that can easily // be dropped into applications that would // like to work offline. dojo.mixin(dojox.off.ui, { // appName: String // This application's name, such as "Foobar". Note that // this is a string, not HTML, so embedded markup will // not work, including entities. Only the following // characters are allowed: numbers, letters, and spaces. // You must set this property. appName: "setme", // autoEmbed: boolean // For advanced usage; most developers can ignore this. // Whether to automatically auto-embed the default Dojo Offline // widget into this page; default is true. autoEmbed: true, // autoEmbedID: String // For advanced usage; most developers can ignore this. // The ID of the DOM element that will contain our // Dojo Offline widget; defaults to the ID 'dot-widget'. autoEmbedID: "dot-widget", // runLink: String // For advanced usage; most developers can ignore this. // The URL that should be navigated to to run this // application offline; this will be placed inside of a // link that the user can drag to their desktop and double // click. Note that this URL must exactly match the URL // of the main page of our resource that is offline for // it to be retrieved from the offline cache correctly. // For example, if you have cached your main page as // http://foobar.com/index.html, and you set this to // http://www.foobar.com/index.html, the run link will // not work. By default this value is automatically set to // the URL of this page, so it does not need to be set // manually unless you have unusual needs. runLink: window.location.href, // runLinkTitle: String // For advanced usage; most developers can ignore this. // The text that will be inside of the link that a user // can drag to their desktop to run this application offline. // By default this is automatically set to "Run " plus your // application's name. runLinkTitle: "Run Application", // learnHowPath: String // For advanced usage; most developers can ignore this. // The path to a web page that has information on // how to use this web app offline; defaults to // src/off/ui-template/learnhow.html, relative to // your Dojo installation. Make sure to set // dojo.to.ui.customLearnHowPath to true if you want // a custom Learn How page. learnHowPath: dojo.moduleUrl("dojox", "off/resources/learnhow.html"), // customLearnHowPath: boolean // For advanced usage; most developers can ignore this. // Whether the developer is using their own custom page // for the Learn How instructional page; defaults to false. // Use in conjunction with dojox.off.ui.learnHowPath. customLearnHowPath: false, htmlTemplatePath: dojo.moduleUrl("dojox", "off/resources/offline-widget.html").uri, cssTemplatePath: dojo.moduleUrl("dojox", "off/resources/offline-widget.css").uri, onlineImagePath: dojo.moduleUrl("dojox", "off/resources/greenball.png").uri, offlineImagePath: dojo.moduleUrl("dojox", "off/resources/redball.png").uri, rollerImagePath: dojo.moduleUrl("dojox", "off/resources/roller.gif").uri, checkmarkImagePath: dojo.moduleUrl("dojox", "off/resources/checkmark.png").uri, learnHowJSPath: dojo.moduleUrl("dojox", "off/resources/learnhow.js").uri, _initialized: false, onLoad: function(){ // summary: // A function that should be connected to allow your // application to know when Dojo Offline, the page, and // the Offline Widget are all initialized and ready to be // used: // // dojo.connect(dojox.off.ui, "onLoad", someFunc) }, _initialize: function(){ //console.debug("dojox.off.ui._initialize"); // make sure our app name is correct if(this._validateAppName(this.appName) == false){ alert("You must set dojox.off.ui.appName; it can only contain " + "letters, numbers, and spaces; right now it " + "is incorrectly set to '" + dojox.off.ui.appName + "'"); dojox.off.enabled = false; return; } // set our run link text to its default this.runLinkText = "Run " + this.appName; // setup our event listeners for Dojo Offline events // to update our UI dojo.connect(dojox.off, "onNetwork", this, "_onNetwork"); dojo.connect(dojox.off.sync, "onSync", this, "_onSync"); // cache our default UI resources dojox.off.files.cache([ this.htmlTemplatePath, this.cssTemplatePath, this.onlineImagePath, this.offlineImagePath, this.rollerImagePath, this.checkmarkImagePath ]); // embed the offline widget UI if(this.autoEmbed){ this._doAutoEmbed(); } }, _doAutoEmbed: function(){ // fetch our HTML for the offline widget // dispatch the request dojo.xhrGet({ url: this.htmlTemplatePath, handleAs: "text", error: function(err){ dojox.off.enabled = false; err = err.message||err; alert("Error loading the Dojo Offline Widget from " + this.htmlTemplatePath + ": " + err); }, load: dojo.hitch(this, this._templateLoaded) }); }, _templateLoaded: function(data){ //console.debug("dojox.off.ui._templateLoaded"); // inline our HTML var container = dojo.byId(this.autoEmbedID); if(container){ container.innerHTML = data; } // fill out our image paths this._initImages(); // update our network indicator status ball this._updateNetIndicator(); // update our 'Learn How' text this._initLearnHow(); this._initialized = true; // check offline cache settings if(!dojox.off.hasOfflineCache){ this._showNeedsOfflineCache(); return; } // check to see if we need a browser restart // to be able to use this web app offline if(dojox.off.hasOfflineCache && dojox.off.browserRestart){ this._needsBrowserRestart(); return; }else{ var browserRestart = dojo.byId("dot-widget-browser-restart"); if(browserRestart){ browserRestart.style.display = "none"; } } // update our sync UI this._updateSyncUI(); // register our event listeners for our main buttons this._initMainEvtHandlers(); // if offline functionality is disabled, disable everything this._setOfflineEnabled(dojox.off.enabled); // update our UI based on the state of the network this._onNetwork(dojox.off.isOnline ? "online" : "offline"); // try to go online this._testNet(); }, _testNet: function(){ dojox.off.goOnline(dojo.hitch(this, function(isOnline){ //console.debug("testNet callback, isOnline="+isOnline); // display our online/offline results this._onNetwork(isOnline ? "online" : "offline"); // indicate that our default UI // and Dojo Offline are now ready to // be used this.onLoad(); })); }, _updateNetIndicator: function(){ var onlineImg = dojo.byId("dot-widget-network-indicator-online"); var offlineImg = dojo.byId("dot-widget-network-indicator-offline"); var titleText = dojo.byId("dot-widget-title-text"); if(onlineImg && offlineImg){ if(dojox.off.isOnline == true){ onlineImg.style.display = "inline"; offlineImg.style.display = "none"; }else{ onlineImg.style.display = "none"; offlineImg.style.display = "inline"; } } if(titleText){ if(dojox.off.isOnline){ titleText.innerHTML = "Online"; }else{ titleText.innerHTML = "Offline"; } } }, _initLearnHow: function(){ var learnHow = dojo.byId("dot-widget-learn-how-link"); if(!learnHow){ return; } if(!this.customLearnHowPath){ // add parameters to URL so the Learn How page // can customize itself and display itself // correctly based on framework settings var dojoPath = dojo.config.baseRelativePath; this.learnHowPath += "?appName=" + encodeURIComponent(this.appName) + "&hasOfflineCache=" + dojox.off.hasOfflineCache + "&runLink=" + encodeURIComponent(this.runLink) + "&runLinkText=" + encodeURIComponent(this.runLinkText) + "&baseRelativePath=" + encodeURIComponent(dojoPath); // cache our Learn How JavaScript page and // the HTML version with full query parameters // so it is available offline without a cache miss dojox.off.files.cache(this.learnHowJSPath); dojox.off.files.cache(this.learnHowPath); } learnHow.setAttribute("href", this.learnHowPath); var appName = dojo.byId("dot-widget-learn-how-app-name"); if(!appName){ return; } appName.innerHTML = ""; appName.appendChild(document.createTextNode(this.appName)); }, _validateAppName: function(appName){ if(!appName){ return false; } return (/^[a-z0-9 ]*$/i.test(appName)); }, _updateSyncUI: function(){ var roller = dojo.byId("dot-roller"); var checkmark = dojo.byId("dot-success-checkmark"); var syncMessages = dojo.byId("dot-sync-messages"); var details = dojo.byId("dot-sync-details"); var cancel = dojo.byId("dot-sync-cancel"); if(dojox.off.sync.isSyncing){ this._clearSyncMessage(); if(roller){ roller.style.display = "inline"; } if(checkmark){ checkmark.style.display = "none"; } if(syncMessages){ dojo.removeClass(syncMessages, "dot-sync-error"); } if(details){ details.style.display = "none"; } if(cancel){ cancel.style.display = "inline"; } }else{ if(roller){ roller.style.display = "none"; } if(cancel){ cancel.style.display = "none"; } if(syncMessages){ dojo.removeClass(syncMessages, "dot-sync-error"); } } }, _setSyncMessage: function(message){ var syncMessage = dojo.byId("dot-sync-messages"); if(syncMessage){ // when used with Google Gears pre-release in Firefox/Mac OS X, // the browser would crash when testing in Moxie // if we set the message this way for some reason. // Brad Neuberg, bkn3@columbia.edu //syncMessage.innerHTML = message; while(syncMessage.firstChild){ syncMessage.removeChild(syncMessage.firstChild); } syncMessage.appendChild(document.createTextNode(message)); } }, _clearSyncMessage: function(){ this._setSyncMessage(""); }, _initImages: function(){ var onlineImg = dojo.byId("dot-widget-network-indicator-online"); if(onlineImg){ onlineImg.setAttribute("src", this.onlineImagePath); } var offlineImg = dojo.byId("dot-widget-network-indicator-offline"); if(offlineImg){ offlineImg.setAttribute("src", this.offlineImagePath); } var roller = dojo.byId("dot-roller"); if(roller){ roller.setAttribute("src", this.rollerImagePath); } var checkmark = dojo.byId("dot-success-checkmark"); if(checkmark){ checkmark.setAttribute("src", this.checkmarkImagePath); } }, _showDetails: function(evt){ // cancel the button's default behavior evt.preventDefault(); evt.stopPropagation(); if(!dojox.off.sync.details.length){ return; } // determine our HTML message to display var html = ""; html += "Sync Details"; html += "

Sync Details

\n"; html += "
    \n"; for(var i = 0; i < dojox.off.sync.details.length; i++){ html += "
  • "; html += dojox.off.sync.details[i]; html += "
  • "; } html += "
\n"; html += "" + "Close Window" + "\n"; html += ""; // open a popup window with this message var windowParams = "height=400,width=600,resizable=true," + "scrollbars=true,toolbar=no,menubar=no," + "location=no,directories=no,dependent=yes"; var popup = window.open("", "SyncDetails", windowParams); if(!popup){ // aggressive popup blocker alert("Please allow popup windows for this domain; can't display sync details window"); return; } popup.document.open(); popup.document.write(html); popup.document.close(); // put the focus on the popup window if(popup.focus){ popup.focus(); } }, _cancel: function(evt){ // cancel the button's default behavior evt.preventDefault(); evt.stopPropagation(); dojox.off.sync.cancel(); }, _needsBrowserRestart: function(){ var browserRestart = dojo.byId("dot-widget-browser-restart"); if(browserRestart){ dojo.addClass(browserRestart, "dot-needs-browser-restart"); } var appName = dojo.byId("dot-widget-browser-restart-app-name"); if(appName){ appName.innerHTML = ""; appName.appendChild(document.createTextNode(this.appName)); } var status = dojo.byId("dot-sync-status"); if(status){ status.style.display = "none"; } }, _showNeedsOfflineCache: function(){ var widgetContainer = dojo.byId("dot-widget-container"); if(widgetContainer){ dojo.addClass(widgetContainer, "dot-needs-offline-cache"); } }, _hideNeedsOfflineCache: function(){ var widgetContainer = dojo.byId("dot-widget-container"); if(widgetContainer){ dojo.removeClass(widgetContainer, "dot-needs-offline-cache"); } }, _initMainEvtHandlers: function(){ var detailsButton = dojo.byId("dot-sync-details-button"); if(detailsButton){ dojo.connect(detailsButton, "onclick", this, this._showDetails); } var cancelButton = dojo.byId("dot-sync-cancel-button"); if(cancelButton){ dojo.connect(cancelButton, "onclick", this, this._cancel); } }, _setOfflineEnabled: function(enabled){ var elems = []; elems.push(dojo.byId("dot-sync-status")); for(var i = 0; i < elems.length; i++){ if(elems[i]){ elems[i].style.visibility = (enabled ? "visible" : "hidden"); } } }, _syncFinished: function(){ this._updateSyncUI(); var checkmark = dojo.byId("dot-success-checkmark"); var details = dojo.byId("dot-sync-details"); if(dojox.off.sync.successful == true){ this._setSyncMessage("Sync Successful"); if(checkmark){ checkmark.style.display = "inline"; } }else if(dojox.off.sync.cancelled == true){ this._setSyncMessage("Sync Cancelled"); if(checkmark){ checkmark.style.display = "none"; } }else{ this._setSyncMessage("Sync Error"); var messages = dojo.byId("dot-sync-messages"); if(messages){ dojo.addClass(messages, "dot-sync-error"); } if(checkmark){ checkmark.style.display = "none"; } } if(dojox.off.sync.details.length && details){ details.style.display = "inline"; } }, _onFrameworkEvent: function(type, saveData){ if(type == "save"){ if(saveData.status == dojox.storage.FAILED && !saveData.isCoreSave){ alert("Please increase the amount of local storage available " + "to this application"); if(dojox.storage.hasSettingsUI()){ dojox.storage.showSettingsUI(); } // FIXME: Be able to know if storage size has changed // due to user configuration } }else if(type == "coreOperationFailed"){ console.log("Application does not have permission to use Dojo Offline"); if(!this._userInformed){ alert("This application will not work if Google Gears is not allowed to run"); this._userInformed = true; } }else if(type == "offlineCacheInstalled"){ // clear out the 'needs offline cache' info this._hideNeedsOfflineCache(); // check to see if we need a browser restart // to be able to use this web app offline if(dojox.off.hasOfflineCache == true && dojox.off.browserRestart == true){ this._needsBrowserRestart(); return; }else{ var browserRestart = dojo.byId("dot-widget-browser-restart"); if(browserRestart){ browserRestart.style.display = "none"; } } // update our sync UI this._updateSyncUI(); // register our event listeners for our main buttons this._initMainEvtHandlers(); // if offline is disabled, disable everything this._setOfflineEnabled(dojox.off.enabled); // try to go online this._testNet(); } }, _onSync: function(type){ //console.debug("ui, onSync="+type); switch(type){ case "start": this._updateSyncUI(); break; case "refreshFiles": this._setSyncMessage("Downloading UI..."); break; case "upload": this._setSyncMessage("Uploading new data..."); break; case "download": this._setSyncMessage("Downloading new data..."); break; case "finished": this._syncFinished(); break; case "cancel": this._setSyncMessage("Canceling Sync..."); break; default: dojo.warn("Programming error: " + "Unknown sync type in dojox.off.ui: " + type); break; } }, _onNetwork: function(type){ // summary: // Called when we go on- or off-line // description: // When we go online or offline, this method is called to update // our UI. Default behavior is to update the Offline // Widget UI and to attempt a synchronization. // type: String // "online" if we just moved online, and "offline" if we just // moved offline. if(!this._initialized){ return; } // update UI this._updateNetIndicator(); if(type == "offline"){ this._setSyncMessage("You are working offline"); // clear old details var details = dojo.byId("dot-sync-details"); if(details){ details.style.display = "none"; } // if we fell offline during a sync, hide // the sync info this._updateSyncUI(); }else{ // online // synchronize, but pause for a few seconds // so that the user can orient themselves if(dojox.off.sync.autoSync){ if(dojo.isAIR){ window.setTimeout(function(){dojox.off.sync.synchronize();}, 1000); }else{ window.setTimeout(dojox._scopeName + ".off.sync.synchronize()", 1000); } } } } }); // register ourselves for low-level framework events dojo.connect(dojox.off, "onFrameworkEvent", dojox.off.ui, "_onFrameworkEvent"); // start our magic when the Dojo Offline framework is ready to go dojo.connect(dojox.off, "onLoad", dojox.off.ui, dojox.off.ui._initialize); } if(!dojo._hasResource["dojox.off.offline"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dojox.off.offline"] = true; dojo.provide("dojox.off.offline"); }