dojo.provide("dojox.off.files"); // Author: Brad Neuberg, bkn3@columbia.edu, http://codinginparadise.org // summary: // Helps maintain resources that should be // available offline, such as CSS files. // description: // dojox.off.files makes it easy to indicate // what resources should be available offline, // such as CSS files, JavaScript, HTML, etc. dojox.off.files = { // versionURL: String // An optional file, that if present, records the version // of our bundle of files to make available offline. If this // file is present, and we are not currently debugging, // then we only refresh our offline files if the version has // changed. versionURL: "version.js", // listOfURLs: Array // For advanced usage; most developers can ignore this. // Our list of URLs that will be cached and made available // offline. listOfURLs: [], // refreshing: boolean // For advanced usage; most developers can ignore this. // Whether we are currently in the middle // of refreshing our list of offline files. refreshing: false, _cancelID: null, _error: false, _errorMessages: [], _currentFileIndex: 0, _store: null, _doSlurp: false, slurp: function(){ // summary: // Autoscans the page to find all resources to // cache. This includes scripts, images, CSS, and hyperlinks // to pages that are in the same scheme/port/host as this // page. We also scan the embedded CSS of any stylesheets // to find @import statements and url()'s. // You should call this method from the top-level, outside of // any functions and before the page loads: // // // // 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); } }); } }