mobile/android/components/SessionStore.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 const Cc = Components.classes;
     6 const Ci = Components.interfaces;
     7 const Cu = Components.utils;
     8 const Cr = Components.results;
    10 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    11 Cu.import("resource://gre/modules/Services.jsm");
    13 #ifdef MOZ_CRASHREPORTER
    14 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
    15   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
    16 #endif
    18 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
    19 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
    20 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm");
    22 function dump(a) {
    23   Services.console.logStringMessage(a);
    24 }
    26 // -----------------------------------------------------------------------
    27 // Session Store
    28 // -----------------------------------------------------------------------
    30 const STATE_STOPPED = 0;
    31 const STATE_RUNNING = 1;
    33 function SessionStore() { }
    35 SessionStore.prototype = {
    36   classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"),
    38   QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
    39                                          Ci.nsIDOMEventListener,
    40                                          Ci.nsIObserver,
    41                                          Ci.nsISupportsWeakReference]),
    43   _windows: {},
    44   _lastSaveTime: 0,
    45   _interval: 10000,
    46   _maxTabsUndo: 1,
    47   _pendingWrite: 0,
    49   init: function ss_init() {
    50     // Get file references
    51     this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
    52     this._sessionFileBackup = this._sessionFile.clone();
    53     this._sessionFile.append("sessionstore.js");
    54     this._sessionFileBackup.append("sessionstore.bak");
    56     this._loadState = STATE_STOPPED;
    58     this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
    59     this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
    60   },
    62   _clearDisk: function ss_clearDisk() {
    63     OS.File.remove(this._sessionFile.path);
    64     OS.File.remove(this._sessionFileBackup.path);
    65   },
    67   observe: function ss_observe(aSubject, aTopic, aData) {
    68     let self = this;
    69     let observerService = Services.obs;
    70     switch (aTopic) {
    71       case "app-startup":
    72         observerService.addObserver(this, "final-ui-startup", true);
    73         observerService.addObserver(this, "domwindowopened", true);
    74         observerService.addObserver(this, "domwindowclosed", true);
    75         observerService.addObserver(this, "browser:purge-session-history", true);
    76         observerService.addObserver(this, "Session:Restore", true);
    77         observerService.addObserver(this, "application-background", true);
    78         break;
    79       case "final-ui-startup":
    80         observerService.removeObserver(this, "final-ui-startup");
    81         this.init();
    82         break;
    83       case "domwindowopened": {
    84         let window = aSubject;
    85         window.addEventListener("load", function() {
    86           self.onWindowOpen(window);
    87           window.removeEventListener("load", arguments.callee, false);
    88         }, false);
    89         break;
    90       }
    91       case "domwindowclosed": // catch closed windows
    92         this.onWindowClose(aSubject);
    93         break;
    94       case "browser:purge-session-history": // catch sanitization 
    95         this._clearDisk();
    97         // Clear all data about closed tabs
    98         for (let [ssid, win] in Iterator(this._windows))
    99           win.closedTabs = [];
   101         if (this._loadState == STATE_RUNNING) {
   102           // Save the purged state immediately
   103           this.saveState();
   104         }
   106         Services.obs.notifyObservers(null, "sessionstore-state-purge-complete", "");
   107         break;
   108       case "timer-callback":
   109         // Timer call back for delayed saving
   110         this._saveTimer = null;
   111         if (this._pendingWrite) {
   112           this.saveState();
   113         }
   114         break;
   115       case "Session:Restore": {
   116         Services.obs.removeObserver(this, "Session:Restore");
   117         if (aData) {
   118           // Be ready to handle any restore failures by making sure we have a valid tab opened
   119           let window = Services.wm.getMostRecentWindow("navigator:browser");
   120           let restoreCleanup = {
   121             observe: function (aSubject, aTopic, aData) {
   122               Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored");
   124               if (window.BrowserApp.tabs.length == 0) {
   125                 window.BrowserApp.addTab("about:home", {
   126                   selected: true
   127                 });
   128               }
   130               // Let Java know we're done restoring tabs so tabs added after this can be animated
   131               sendMessageToJava({
   132                 type: "Session:RestoreEnd"
   133               });
   134             }.bind(this)
   135           };
   136           Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false);
   138           // Do a restore, triggered by Java
   139           let data = JSON.parse(aData);
   140           this.restoreLastSession(data.sessionString);
   141         } else {
   142           // Not doing a restore; just send restore message
   143           Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
   144         }
   145         break;
   146       }
   147       case "application-background":
   148         // We receive this notification when Android's onPause callback is
   149         // executed. After onPause, the application may be terminated at any
   150         // point without notice; therefore, we must synchronously write out any
   151         // pending save state to ensure that this data does not get lost.
   152         this.flushPendingState();
   153         break;
   154     }
   155   },
   157   handleEvent: function ss_handleEvent(aEvent) {
   158     let window = aEvent.currentTarget.ownerDocument.defaultView;
   159     switch (aEvent.type) {
   160       case "TabOpen": {
   161         let browser = aEvent.target;
   162         this.onTabAdd(window, browser);
   163         break;
   164       }
   165       case "TabClose": {
   166         let browser = aEvent.target;
   167         this.onTabClose(window, browser);
   168         this.onTabRemove(window, browser);
   169         break;
   170       }
   171       case "TabSelect": {
   172         let browser = aEvent.target;
   173         this.onTabSelect(window, browser);
   174         break;
   175       }
   176       case "DOMTitleChanged": {
   177         let browser = aEvent.currentTarget;
   179         // Handle only top-level DOMTitleChanged event
   180         if (browser.contentDocument !== aEvent.originalTarget)
   181           return;
   183         // Use DOMTitleChanged to detect page loads over alternatives.
   184         // onLocationChange happens too early, so we don't have the page title
   185         // yet; pageshow happens too late, so we could lose session data if the
   186         // browser were killed.
   187         this.onTabLoad(window, browser);
   188         break;
   189       }
   190     }
   191   },
   193   onWindowOpen: function ss_onWindowOpen(aWindow) {
   194     // Return if window has already been initialized
   195     if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID])
   196       return;
   198     // Ignore non-browser windows and windows opened while shutting down
   199     if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser")
   200       return;
   202     // Assign it a unique identifier (timestamp) and create its data object
   203     aWindow.__SSID = "window" + Date.now();
   204     this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] };
   206     // Perform additional initialization when the first window is loading
   207     if (this._loadState == STATE_STOPPED) {
   208       this._loadState = STATE_RUNNING;
   209       this._lastSaveTime = Date.now();
   210     }
   212     // Add tab change listeners to all already existing tabs
   213     let tabs = aWindow.BrowserApp.tabs;
   214     for (let i = 0; i < tabs.length; i++)
   215       this.onTabAdd(aWindow, tabs[i].browser, true);
   217     // Notification of tab add/remove/selection
   218     let browsers = aWindow.document.getElementById("browsers");
   219     browsers.addEventListener("TabOpen", this, true);
   220     browsers.addEventListener("TabClose", this, true);
   221     browsers.addEventListener("TabSelect", this, true);
   222   },
   224   onWindowClose: function ss_onWindowClose(aWindow) {
   225     // Ignore windows not tracked by SessionStore
   226     if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
   227       return;
   229     let browsers = aWindow.document.getElementById("browsers");
   230     browsers.removeEventListener("TabOpen", this, true);
   231     browsers.removeEventListener("TabClose", this, true);
   232     browsers.removeEventListener("TabSelect", this, true);
   234     if (this._loadState == STATE_RUNNING) {
   235       // Update all window data for a last time
   236       this._collectWindowData(aWindow);
   238       // Clear this window from the list
   239       delete this._windows[aWindow.__SSID];
   241       // Save the state without this window to disk
   242       this.saveStateDelayed();
   243     }
   245     let tabs = aWindow.BrowserApp.tabs;
   246     for (let i = 0; i < tabs.length; i++)
   247       this.onTabRemove(aWindow, tabs[i].browser, true);
   249     delete aWindow.__SSID;
   250   },
   252   onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
   253     aBrowser.addEventListener("DOMTitleChanged", this, true);
   254     if (!aNoNotification)
   255       this.saveStateDelayed();
   256     this._updateCrashReportURL(aWindow);
   257   },
   259   onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
   260     aBrowser.removeEventListener("DOMTitleChanged", this, true);
   262     // If this browser is being restored, skip any session save activity
   263     if (aBrowser.__SS_restore)
   264       return;
   266     delete aBrowser.__SS_data;
   268     if (!aNoNotification)
   269       this.saveStateDelayed();
   270   },
   272   onTabClose: function ss_onTabClose(aWindow, aBrowser) {
   273     if (this._maxTabsUndo == 0)
   274       return;
   276     if (aWindow.BrowserApp.tabs.length > 0) {
   277       // Bundle this browser's data and extra data and save in the closedTabs
   278       // window property
   279       let data = aBrowser.__SS_data;
   280       data.extData = aBrowser.__SS_extdata;
   282       this._windows[aWindow.__SSID].closedTabs.unshift(data);
   283       let length = this._windows[aWindow.__SSID].closedTabs.length;
   284       if (length > this._maxTabsUndo)
   285         this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
   286     }
   287   },
   289   onTabLoad: function ss_onTabLoad(aWindow, aBrowser) {
   290     // If this browser is being restored, skip any session save activity
   291     if (aBrowser.__SS_restore)
   292       return;
   294     // Ignore a transient "about:blank"
   295     if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank")
   296       return;
   298     let history = aBrowser.sessionHistory;
   300     // Serialize the tab data
   301     let entries = [];
   302     let index = history.index + 1;
   303     for (let i = 0; i < history.count; i++) {
   304       let historyEntry = history.getEntryAtIndex(i, false);
   305       // Don't try to restore wyciwyg URLs
   306       if (historyEntry.URI.schemeIs("wyciwyg")) {
   307         // Adjust the index to account for skipped history entries
   308         if (i <= history.index)
   309           index--;
   310         continue;
   311       }
   312       let entry = this._serializeHistoryEntry(historyEntry);
   313       entries.push(entry);
   314     }
   315     let data = { entries: entries, index: index };
   317     delete aBrowser.__SS_data;
   318     this._collectTabData(aWindow, aBrowser, data);
   319     this.saveStateDelayed();
   321     this._updateCrashReportURL(aWindow);
   322   },
   324   onTabSelect: function ss_onTabSelect(aWindow, aBrowser) {
   325     if (this._loadState != STATE_RUNNING)
   326       return;
   328     let browsers = aWindow.document.getElementById("browsers");
   329     let index = browsers.selectedIndex;
   330     this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based
   332     // Restore the resurrected browser
   333     if (aBrowser.__SS_restore) {
   334       let data = aBrowser.__SS_data;
   335       if (data.entries.length > 0)
   336         this._restoreHistory(data, aBrowser.sessionHistory);
   338       delete aBrowser.__SS_restore;
   339       aBrowser.removeAttribute("pending");
   340     }
   342     this.saveStateDelayed();
   343     this._updateCrashReportURL(aWindow);
   344   },
   346   saveStateDelayed: function ss_saveStateDelayed() {
   347     if (!this._saveTimer) {
   348       // Interval until the next disk operation is allowed
   349       let minimalDelay = this._lastSaveTime + this._interval - Date.now();
   351       // If we have to wait, set a timer, otherwise saveState directly
   352       let delay = Math.max(minimalDelay, 2000);
   353       if (delay > 0) {
   354         this._pendingWrite++;
   355         this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   356         this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
   357       } else {
   358         this.saveState();
   359       }
   360     }
   361   },
   363   saveState: function ss_saveState() {
   364     this._pendingWrite++;
   365     this._saveState(true);
   366   },
   368   // Immediately and synchronously writes any pending state to disk.
   369   flushPendingState: function ss_flushPendingState() {
   370     if (this._pendingWrite) {
   371       this._saveState(false);
   372     }
   373   },
   375   _saveState: function ss_saveState(aAsync) {
   376     // Kill any queued timer and save immediately
   377     if (this._saveTimer) {
   378       this._saveTimer.cancel();
   379       this._saveTimer = null;
   380     }
   382     let data = this._getCurrentState();
   383     let normalData = { windows: [] };
   384     let privateData = { windows: [] };
   386     for (let winIndex = 0; winIndex < data.windows.length; ++winIndex) {
   387       let win = data.windows[winIndex];
   388       let normalWin = {};
   389       for (let prop in win) {
   390         normalWin[prop] = data[prop];
   391       }
   392       normalWin.tabs = [];
   393       normalData.windows.push(normalWin);
   394       privateData.windows.push({ tabs: [] });
   396       // Split the session data into private and non-private data objects.
   397       // Non-private session data will be saved to disk, and private session
   398       // data will be sent to Java for Android to hold it in memory.
   399       for (let i = 0; i < win.tabs.length; ++i) {
   400         let tab = win.tabs[i];
   401         let savedWin = tab.isPrivate ? privateData.windows[winIndex] : normalData.windows[winIndex];
   402         savedWin.tabs.push(tab);
   403         if (win.selected == i + 1) {
   404           savedWin.selected = savedWin.tabs.length;
   405         }
   406       }
   407     }
   409     // Write only non-private data to disk
   410     this._writeFile(this._sessionFile, JSON.stringify(normalData), aAsync);
   412     // If we have private data, send it to Java; otherwise, send null to
   413     // indicate that there is no private data
   414     sendMessageToJava({
   415       type: "PrivateBrowsing:Data",
   416       session: (privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null
   417     });
   419     this._lastSaveTime = Date.now();
   420   },
   422   _getCurrentState: function ss_getCurrentState() {
   423     let self = this;
   424     this._forEachBrowserWindow(function(aWindow) {
   425       self._collectWindowData(aWindow);
   426     });
   428     let data = { windows: [] };
   429     for (let index in this._windows) {
   430       data.windows.push(this._windows[index]);
   431     }
   433     return data;
   434   },
   436   _collectTabData: function ss__collectTabData(aWindow, aBrowser, aHistory) {
   437     // If this browser is being restored, skip any session save activity
   438     if (aBrowser.__SS_restore)
   439       return;
   441     aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
   443     let tabData = {};
   444     tabData.entries = aHistory.entries;
   445     tabData.index = aHistory.index;
   446     tabData.attributes = { image: aBrowser.mIconURL };
   447     tabData.desktopMode = aWindow.BrowserApp.getTabForBrowser(aBrowser).desktopMode;
   448     tabData.isPrivate = aBrowser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing;
   450     aBrowser.__SS_data = tabData;
   451   },
   453   _collectWindowData: function ss__collectWindowData(aWindow) {
   454     // Ignore windows not tracked by SessionStore
   455     if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
   456       return;
   458     let winData = this._windows[aWindow.__SSID];
   459     winData.tabs = [];
   461     let browsers = aWindow.document.getElementById("browsers");
   462     let index = browsers.selectedIndex;
   463     winData.selected = parseInt(index) + 1; // 1-based
   465     let tabs = aWindow.BrowserApp.tabs;
   466     for (let i = 0; i < tabs.length; i++) {
   467       let browser = tabs[i].browser;
   468       if (browser.__SS_data) {
   469         let tabData = browser.__SS_data;
   470         if (browser.__SS_extdata)
   471           tabData.extData = browser.__SS_extdata;
   472         winData.tabs.push(tabData);
   473       }
   474     }
   475   },
   477   _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) {
   478     let windowsEnum = Services.wm.getEnumerator("navigator:browser");
   479     while (windowsEnum.hasMoreElements()) {
   480       let window = windowsEnum.getNext();
   481       if (window.__SSID && !window.closed)
   482         aFunc.call(this, window);
   483     }
   484   },
   486   _writeFile: function ss_writeFile(aFile, aData, aAsync) {
   487     let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
   488     stateString.data = aData;
   489     Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
   491     // Don't touch the file if an observer has deleted all state data
   492     if (!stateString.data)
   493       return;
   495     if (aAsync) {
   496       let array = new TextEncoder().encode(aData);
   497       let pendingWrite = this._pendingWrite;
   498       OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(function onSuccess() {
   499         // Make sure this._pendingWrite is the same value it was before we
   500         // fired off the async write. If the count is different, another write
   501         // is pending, so we shouldn't reset this._pendingWrite yet.
   502         if (pendingWrite === this._pendingWrite)
   503           this._pendingWrite = 0;
   504         Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
   505       }.bind(this));
   506     } else {
   507       this._pendingWrite = 0;
   508       let foStream = Cc["@mozilla.org/network/file-output-stream;1"].
   509                      createInstance(Ci.nsIFileOutputStream);
   510       foStream.init(aFile, 0x02 | 0x08 | 0x20, 0666, 0);
   511       let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
   512                       createInstance(Ci.nsIConverterOutputStream);
   513       converter.init(foStream, "UTF-8", 0, 0);
   514       converter.writeString(aData);
   515       converter.close();
   516     }
   517   },
   519   _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
   520 #ifdef MOZ_CRASHREPORTER
   521     if (!aWindow.BrowserApp.selectedBrowser)
   522       return;
   524     try {
   525       let currentURI = aWindow.BrowserApp.selectedBrowser.currentURI.clone();
   526       // if the current URI contains a username/password, remove it
   527       try {
   528         currentURI.userPass = "";
   529       }
   530       catch (ex) { } // ignore failures on about: URIs
   532       CrashReporter.annotateCrashReport("URL", currentURI.spec);
   533     }
   534     catch (ex) {
   535       // don't make noise when crashreporter is built but not enabled
   536       if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED)
   537         Components.utils.reportError("SessionStore:" + ex);
   538     }
   539 #endif
   540   },
   542   _serializeHistoryEntry: function _serializeHistoryEntry(aEntry) {
   543     let entry = { url: aEntry.URI.spec };
   545     if (aEntry.title && aEntry.title != entry.url)
   546       entry.title = aEntry.title;
   548     if (!(aEntry instanceof Ci.nsISHEntry))
   549       return entry;
   551     let cacheKey = aEntry.cacheKey;
   552     if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0)
   553       entry.cacheKey = cacheKey.data;
   555     entry.ID = aEntry.ID;
   556     entry.docshellID = aEntry.docshellID;
   558     if (aEntry.referrerURI)
   559       entry.referrer = aEntry.referrerURI.spec;
   561     if (aEntry.contentType)
   562       entry.contentType = aEntry.contentType;
   564     let x = {}, y = {};
   565     aEntry.getScrollPosition(x, y);
   566     if (x.value != 0 || y.value != 0)
   567       entry.scroll = x.value + "," + y.value;
   569     if (aEntry.owner) {
   570       try {
   571         let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIObjectOutputStream);
   572         let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
   573         pipe.init(false, false, 0, 0xffffffff, null);
   574         binaryStream.setOutputStream(pipe.outputStream);
   575         binaryStream.writeCompoundObject(aEntry.owner, Ci.nsISupports, true);
   576         binaryStream.close();
   578         // Now we want to read the data from the pipe's input end and encode it.
   579         let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
   580         scriptableStream.setInputStream(pipe.inputStream);
   581         let ownerBytes = scriptableStream.readByteArray(scriptableStream.available());
   582         // We can stop doing base64 encoding once our serialization into JSON
   583         // is guaranteed to handle all chars in strings, including embedded
   584         // nulls.
   585         entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes));
   586       } catch (e) { dump(e); }
   587     }
   589     entry.docIdentifier = aEntry.BFCacheEntry.ID;
   591     if (aEntry.stateData != null) {
   592       entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
   593       entry.structuredCloneVersion = aEntry.stateData.formatVersion;
   594     }
   596     if (!(aEntry instanceof Ci.nsISHContainer))
   597       return entry;
   599     if (aEntry.childCount > 0) {
   600       let children = [];
   601       for (let i = 0; i < aEntry.childCount; i++) {
   602         let child = aEntry.GetChildAt(i);
   604         if (child) {
   605           // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
   606           if (child.URI.schemeIs("wyciwyg")) {
   607             children = [];
   608             break;
   609           }
   610           children.push(this._serializeHistoryEntry(child));
   611         }
   613         if (children.length)
   614           entry.children = children;
   615       }
   616     }
   618     return entry;
   619   },
   621   _deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) {
   622     let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry);
   624     shEntry.setURI(Services.io.newURI(aEntry.url, null, null));
   625     shEntry.setTitle(aEntry.title || aEntry.url);
   626     if (aEntry.subframe)
   627       shEntry.setIsSubFrame(aEntry.subframe || false);
   628     shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
   629     if (aEntry.contentType)
   630       shEntry.contentType = aEntry.contentType;
   631     if (aEntry.referrer)
   632       shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null);
   634     if (aEntry.cacheKey) {
   635       let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32);
   636       cacheKey.data = aEntry.cacheKey;
   637       shEntry.cacheKey = cacheKey;
   638     }
   640     if (aEntry.ID) {
   641       // get a new unique ID for this frame (since the one from the last
   642       // start might already be in use)
   643       let id = aIdMap[aEntry.ID] || 0;
   644       if (!id) {
   645         for (id = Date.now(); id in aIdMap.used; id++);
   646         aIdMap[aEntry.ID] = id;
   647         aIdMap.used[id] = true;
   648       }
   649       shEntry.ID = id;
   650     }
   652     if (aEntry.docshellID)
   653       shEntry.docshellID = aEntry.docshellID;
   655     if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
   656       shEntry.stateData =
   657         Cc["@mozilla.org/docshell/structured-clone-container;1"].
   658         createInstance(Ci.nsIStructuredCloneContainer);
   660       shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion);
   661     }
   663     if (aEntry.scroll) {
   664       let scrollPos = aEntry.scroll.split(",");
   665       scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
   666       shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
   667     }
   669     let childDocIdents = {};
   670     if (aEntry.docIdentifier) {
   671       // If we have a serialized document identifier, try to find an SHEntry
   672       // which matches that doc identifier and adopt that SHEntry's
   673       // BFCacheEntry.  If we don't find a match, insert shEntry as the match
   674       // for the document identifier.
   675       let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
   676       if (!matchingEntry) {
   677         matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
   678         aDocIdentMap[aEntry.docIdentifier] = matchingEntry;
   679       } else {
   680         shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
   681         childDocIdents = matchingEntry.childDocIdents;
   682       }
   683     }
   685     if (aEntry.owner_b64) {
   686       let ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
   687       let binaryData = atob(aEntry.owner_b64);
   688       ownerInput.setData(binaryData, binaryData.length);
   689       let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIObjectInputStream);
   690       binaryStream.setInputStream(ownerInput);
   691       try { // Catch possible deserialization exceptions
   692         shEntry.owner = binaryStream.readObject(true);
   693       } catch (ex) { dump(ex); }
   694     }
   696     if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
   697       for (let i = 0; i < aEntry.children.length; i++) {
   698         if (!aEntry.children[i].url)
   699           continue;
   701         // We're getting sessionrestore.js files with a cycle in the
   702         // doc-identifier graph, likely due to bug 698656.  (That is, we have
   703         // an entry where doc identifier A is an ancestor of doc identifier B,
   704         // and another entry where doc identifier B is an ancestor of A.)
   705         //
   706         // If we were to respect these doc identifiers, we'd create a cycle in
   707         // the SHEntries themselves, which causes the docshell to loop forever
   708         // when it looks for the root SHEntry.
   709         //
   710         // So as a hack to fix this, we restrict the scope of a doc identifier
   711         // to be a node's siblings and cousins, and pass childDocIdents, not
   712         // aDocIdents, to _deserializeHistoryEntry.  That is, we say that two
   713         // SHEntries with the same doc identifier have the same document iff
   714         // they have the same parent or their parents have the same document.
   716         shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i);
   717       }
   718     }
   720     return shEntry;
   721   },
   723   _restoreHistory: function _restoreHistory(aTabData, aHistory) {
   724     if (aHistory.count > 0)
   725       aHistory.PurgeHistory(aHistory.count);
   726     aHistory.QueryInterface(Ci.nsISHistoryInternal);
   728     // helper hashes for ensuring unique frame IDs and unique document
   729     // identifiers.
   730     let idMap = { used: {} };
   731     let docIdentMap = {};
   733     for (let i = 0; i < aTabData.entries.length; i++) {
   734       if (!aTabData.entries[i].url)
   735         continue;
   736       aHistory.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true);
   737     }
   739     // We need to force set the active history item and cause it to reload since
   740     // we stop the load above
   741     let activeIndex = (aTabData.index || aTabData.entries.length) - 1;
   742     aHistory.getEntryAtIndex(activeIndex, true);
   743     aHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
   744   },
   746   getBrowserState: function ss_getBrowserState() {
   747     return this._getCurrentState();
   748   },
   750   _restoreWindow: function ss_restoreWindow(aData) {
   751     let state;
   752     try {
   753       state = JSON.parse(aData);
   754     } catch (e) {
   755       Cu.reportError("SessionStore: invalid session JSON");
   756       return false;
   757     }
   759     // To do a restore, we must have at least one window with one tab
   760     if (!state || state.windows.length == 0 || !state.windows[0].tabs || state.windows[0].tabs.length == 0) {
   761       Cu.reportError("SessionStore: no tabs to restore");
   762       return false;
   763     }
   765     let window = Services.wm.getMostRecentWindow("navigator:browser");
   767     let tabs = state.windows[0].tabs;
   768     let selected = state.windows[0].selected;
   769     if (selected == null || selected > tabs.length) // Clamp the selected index if it's bogus
   770       selected = 1;
   772     for (let i = 0; i < tabs.length; i++) {
   773       let tabData = tabs[i];
   774       let entry = tabData.entries[tabData.index - 1];
   776       // Use stubbed tab if we've already created it; otherwise, make a new tab
   777       let tab;
   778       if (tabData.tabId == null) {
   779         let params = {
   780           selected: (selected == i+1),
   781           delayLoad: true,
   782           title: entry.title,
   783           desktopMode: (tabData.desktopMode == true),
   784           isPrivate: (tabData.isPrivate == true)
   785         };
   786         tab = window.BrowserApp.addTab(entry.url, params);
   787       } else {
   788         tab = window.BrowserApp.getTabForId(tabData.tabId);
   789         delete tabData.tabId;
   791         // Don't restore tab if user has closed it
   792         if (tab == null) {
   793           continue;
   794         }
   795       }
   797       if (window.BrowserApp.selectedTab == tab) {
   798         this._restoreHistory(tabData, tab.browser.sessionHistory);
   799         delete tab.browser.__SS_restore;
   800         tab.browser.removeAttribute("pending");
   801       } else {
   802         // Make sure the browser has its session data for the delay reload
   803         tab.browser.__SS_data = tabData;
   804         tab.browser.__SS_restore = true;
   805         tab.browser.setAttribute("pending", "true");
   806       }
   808       tab.browser.__SS_extdata = tabData.extData;
   809     }
   811     return true;
   812   },
   814   getClosedTabCount: function ss_getClosedTabCount(aWindow) {
   815     if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID])
   816       return 0; // not a browser window, or not otherwise tracked by SS.
   818     return this._windows[aWindow.__SSID].closedTabs.length;
   819   },
   821   getClosedTabData: function ss_getClosedTabData(aWindow) {
   822     if (!aWindow.__SSID)
   823       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   825     return JSON.stringify(this._windows[aWindow.__SSID].closedTabs);
   826   },
   828   undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
   829     if (!aWindow.__SSID)
   830       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   832     let closedTabs = this._windows[aWindow.__SSID].closedTabs;
   833     if (!closedTabs)
   834       return null;
   836     // default to the most-recently closed tab
   837     aIndex = aIndex || 0;
   838     if (!(aIndex in closedTabs))
   839       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   841     // fetch the data of closed tab, while removing it from the array
   842     let closedTab = closedTabs.splice(aIndex, 1).shift();
   844     // create a new tab and bring to front
   845     let params = { selected: true };
   846     let tab = aWindow.BrowserApp.addTab(closedTab.entries[closedTab.index - 1].url, params);
   847     this._restoreHistory(closedTab, tab.browser.sessionHistory);
   849     // Put back the extra data
   850     tab.browser.__SS_extdata = closedTab.extData;
   852     return tab.browser;
   853   },
   855   forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
   856     if (!aWindow.__SSID)
   857       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   859     let closedTabs = this._windows[aWindow.__SSID].closedTabs;
   861     // default to the most-recently closed tab
   862     aIndex = aIndex || 0;
   863     if (!(aIndex in closedTabs))
   864       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   866     // remove closed tab from the array
   867     closedTabs.splice(aIndex, 1);
   868   },
   870   getTabValue: function ss_getTabValue(aTab, aKey) {
   871     let browser = aTab.browser;
   872     let data = browser.__SS_extdata || {};
   873     return data[aKey] || "";
   874   },
   876   setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
   877     let browser = aTab.browser;
   879     if (!browser.__SS_extdata)
   880       browser.__SS_extdata = {};
   881     browser.__SS_extdata[aKey] = aStringValue;
   882     this.saveStateDelayed();
   883   },
   885   deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
   886     let browser = aTab.browser;
   887     if (browser.__SS_extdata && browser.__SS_extdata[aKey])
   888       delete browser.__SS_extdata[aKey];
   889     else
   890       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   891   },
   893   restoreLastSession: function ss_restoreLastSession(aSessionString) {
   894     let self = this;
   896     function restoreWindow(data) {
   897       if (!self._restoreWindow(data)) {
   898         throw "Could not restore window";
   899       }
   901       notifyObservers();
   902     }
   904     function notifyObservers(aMessage) {
   905       Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || "");
   906     }
   908     try {
   909       // Normally, we'll receive the session string from Java, but there are
   910       // cases where we may want to restore that Java cannot detect (e.g., if
   911       // browser.sessionstore.resume_session_once is true). In these cases, the
   912       // session will be read from sessionstore.bak (which is also used for
   913       // "tabs from last time").
   914       if (aSessionString == null) {
   915         Task.spawn(function() {
   916           let bytes = yield OS.File.read(this._sessionFileBackup.path);
   917           let data = JSON.parse(new TextDecoder().decode(bytes) || "");
   918           restoreWindow(data);
   919         }.bind(this)).then(null, function onError(reason) {
   920           if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) {
   921             Cu.reportError("Session file doesn't exist");
   922           } else {
   923             Cu.reportError("SessionStore: " + reason.message);
   924           }
   925           notifyObservers("fail");
   926         });
   927       } else {
   928         restoreWindow(aSessionString);
   929       }
   930     } catch (e) {
   931       Cu.reportError("SessionStore: " + e);
   932       notifyObservers("fail");
   933     }
   934   }
   935 };
   937 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);

mercurial