43
43
* This service keeps track of a user's session, storing the various bits
44
44
* required to return the browser to it's current state. The relevant data is
45
* stored in memory, and is periodically saved to disk in an ini file in the
46
* profile directory. The service is started at first window load, in delayedStartup, and will restore
47
* the session from the data received from the nsSessionStartup service.
45
* stored in memory, and is periodically saved to disk in a file in the
46
* profile directory. The service is started at first window load, in
47
* delayedStartup, and will restore the session from the data received from
48
* the nsSessionStartup service.
50
51
/* :::::::: Constants and Helpers ::::::::::::::: */
166
167
* Initialize the component
168
169
init: function sss_init(aWindow) {
169
if (!aWindow || this._loadState == STATE_RUNNING)
170
if (!aWindow || this._loadState == STATE_RUNNING) {
171
// make sure that all browser windows which try to initialize
172
// SessionStore are really tracked by it
173
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
174
this.onLoad(aWindow);
172
178
this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
173
179
getService(Ci.nsIPrefService).getBranch("browser.");
225
231
delete this._initialState.windows[0].hidden;
227
catch (ex) { debug("The session file is invalid: " + ex); } // invalid .INI file - nothing can be restored
233
catch (ex) { debug("The session file is invalid: " + ex); }
230
236
// if last session crashed, backup the session
231
237
if (this._lastSessionCrashed) {
233
this._writeFile(this._getSessionFile(true), iniString);
239
this._writeFile(this._sessionFileBackup, iniString);
235
241
catch (ex) { } // nothing else we can do here
385
391
* Window reference
387
393
onLoad: function sss_onLoad(aWindow) {
394
// return if window has already been initialized
395
if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi])
388
398
var _this = this;
390
// ignore non-browser windows
391
if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser")
400
// ignore non-browser windows and windows opened while shutting down
401
if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" ||
402
this._loadState == STATE_QUITTING)
394
405
// assign it a unique identifier (timestamp)
604
616
/* ........ nsISessionStore API .............. */
606
// This API is terribly rough. TODO:
607
// * figure out what functionality we really need
608
// * figure out how to name the functions
610
// * get/set the (opaque) state of all windows
613
return this._getCurrentState();
618
getBrowserState: function sss_getBrowserState() {
619
return this._toJSONString(this._getCurrentState());
616
set opaqueState(aState) {
622
setBrowserState: function sss_setBrowserState(aState) {
617
623
var window = this._getMostRecentBrowserWindow();
619
this._openWindowWithState(aState);
625
this._openWindowWithState("(" + aState + ")");
623
629
// close all other browser windows
624
630
this._forEachBrowserWindow(function(aWindow) {
625
631
if (aWindow != window) {
630
636
// restore to the given state
631
this.restoreWindow(window, aState, true);
634
// * get/set the opaque state of a closed window (for persisting it over sessions)
636
getOpaqueWindowState: function sss_getOpaqueWindowState(aWindow) {
637
return this._getWindowState(aWindow);
640
setOpaqueWindowState: function sss_setOpaqueWindowState(aWindow, aState) {
641
this.restoreWindow(aWindow, aState, true);
644
// * allow to reopen closed tabs
645
// * get/set the opaque state of a closed tab (for persisting it over sessions)
637
this.restoreWindow(window, "(" + aState + ")", true);
640
getWindowState: function sss_getWindowState(aWindow) {
641
return this._toJSONString(this._getWindowState(aWindow));
644
setWindowState: function sss_setWindowState(aWindow, aState, aOverwrite) {
645
this.restoreWindow(aWindow, "(" + aState + ")", aOverwrite);
647
648
getClosedTabCount: function sss_getClosedTabCount(aWindow) {
648
649
return this._windows[aWindow.__SSi]._closedTabs.length;
654
655
return aIx in tabs ? tabs[aIx].title : null;
658
* Returns nsIDictionary
660
658
getClosedTabData: function sss_getClosedTabDataAt(aWindow) {
662
var wrappedData = Cc["@mozilla.org/dictionary;1"].createInstance(Ci.nsIDictionary);
663
var closedTabData = this._windows[aWindow.__SSi]._closedTabs;
664
if (closedTabData.length == 0)
668
for (var i = 0; i < closedTabData.length; i++)
669
wrappedData.setValue(i, { wrappedJSObject: closedTabData[i] });
672
} catch(ex) { debug("getClosedTabData: " + ex); }
675
setClosedTabData: function sss_setClosedTabDataAt(aWindow, aData) {
676
this._windows[aWindow.__SSi]._closedTabs = aData;
659
return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs);
679
662
undoCloseTab: function sss_undoCloseTab(aWindow, aIndex) {
1165
1148
* @param aWindow
1166
1149
* Window reference
1167
1150
* @param aState
1168
* Ini formatted string, or object
1151
* JS object or its eval'able source
1169
1152
* @param aOverwriteTabs
1170
1153
* bool overwrite existing tabs w/ new ones
1172
1155
restoreWindow: function sss_restoreWindow(aWindow, aState, aOverwriteTabs) {
1156
// initialize window if necessary
1157
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
1158
this.onLoad(aWindow);
1174
1161
var root = typeof aState == "string" ? this._safeEval(aState) : aState;
1175
1162
if (!root.windows[0]) {
1176
1163
return; // nothing to restore
1179
catch (ex) { // invalid .INI file - don't restore anything
1166
catch (ex) { // invalid state object - don't restore anything
1184
1172
// open new windows for all further window entries of a multi-window session
1173
// (unless they don't contain any tab data)
1185
1174
for (var w = 1; w < root.windows.length; w++) {
1186
this._openWindowWithState({ windows: [root.windows[w]], opener: aWindow });
1175
winData = root.windows[w];
1176
if (winData && winData.tabs && winData.tabs[0]) {
1177
this._openWindowWithState({ windows: [winData], opener: aWindow });
1189
var winData = root.windows[0];
1181
winData = root.windows[0];
1190
1182
if (!winData.tabs) {
1191
1183
winData.tabs = [];
1465
1461
var content = XPCNativeWrapper(aEvent.originalTarget).defaultView;
1466
1462
if (this.currentURI.spec == "about:config") {
1467
//XXXzeniko why ever this doesn't work with an XPCNativeWrapper...
1468
content = aEvent.originalTarget.defaultView;
1463
// unwrap the document for about:config because otherwise the properties
1464
// of the XBL bindings - as the textbox - aren't accessible (see bug 350718)
1465
content = content.wrappedJSObject;
1470
1467
restoreTextDataAndScrolling(content, this.__SS_restore_data, "");
1597
var downloads = rdfContainer.GetElements();
1599
1594
// iterate through all downloads currently available in the RDF store
1600
1595
// and restart the ones which were in progress before the crash
1596
var downloads = rdfContainer.GetElements();
1601
1597
while (downloads.hasMoreElements()) {
1602
1598
var download = downloads.getNext().QueryInterface(Ci.nsIRDFResource);
1604
var node = datasource.GetTarget(rdfService.GetResource(download.Value), rdfService.GetResource("http://home.netscape.com/NC-rdf#DownloadState"), true);
1600
// restart only if the download's in progress
1601
var node = datasource.GetTarget(download, rdfService.GetResource("http://home.netscape.com/NC-rdf#DownloadState"), true);
1606
1603
node.QueryInterface(Ci.nsIRDFInt);
1608
1605
if (!node || node.Value != Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING) {
1612
node = datasource.GetTarget(rdfService.GetResource(download.Value), rdfService.GetResource("http://home.netscape.com/NC-rdf#URL"), true);
1613
node.QueryInterface(Ci.nsIRDFResource);
1615
var url = node.Value;
1617
node = datasource.GetTarget(rdfService.GetResource(download.Value), rdfService.GetResource("http://home.netscape.com/NC-rdf#File"), true);
1618
node.QueryInterface(Ci.nsIRDFResource);
1609
// URL being downloaded
1610
node = datasource.GetTarget(download, rdfService.GetResource("http://home.netscape.com/NC-rdf#URL"), true);
1611
var url = node.QueryInterface(Ci.nsIRDFResource).Value;
1613
// location where download's being saved
1614
node = datasource.GetTarget(download, rdfService.GetResource("http://home.netscape.com/NC-rdf#File"), true);
1616
// nsIRDFResource.Value is a string that's a URI; the downloads.rdf from
1617
// which this was created will have a string in one of the following two
1618
// forms, depending on platform:
1620
// /home/lumpy/dogtreat.txt
1621
// C:\lumpy\dogtreat.txt
1623
// During RDF loading, the string *appears* to be converted to a URL if
1624
// necessary. Strings in the first form are not URLs and are converted to
1625
// file: URLs; strings in the latter form seem to be treated as if they
1626
// already are URLs and thus are not modified. Consequently, on platforms
1627
// where paths aren't URLs, we need to extract the path from the file:
1630
// See also bug 335725, bug 239948, and bug 349971.
1631
var savedTo = node.QueryInterface(Ci.nsIRDFResource).Value;
1633
var savedToURI = Cc["@mozilla.org/network/io-service;1"].
1634
getService(Ci.nsIIOService).
1635
newURI(savedTo, null, null);
1636
if (savedToURI.schemeIs("file"))
1637
savedTo = savedToURI.path;
1639
catch (e) { /* not a URI, assume it was a string of form #1 */ }
1620
1641
var linkChecker = Cc["@mozilla.org/network/urichecker;1"].
1621
1642
createInstance(Ci.nsIURIChecker);
1623
1643
linkChecker.init(ioService.newURI(url, null, null));
1624
1644
linkChecker.loadFlags = Ci.nsIRequest.LOAD_BACKGROUND;
1625
linkChecker.asyncCheck(new AutoDownloader(url, node.Value, aWindow), null);
1645
linkChecker.asyncCheck(new AutoDownloader(url, savedTo, aWindow), null);
1674
1694
* delete session datafile and backup
1676
1696
_clearDisk: function sss_clearDisk() {
1677
var file = this._getSessionFile();
1679
if (file.exists()) {
1683
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
1686
if (!this._lastSessionCrashed)
1690
this._getSessionFile(true).remove(false);
1692
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
1696
* get session datafile (or its backup)
1699
_getSessionFile: function sss_getSessionFile(aBackup) {
1700
return aBackup ? this._sessionFileBackup : this._sessionFile;
1697
if (this._sessionFile.exists()) {
1699
this._sessionFile.remove(false);
1701
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
1703
if (this._sessionFileBackup.exists()) {
1705
this._sessionFileBackup.remove(false);
1707
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
1703
1711
/* ........ Auxiliary Functions .............. */
1862
1870
* safe eval'ing
1864
1872
_safeEval: function sss_safeEval(aStr) {
1865
var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
1866
var uri = ioService.newFileURI(this._sessionFile, null, null);
1867
var s = new Components.utils.Sandbox(uri.spec);
1873
var s = new Components.utils.Sandbox("about:blank");
1868
1874
return Components.utils.evalInSandbox(aStr, s);
1878
* Converts a JavaScript object into a JSON string
1879
* (see http://www.json.org/ for the full grammar).
1881
* The inverse operation consists of eval("(" + JSON_string + ")");
1882
* and should be provably safe.
1884
* @param aJSObject is the object to be converted
1885
* @return the object's JSON representation
1887
_toJSONString: function sss_toJSONString(aJSObject) {
1888
// these characters have a special escape notation
1889
const charMap = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f",
1890
"\r": "\\r", '"': '\\"', "\\": "\\\\" };
1891
// we use a single string builder for efficiency reasons
1894
// this recursive function walks through all objects and appends their
1895
// JSON representation to the string builder
1896
function jsonIfy(aObj) {
1897
if (typeof aObj == "boolean") {
1898
parts.push(aObj ? "true" : "false");
1900
else if (typeof aObj == "number" && isFinite(aObj)) {
1901
// there is no representation for infinite numbers or for NaN!
1902
parts.push(aObj.toString());
1904
else if (typeof aObj == "string") {
1905
aObj = aObj.replace(/[\\"\x00-\x1F\u0080-\uFFFF]/g, function($0) {
1906
// use the special escape notation if one exists, otherwise
1907
// produce a general unicode escape sequence
1908
return charMap[$0] ||
1909
"\\u" + ("0000" + $0.charCodeAt(0).toString(16)).slice(-4);
1911
parts.push('"' + aObj + '"')
1913
else if (aObj == null) {
1916
else if (aObj instanceof Array) {
1918
for (var i = 0; i < aObj.length; i++) {
1922
if (parts[parts.length - 1] == ",")
1923
parts.pop(); // drop the trailing colon
1926
else if (typeof aObj == "object") {
1928
for (var key in aObj) {
1929
jsonIfy(key.toString());
1934
if (parts[parts.length - 1] == ",")
1935
parts.pop(); // drop the trailing colon
1939
throw new Error("No JSON representation for this object!");
1944
var newJSONString = parts.join(" ");
1945
// sanity check - so that API consumers can just eval this string
1946
if (/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
1947
newJSONString.replace(/"(\\.|[^"\\])*"/g, "")
1949
throw new Error("JSON conversion failed unexpectedly!");
1951
return newJSONString;
1871
1954
/* ........ Storage API .............. */