diff options
Diffstat (limited to 'browser/components/sessionstore/nsSessionStore.js')
-rw-r--r-- | browser/components/sessionstore/nsSessionStore.js | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/browser/components/sessionstore/nsSessionStore.js b/browser/components/sessionstore/nsSessionStore.js new file mode 100644 index 0000000..0ce3401 --- /dev/null +++ b/browser/components/sessionstore/nsSessionStore.js @@ -0,0 +1,541 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the nsSessionStore component. + * + * The Initial Developer of the Original Code is + * Simon Bünzli <zeniko@gmail.com> + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dietrich Ayala <autonome@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Heavily adapted to xulrunner for the OLPC from the firefox code in + * http://lxr.mozilla.org/seamonkey/source/browser/components/sessionstore. + * + * May 2007 Tomeu Vizoso + */ + +/** + * Session Storage and Restoration + * + * Overview + * This service keeps track of a user's session, storing the various bits + * required to return the browser to it's current state. The relevant data is + * stored in memory, and is periodically saved to disk in a file in the + * profile directory. The service is started at first window load, in + * delayedStartup, and will restore the session from the data received from + * the nsSessionStartup service. + */ + +/* :::::::: Constants and Helpers ::::::::::::::: */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +const CID = Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}"); +const CONTRACT_ID = "@mozilla.org/browser/sessionstore;1"; +const CLASS_NAME = "Browser Session Store Service"; + +// sandbox to evaluate JavaScript code from non-trustable sources +var EVAL_SANDBOX = new Components.utils.Sandbox("about:blank"); + +function debug(aMsg) { + aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); + Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService) + .logStringMessage(aMsg); +} + +/* :::::::: The Service ::::::::::::::: */ + +function SessionStoreService() { +} + +SessionStoreService.prototype = { + +/* ........ nsISessionStore API .............. */ + + getBrowserState: function sss_getBrowserState(aBrowser) { + dump("nsSessionStore::getBrowserState\n") + return this._toJSONString(this._getWindowState(aBrowser)); + }, + + setBrowserState: function sss_setBrowserState(aBrowser, aState) { + dump("nsSessionStore::setBrowserState\n") + this.restoreWindow(aBrowser, "(" + aState + ")"); + }, + +/* ........ Saving Functionality .............. */ + + /** + * Store all session data for a window + * @param aSHistory + * nsISHistory reference + */ + _saveWindowHistory: function sss_saveWindowHistory(aSHistory) { + var entries = []; + dump("nsSessionStore._saveWindowHistory " + aSHistory.count); + for (var i = 0; i < aSHistory.count; i++) { + entries.push(this._serializeHistoryEntry(aSHistory.getEntryAtIndex(i, false))); + } + + return entries; + }, + + /** + * Get an object that is a serialized representation of a History entry + * Used for data storage + * @param aEntry + * nsISHEntry instance + * @returns object + */ + _serializeHistoryEntry: function sss_serializeHistoryEntry(aEntry) { + var entry = { url: aEntry.URI.spec, children: [] }; + + if (aEntry.title && aEntry.title != entry.url) { + entry.title = aEntry.title; + } + if (aEntry.isSubFrame) { + entry.subframe = true; + } + if (!(aEntry instanceof Ci.nsISHEntry)) { + return entry; + } + + var cacheKey = aEntry.cacheKey; + if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32) { + entry.cacheKey = cacheKey.data; + } + entry.ID = aEntry.ID; + + var x = {}, y = {}; + aEntry.getScrollPosition(x, y); + entry.scroll = x.value + "," + y.value; + + try { + var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata"); + if (prefPostdata && aEntry.postData && this._checkPrivacyLevel(aEntry.URI.schemeIs("https"))) { + aEntry.postData.QueryInterface(Ci.nsISeekableStream). + seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + var stream = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + stream.init(aEntry.postData); + var postdata = stream.read(stream.available()); + if (prefPostdata == -1 || postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <= prefPostdata) { + entry.postdata = postdata; + } + } + } + catch (ex) { debug(ex); } // POSTDATA is tricky - especially since some extensions don't get it right + + if (!(aEntry instanceof Ci.nsISHContainer)) { + return entry; + } + + for (var i = 0; i < aEntry.childCount; i++) { + var child = aEntry.GetChildAt(i); + if (child) { + entry.children.push(this._serializeHistoryEntry(child)); + } + else { // to maintain the correct frame order, insert a dummy entry + entry.children.push({ url: "about:blank" }); + } + } + + return entry; + }, + + /** + * serialize session data for a window + * @param aBrowser + * Browser reference + * @returns string + */ + _getWindowState: function sss_getWindowState(aBrowser) { + dump("nsSessionStore::_getWindowState: " + aBrowser + "\n") + windowState = this._collectWindowData(aBrowser); + + /* + this._updateCookies(windowState); + */ + + return windowState; + }, + + _collectWindowData: function sss_collectWindowData(aBrowser) { + dump("nsSessionStore::_collectWindowData\n") + aBrowser.QueryInterface(Ci.nsIWebNavigation); + historyState = this._saveWindowHistory(aBrowser.sessionHistory); + /* + this._updateTextAndScrollData(aWindow); + this._updateCookieHosts(aWindow); + this._updateWindowFeatures(aWindow); + */ + + return {history: historyState/*, textAndScroll: textAndScrollState*/}; + }, + +/* ........ Restoring Functionality .............. */ + + /** + * restore features to a single window + * @param aBrowser + * Browser reference + * @param aState + * JS object or its eval'able source + */ + restoreWindow: function sss_restoreWindow(aBrowser, aState) { + try { + var winData = typeof aState == "string" ? this._safeEval(aState) : aState; + } + catch (ex) { // invalid state object - don't restore anything + debug(ex); + dump(ex); + return; + } + + this.restoreHistoryPrecursor(aBrowser, winData.history); + }, + + /** + * Manage history restoration for a window + * @param aBrowser + * Browser reference + * @param aHistoryData + * History data to be restored + */ + restoreHistoryPrecursor: function sss_restoreHistoryPrecursor(aBrowser, aHistoryData) { + /* + // make sure that all browsers and their histories are available + // - if one's not, resume this check in 100ms (repeat at most 10 times) + for (var t = aIx; t < aTabs.length; t++) { + try { + if (!tabbrowser.getBrowserForTab(aTabs[t]._tab).webNavigation.sessionHistory) { + throw new Error(); + } + } + catch (ex) { // in case browser or history aren't ready yet + if (aCount < 10) { + var restoreHistoryFunc = function(self) { + self.restoreHistoryPrecursor(aWindow, aTabs, aSelectTab, aIx, aCount + 1); + } + aWindow.setTimeout(restoreHistoryFunc, 100, this); + return; + } + } + } + */ + + // helper hash for ensuring unique frame IDs + var aIdMap = { used: {} }; + this.restoreHistory(aBrowser, aHistoryData, aIdMap); + }, + + /** + * Restory history for a window + * @param aBrowser + * Browser reference + * @param aHistoryData + * History data to be restored + * @param aIdMap + * Hash for ensuring unique frame IDs + */ + restoreHistory: function sss_restoreHistory(aBrowser, aHistoryData, aIdMap) { + dump("nsSessionStore::restoreHistory\n") + + aBrowser.QueryInterface(Ci.nsIWebNavigation); + aSHistory = aBrowser.sessionHistory; + aSHistory.QueryInterface(Ci.nsISHistoryInternal); + + if (aSHistory.count > 0) { + aSHistory.PurgeHistory(aSHistory.count); + } + + if (!aHistoryData) { + aHistoryData = []; + } + + for (var i = 0; i < aHistoryData.length; i++) { + aSHistory.addEntry(this._deserializeHistoryEntry(aHistoryData[i], aIdMap), true); + } + + /* + // make sure to reset the capabilities and attributes, in case this tab gets reused + var disallow = (aHistoryData.disallow)?aHistoryData.disallow.split(","):[]; + CAPABILITIES.forEach(function(aCapability) { + browser.docShell["allow" + aCapability] = disallow.indexOf(aCapability) == -1; + }); + Array.filter(tab.attributes, function(aAttr) { + return (_this.xulAttributes.indexOf(aAttr.name) > -1); + }).forEach(tab.removeAttribute, tab); + if (aHistoryData.xultab) { + aHistoryData.xultab.split(" ").forEach(function(aAttr) { + if (/^([^\s=]+)=(.*)/.test(aAttr)) { + tab.setAttribute(RegExp.$1, decodeURI(RegExp.$2)); + } + }); + } + */ + try { + aBrowser.gotoIndex(aHistoryData.length - 1); + } + catch (ex) { dump(ex + "\n"); } // ignore an invalid aHistoryData.index + }, + + /** + * expands serialized history data into a session-history-entry instance + * @param aEntry + * Object containing serialized history data for a URL + * @param aIdMap + * Hash for ensuring unique frame IDs + * @returns nsISHEntry + */ + _deserializeHistoryEntry: function sss_deserializeHistoryEntry(aEntry, aIdMap) { + var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. + createInstance(Ci.nsISHEntry); + + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + shEntry.setURI(ioService.newURI(aEntry.url, null, null)); + shEntry.setTitle(aEntry.title || aEntry.url); + shEntry.setIsSubFrame(aEntry.subframe || false); + shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; + + if (aEntry.cacheKey) { + var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"]. + createInstance(Ci.nsISupportsPRUint32); + cacheKey.data = aEntry.cacheKey; + shEntry.cacheKey = cacheKey; + } + if (aEntry.ID) { + // get a new unique ID for this frame (since the one from the last + // start might already be in use) + var id = aIdMap[aEntry.ID] || 0; + if (!id) { + for (id = Date.now(); aIdMap.used[id]; id++); + aIdMap[aEntry.ID] = id; + aIdMap.used[id] = true; + } + shEntry.ID = id; + } + + var scrollPos = (aEntry.scroll || "0,0").split(","); + scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; + shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); + + if (aEntry.postdata) { + var stream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + stream.setData(aEntry.postdata, -1); + shEntry.postData = stream; + } + + if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { + for (var i = 0; i < aEntry.children.length; i++) { + shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap), i); + } + } + + return shEntry; + }, + +/* ........ Auxiliary Functions .............. */ + + /** + * don't save sensitive data if the user doesn't want to + * (distinguishes between encrypted and non-encrypted sites) + * @param aIsHTTPS + * Bool is encrypted + * @returns bool + */ + _checkPrivacyLevel: function sss_checkPrivacyLevel(aIsHTTPS) { + return this._prefBranch.getIntPref("sessionstore.privacy_level") < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL); + }, + + /** + * safe eval'ing + */ + _safeEval: function sss_safeEval(aStr) { + return Components.utils.evalInSandbox(aStr, EVAL_SANDBOX); + }, + + /** + * Converts a JavaScript object into a JSON string + * (see http://www.json.org/ for the full grammar). + * + * The inverse operation consists of eval("(" + JSON_string + ")"); + * and should be provably safe. + * + * @param aJSObject is the object to be converted + * @return the object's JSON representation + */ + _toJSONString: function sss_toJSONString(aJSObject) { + // these characters have a special escape notation + const charMap = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", + "\r": "\\r", '"': '\\"', "\\": "\\\\" }; + // we use a single string builder for efficiency reasons + var parts = []; + + // this recursive function walks through all objects and appends their + // JSON representation to the string builder + function jsonIfy(aObj) { + if (typeof aObj == "boolean") { + parts.push(aObj ? "true" : "false"); + } + else if (typeof aObj == "number" && isFinite(aObj)) { + // there is no representation for infinite numbers or for NaN! + parts.push(aObj.toString()); + } + else if (typeof aObj == "string") { + aObj = aObj.replace(/[\\"\x00-\x1F\u0080-\uFFFF]/g, function($0) { + // use the special escape notation if one exists, otherwise + // produce a general unicode escape sequence + return charMap[$0] || + "\\u" + ("0000" + $0.charCodeAt(0).toString(16)).slice(-4); + }); + parts.push('"' + aObj + '"') + } + else if (aObj == null) { + parts.push("null"); + } + else if (aObj instanceof Array || aObj instanceof EVAL_SANDBOX.Array) { + parts.push("["); + for (var i = 0; i < aObj.length; i++) { + jsonIfy(aObj[i]); + parts.push(","); + } + if (parts[parts.length - 1] == ",") + parts.pop(); // drop the trailing colon + parts.push("]"); + } + else if (typeof aObj == "object") { + parts.push("{"); + for (var key in aObj) { + jsonIfy(key.toString()); + parts.push(":"); + jsonIfy(aObj[key]); + parts.push(","); + } + if (parts[parts.length - 1] == ",") + parts.pop(); // drop the trailing colon + parts.push("}"); + } + else { + throw new Error("No JSON representation for this object!"); + } + } + jsonIfy(aJSObject); + + var newJSONString = parts.join(" "); + // sanity check - so that API consumers can just eval this string + if (/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( + newJSONString.replace(/"(\\.|[^"\\])*"/g, "") + )) + throw new Error("JSON conversion failed unexpectedly!"); + + return newJSONString; + }, + +/* ........ QueryInterface .............. */ + + QueryInterface: function(aIID) { + if (!aIID.equals(Ci.nsISupports) && + !aIID.equals(Ci.nsIObserver) && + !aIID.equals(Ci.nsISupportsWeakReference) && + !aIID.equals(Ci.nsIDOMEventListener) && + !aIID.equals(Ci.nsISessionStore)) { + Components.returnCode = Cr.NS_ERROR_NO_INTERFACE; + return null; + } + + return this; + } +}; + +/* :::::::: Service Registration & Initialization ::::::::::::::: */ + +/* ........ nsIModule .............. */ + +const SessionStoreModule = { + + getClassObject: function(aCompMgr, aCID, aIID) { + if (aCID.equals(CID)) { + return SessionStoreFactory; + } + + Components.returnCode = Cr.NS_ERROR_NOT_REGISTERED; + return null; + }, + + registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) { + aCompMgr.QueryInterface(Ci.nsIComponentRegistrar); + aCompMgr.registerFactoryLocation(CID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType); + }, + + unregisterSelf: function(aCompMgr, aLocation, aType) { + aCompMgr.QueryInterface(Ci.nsIComponentRegistrar); + aCompMgr.unregisterFactoryLocation(CID, aLocation); + }, + + canUnload: function(aCompMgr) { + return true; + } +} + +/* ........ nsIFactory .............. */ + +const SessionStoreFactory = { + + createInstance: function(aOuter, aIID) { + if (aOuter != null) { + Components.returnCode = Cr.NS_ERROR_NO_AGGREGATION; + return null; + } + + return (new SessionStoreService()).QueryInterface(aIID); + }, + + lockFactory: function(aLock) { }, + + QueryInterface: function(aIID) { + if (!aIID.equals(Ci.nsISupports) && !aIID.equals(Ci.nsIModule) && + !aIID.equals(Ci.nsIFactory) && !aIID.equals(Ci.nsISessionStore)) { + Components.returnCode = Cr.NS_ERROR_NO_INTERFACE; + return null; + } + + return this; + } +}; + +function NSGetModule(aComMgr, aFileSpec) { + dump("nsSessionStore: NSGetModule\n") + return SessionStoreModule; +} |