browser/metro/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");
    12 Cu.import("resource://gre/modules/WindowsPrefSync.jsm");
    14 #ifdef MOZ_CRASHREPORTER
    15 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
    16   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
    17 #endif
    19 XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
    20   "resource://gre/modules/CrashMonitor.jsm");
    22 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
    23   "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
    25 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
    26   Cu.import("resource://gre/modules/NetUtil.jsm");
    27   return NetUtil;
    28 });
    30 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
    31   "resource://gre/modules/UITelemetry.jsm");
    33 // -----------------------------------------------------------------------
    34 // Session Store
    35 // -----------------------------------------------------------------------
    37 const STATE_STOPPED = 0;
    38 const STATE_RUNNING = 1;
    39 const STATE_QUITTING = -1;
    41 function SessionStore() { }
    43 SessionStore.prototype = {
    44   classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"),
    46   QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
    47                                          Ci.nsIDOMEventListener,
    48                                          Ci.nsIObserver,
    49                                          Ci.nsISupportsWeakReference]),
    51   _windows: {},
    52   _tabsFromOtherGroups: [],
    53   _selectedWindow: 1,
    54   _orderedWindows: [],
    55   _lastSaveTime: 0,
    56   _lastSessionTime: 0,
    57   _interval: 10000,
    58   _maxTabsUndo: 1,
    59   _shouldRestore: false,
    61   // Tab telemetry variables
    62   _maxTabsOpen: 1,
    64   init: function ss_init() {
    65     // Get file references
    66     this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
    67     this._sessionFileBackup = this._sessionFile.clone();
    68     this._sessionCache = this._sessionFile.clone();
    69     this._sessionFile.append("sessionstore.js");
    70     this._sessionFileBackup.append("sessionstore.bak");
    71     this._sessionCache.append("sessionstoreCache");
    73     this._loadState = STATE_STOPPED;
    75     try {
    76       UITelemetry.addSimpleMeasureFunction("metro-tabs",
    77                                           this._getTabStats.bind(this));
    78     } catch (ex) {
    79       // swallow exception that occurs if metro-tabs measure is already set up
    80     }
    82     CrashMonitor.previousCheckpoints.then(checkpoints => {
    83       let previousSessionCrashed = false;
    85       if (checkpoints) {
    86         // If the previous session finished writing the final state, we'll
    87         // assume there was no crash.
    88         previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"];
    89       } else {
    90         // If no checkpoints are saved, this is the first run with CrashMonitor or the
    91         // metroSessionCheckpoints file was corrupted/deleted, so fallback to defining
    92         // a crash as init-ing with an unexpected previousExecutionState
    93         // 1 == RUNNING, 2 == SUSPENDED
    94         previousSessionCrashed = Services.metro.previousExecutionState == 1 ||
    95           Services.metro.previousExecutionState == 2;
    96       }
    98       Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!previousSessionCrashed);
    99     });
   101     try {
   102       let shutdownWasUnclean = false;
   104       if (this._sessionFileBackup.exists()) {
   105         this._sessionFileBackup.remove(false);
   106         shutdownWasUnclean = true;
   107       }
   109       if (this._sessionFile.exists()) {
   110         this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
   112         switch(Services.metro.previousExecutionState) {
   113           // 0 == NotRunning
   114           case 0:
   115             // Disable crash recovery if we have exceeded the timeout
   116             this._lastSessionTime = this._sessionFile.lastModifiedTime;
   117             let delta = Date.now() - this._lastSessionTime;
   118             let timeout =
   119               Services.prefs.getIntPref(
   120                   "browser.sessionstore.resume_from_crash_timeout");
   121             this._shouldRestore = shutdownWasUnclean
   122                                 && (delta < (timeout * 60000));
   123             break;
   124           // 1 == Running
   125           case 1:
   126             // We should never encounter this situation
   127             Components.utils.reportError("SessionRestore.init called with "
   128                                        + "previous execution state 'Running'");
   129             this._shouldRestore = true;
   130             break;
   131           // 2 == Suspended
   132           case 2:
   133             // We should never encounter this situation
   134             Components.utils.reportError("SessionRestore.init called with "
   135                                        + "previous execution state 'Suspended'");
   136             this._shouldRestore = true;
   137             break;
   138           // 3 == Terminated
   139           case 3:
   140             // Terminated means that Windows terminated our already-suspended
   141             // process to get back some resources. When we re-launch, we want
   142             // to provide the illusion that our process was suspended the
   143             // whole time, and never terminated.
   144             this._shouldRestore = true;
   145             break;
   146           // 4 == ClosedByUser
   147           case 4:
   148             // ClosedByUser indicates that the user performed a "close" gesture
   149             // on our tile. We should act as if the browser closed normally,
   150             // even if we were closed from a suspended state (in which case
   151             // we'll have determined that it was an unclean shtudown)
   152             this._shouldRestore = false;
   153             break;
   154         }
   155       }
   157       if (!this._sessionCache.exists() || !this._sessionCache.isDirectory()) {
   158         this._sessionCache.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
   159       }
   160     } catch (ex) {
   161       Cu.reportError(ex); // file was write-locked?
   162     }
   164     this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
   165     this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
   167     // Disable crash recovery if it has been turned off
   168     if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"))
   169       this._shouldRestore = false;
   171     // Do we need to restore session just this once, in case of a restart?
   172     if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
   173       Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false);
   174       this._shouldRestore = true;
   175     }
   176   },
   178   _clearDisk: function ss_clearDisk() {
   179     if (this._sessionFile.exists()) {
   180       try {
   181         this._sessionFile.remove(false);
   182       } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
   183     }
   184     if (this._sessionFileBackup.exists()) {
   185       try {
   186         this._sessionFileBackup.remove(false);
   187       } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
   188     }
   190     this._clearCache();
   191   },
   193   _clearCache: function ss_clearCache() {
   194     // First, let's get a list of files we think should be active
   195     let activeFiles = [];
   196     this._forEachBrowserWindow(function(aWindow) {
   197       let tabs = aWindow.Browser.tabs;
   198       for (let i = 0; i < tabs.length; i++) {
   199         let browser = tabs[i].browser;
   200         if (browser.__SS_extdata && "thumbnail" in browser.__SS_extdata)
   201           activeFiles.push(browser.__SS_extdata.thumbnail);
   202       }
   203     });
   205     // Now, let's find the stale files in the cache folder
   206     let staleFiles = [];
   207     let cacheFiles = this._sessionCache.directoryEntries;
   208     while (cacheFiles.hasMoreElements()) {
   209       let file = cacheFiles.getNext().QueryInterface(Ci.nsILocalFile);
   210       let fileURI = Services.io.newFileURI(file);
   211       if (activeFiles.indexOf(fileURI) == -1)
   212         staleFiles.push(file);
   213     }
   215     // Remove the stale files in a separate step to keep the enumerator from
   216     // messing up if we remove the files as we collect them.
   217     staleFiles.forEach(function(aFile) {
   218       aFile.remove(false);
   219     })
   220   },
   222   _getTabStats: function() {
   223     return {
   224       currTabCount: this._currTabCount,
   225       maxTabCount: this._maxTabsOpen
   226     };
   227   },
   229   observe: function ss_observe(aSubject, aTopic, aData) {
   230     let self = this;
   231     let observerService = Services.obs;
   232     switch (aTopic) {
   233       case "app-startup":
   234         observerService.addObserver(this, "final-ui-startup", true);
   235         observerService.addObserver(this, "domwindowopened", true);
   236         observerService.addObserver(this, "domwindowclosed", true);
   237         observerService.addObserver(this, "browser-lastwindow-close-granted", true);
   238         observerService.addObserver(this, "browser:purge-session-history", true);
   239         observerService.addObserver(this, "quit-application-requested", true);
   240         observerService.addObserver(this, "quit-application-granted", true);
   241         observerService.addObserver(this, "quit-application", true);
   242         observerService.addObserver(this, "reset-telemetry-vars", true);
   243         break;
   244       case "final-ui-startup":
   245         observerService.removeObserver(this, "final-ui-startup");
   246         if (WindowsPrefSync) {
   247           // Pulls in Desktop controlled prefs and pushes out Metro controlled prefs
   248           WindowsPrefSync.init();
   249         }
   250         this.init();
   251         break;
   252       case "domwindowopened":
   253         let window = aSubject;
   254         window.addEventListener("load", function() {
   255           self.onWindowOpen(window);
   256           window.removeEventListener("load", arguments.callee, false);
   257         }, false);
   258         break;
   259       case "domwindowclosed": // catch closed windows
   260         this.onWindowClose(aSubject);
   261         break;
   262       case "browser-lastwindow-close-granted":
   263         // If a save has been queued, kill the timer and save state now
   264         if (this._saveTimer) {
   265           this._saveTimer.cancel();
   266           this._saveTimer = null;
   267           this.saveState();
   268         }
   270         // Freeze the data at what we've got (ignoring closing windows)
   271         this._loadState = STATE_QUITTING;
   272         break;
   273       case "quit-application-requested":
   274         // Get a current snapshot of all windows
   275         this._forEachBrowserWindow(function(aWindow) {
   276           self._collectWindowData(aWindow);
   277         });
   278         break;
   279       case "quit-application-granted":
   280         // Get a current snapshot of all windows
   281         this._forEachBrowserWindow(function(aWindow) {
   282           self._collectWindowData(aWindow);
   283         });
   285         // Freeze the data at what we've got (ignoring closing windows)
   286         this._loadState = STATE_QUITTING;
   287         break;
   288       case "quit-application":
   289         // If we are restarting, lets restore the tabs
   290         if (aData == "restart") {
   291           Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true);
   293           // Ignore purges when restarting. The notification is fired after "quit-application".
   294           Services.obs.removeObserver(this, "browser:purge-session-history");
   295         }
   297         // Freeze the data at what we've got (ignoring closing windows)
   298         this._loadState = STATE_QUITTING;
   300         // No need for this back up, we are shutting down just fine
   301         if (this._sessionFileBackup.exists())
   302           this._sessionFileBackup.remove(false);
   304         observerService.removeObserver(this, "domwindowopened");
   305         observerService.removeObserver(this, "domwindowclosed");
   306         observerService.removeObserver(this, "browser-lastwindow-close-granted");
   307         observerService.removeObserver(this, "quit-application-requested");
   308         observerService.removeObserver(this, "quit-application-granted");
   309         observerService.removeObserver(this, "quit-application");
   310         observerService.removeObserver(this, "reset-telemetry-vars");
   312         // If a save has been queued, kill the timer and save state now
   313         if (this._saveTimer) {
   314           this._saveTimer.cancel();
   315           this._saveTimer = null;
   316         }
   317         this.saveState();
   318         break;
   319       case "browser:purge-session-history": // catch sanitization
   320         this._clearDisk();
   322         // If the browser is shutting down, simply return after clearing the
   323         // session data on disk as this notification fires after the
   324         // quit-application notification so the browser is about to exit.
   325         if (this._loadState == STATE_QUITTING)
   326           return;
   328         // Clear all data about closed tabs
   329         for (let [ssid, win] in Iterator(this._windows))
   330           win._closedTabs = [];
   332         if (this._loadState == STATE_RUNNING) {
   333           // Save the purged state immediately
   334           this.saveStateNow();
   335         }
   336         break;
   337       case "timer-callback":
   338         // Timer call back for delayed saving
   339         this._saveTimer = null;
   340         this.saveState();
   341         break;
   342       case "reset-telemetry-vars":
   343         // Used in mochitests only.
   344         this._maxTabsOpen = 1;
   345     }
   346   },
   348   updateTabTelemetryVars: function(window) {
   349     this._currTabCount = window.Browser.tabs.length;
   350       if (this._currTabCount > this._maxTabsOpen) {
   351         this._maxTabsOpen = this._currTabCount;
   352       }
   353   },
   355   handleEvent: function ss_handleEvent(aEvent) {
   356     let window = aEvent.currentTarget.ownerDocument.defaultView;
   357     switch (aEvent.type) {
   358       case "load":
   359         browser = aEvent.currentTarget;
   360         if (aEvent.target == browser.contentDocument && browser.__SS_tabFormData) {
   361           browser.messageManager.sendAsyncMessage("SessionStore:restoreSessionTabData", {
   362             formdata: browser.__SS_tabFormData.formdata,
   363             scroll: browser.__SS_tabFormData.scroll
   364           });
   365         }
   366         break;
   367       case "TabOpen":
   368         this.updateTabTelemetryVars(window);
   369         let browser = aEvent.originalTarget.linkedBrowser;
   370         browser.addEventListener("load", this, true);
   371       case "TabClose": {
   372         let browser = aEvent.originalTarget.linkedBrowser;
   373         if (aEvent.type == "TabOpen") {
   374           this.onTabAdd(window, browser);
   375         }
   376         else {
   377           this.onTabClose(window, browser);
   378           this.onTabRemove(window, browser);
   379         }
   380         break;
   381     }
   382       case "TabRemove":
   383         this.updateTabTelemetryVars(window);
   384         break;
   385       case "TabSelect": {
   386         let browser = aEvent.originalTarget.linkedBrowser;
   387         this.onTabSelect(window, browser);
   388         break;
   389       }
   390     }
   391   },
   393   receiveMessage: function ss_receiveMessage(aMessage) {
   394     let browser = aMessage.target;
   395     switch (aMessage.name) {
   396       case "SessionStore:collectFormdata":
   397         browser.__SS_data.formdata = aMessage.json.data;
   398         break;
   399       case "SessionStore:collectScrollPosition":
   400         browser.__SS_data.scroll = aMessage.json.data;
   401         break;
   402       default:
   403         let window = aMessage.target.ownerDocument.defaultView;
   404         this.onTabLoad(window, aMessage.target, aMessage);
   405         break;
   406     }
   407   },
   409   onWindowOpen: function ss_onWindowOpen(aWindow) {
   410     // Return if window has already been initialized
   411     if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID])
   412       return;
   414     // Ignore non-browser windows and windows opened while shutting down
   415     if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING)
   416       return;
   418     // Assign it a unique identifier and create its data object
   419     aWindow.__SSID = "window" + gUUIDGenerator.generateUUID().toString();
   420     this._windows[aWindow.__SSID] = { tabs: [], selected: 0, _closedTabs: [] };
   421     this._orderedWindows.push(aWindow.__SSID);
   423     // Perform additional initialization when the first window is loading
   424     if (this._loadState == STATE_STOPPED) {
   425       this._loadState = STATE_RUNNING;
   426       this._lastSaveTime = Date.now();
   428       // Nothing to restore, notify observers things are complete
   429       if (!this.shouldRestore()) {
   430         this._clearCache();
   431         Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
   432       }
   433     }
   435     // Add tab change listeners to all already existing tabs
   436     let tabs = aWindow.Browser.tabs;
   437     for (let i = 0; i < tabs.length; i++)
   438       this.onTabAdd(aWindow, tabs[i].browser, true);
   440     // Notification of tab add/remove/selection
   441     let tabContainer = aWindow.document.getElementById("tabs");
   442     tabContainer.addEventListener("TabOpen", this, true);
   443     tabContainer.addEventListener("TabClose", this, true);
   444     tabContainer.addEventListener("TabRemove", this, true);
   445     tabContainer.addEventListener("TabSelect", this, true);
   446   },
   448   onWindowClose: function ss_onWindowClose(aWindow) {
   449     // Ignore windows not tracked by SessionStore
   450     if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
   451       return;
   453     let tabContainer = aWindow.document.getElementById("tabs");
   454     tabContainer.removeEventListener("TabOpen", this, true);
   455     tabContainer.removeEventListener("TabClose", this, true);
   456     tabContainer.removeEventListener("TabRemove", this, true);
   457     tabContainer.removeEventListener("TabSelect", this, true);
   459     if (this._loadState == STATE_RUNNING) {
   460       // Update all window data for a last time
   461       this._collectWindowData(aWindow);
   463       // Clear this window from the list
   464       delete this._windows[aWindow.__SSID];
   466       // Save the state without this window to disk
   467       this.saveStateDelayed();
   468     }
   470     let tabs = aWindow.Browser.tabs;
   471     for (let i = 0; i < tabs.length; i++)
   472       this.onTabRemove(aWindow, tabs[i].browser, true);
   474     delete aWindow.__SSID;
   475   },
   477   onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
   478     aBrowser.messageManager.addMessageListener("pageshow", this);
   479     aBrowser.messageManager.addMessageListener("Content:SessionHistory", this);
   480     aBrowser.messageManager.addMessageListener("SessionStore:collectFormdata", this);
   481     aBrowser.messageManager.addMessageListener("SessionStore:collectScrollPosition", this);
   483     if (!aNoNotification)
   484       this.saveStateDelayed();
   485     this._updateCrashReportURL(aWindow);
   486   },
   488   onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
   489     aBrowser.messageManager.removeMessageListener("pageshow", this);
   490     aBrowser.messageManager.removeMessageListener("Content:SessionHistory", this);
   491     aBrowser.messageManager.removeMessageListener("SessionStore:collectFormdata", this);
   492     aBrowser.messageManager.removeMessageListener("SessionStore:collectScrollPosition", this);
   494     // If this browser is being restored, skip any session save activity
   495     if (aBrowser.__SS_restore)
   496       return;
   498     delete aBrowser.__SS_data;
   500     if (!aNoNotification)
   501       this.saveStateDelayed();
   502   },
   504   onTabClose: function ss_onTabClose(aWindow, aBrowser) {
   505     if (this._maxTabsUndo == 0)
   506       return;
   508     if (aWindow.Browser.tabs.length > 0) {
   509       // Bundle this browser's data and extra data and save in the closedTabs
   510       // window property
   511       //
   512       // NB: The access to aBrowser.__SS_extdata throws during automation (in
   513       // browser_msgmgr_01). See bug 888736.
   514       let data = aBrowser.__SS_data;
   515       if (!data) {
   516         return; // Cannot restore an empty tab.
   517       }
   518       try { data.extData = aBrowser.__SS_extdata; } catch (e) { }
   520       this._windows[aWindow.__SSID]._closedTabs.unshift({ state: data });
   521       let length = this._windows[aWindow.__SSID]._closedTabs.length;
   522       if (length > this._maxTabsUndo)
   523         this._windows[aWindow.__SSID]._closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
   524     }
   525   },
   527   onTabLoad: function ss_onTabLoad(aWindow, aBrowser, aMessage) {
   528     // If this browser is being restored, skip any session save activity
   529     if (aBrowser.__SS_restore)
   530       return;
   532     // Ignore a transient "about:blank"
   533     if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank")
   534       return;
   536     if (aMessage.name == "Content:SessionHistory") {
   537       delete aBrowser.__SS_data;
   538       this._collectTabData(aBrowser, aMessage.json);
   539     }
   541     // Save out the state as quickly as possible
   542     if (aMessage.name == "pageshow")
   543       this.saveStateNow();
   545     this._updateCrashReportURL(aWindow);
   546   },
   548   onTabSelect: function ss_onTabSelect(aWindow, aBrowser) {
   549     if (this._loadState != STATE_RUNNING)
   550       return;
   552     let index = aWindow.Elements.browsers.selectedIndex;
   553     this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based
   555     // Restore the resurrected browser
   556     if (aBrowser.__SS_restore) {
   557       let data = aBrowser.__SS_data;
   558       if (data.entries.length > 0) {
   559         let json = {
   560           uri: data.entries[data.index - 1].url,
   561           flags: null,
   562           entries: data.entries,
   563           index: data.index
   564         };
   565         aBrowser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json);
   566       }
   568       delete aBrowser.__SS_restore;
   569     }
   571     this._updateCrashReportURL(aWindow);
   572   },
   574   saveStateDelayed: function ss_saveStateDelayed() {
   575     if (!this._saveTimer) {
   576       // Interval until the next disk operation is allowed
   577       let minimalDelay = this._lastSaveTime + this._interval - Date.now();
   579       // If we have to wait, set a timer, otherwise saveState directly
   580       let delay = Math.max(minimalDelay, 2000);
   581       if (delay > 0) {
   582         this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   583         this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
   584       } else {
   585         this.saveState();
   586       }
   587     }
   588   },
   590   saveStateNow: function ss_saveStateNow() {
   591     // Kill any queued timer and save immediately
   592     if (this._saveTimer) {
   593       this._saveTimer.cancel();
   594       this._saveTimer = null;
   595     }
   596     this.saveState();
   597   },
   599   saveState: function ss_saveState() {
   600     let data = this._getCurrentState();
   601     // sanity check before we overwrite the session file
   602     if (data.windows && data.windows.length && data.selectedWindow) {
   603       this._writeFile(this._sessionFile, JSON.stringify(data));
   605       this._lastSaveTime = Date.now();
   606     } else {
   607       dump("SessionStore: Not saving state with invalid data: " + JSON.stringify(data) + "\n");
   608     }
   609   },
   611   _getCurrentState: function ss_getCurrentState() {
   612     let self = this;
   613     this._forEachBrowserWindow(function(aWindow) {
   614       self._collectWindowData(aWindow);
   615     });
   617     let data = { windows: [] };
   618     for (let i = 0; i < this._orderedWindows.length; i++)
   619       data.windows.push(this._windows[this._orderedWindows[i]]);
   620     data.selectedWindow = this._selectedWindow;
   621     return data;
   622   },
   624   _collectTabData: function ss__collectTabData(aBrowser, aHistory) {
   625     // If this browser is being restored, skip any session save activity
   626     if (aBrowser.__SS_restore)
   627       return;
   629     let aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
   631     let tabData = {};
   632     tabData.entries = aHistory.entries;
   633     tabData.index = aHistory.index;
   634     tabData.attributes = { image: aBrowser.mIconURL };
   636     aBrowser.__SS_data = tabData;
   637   },
   639   _getTabData: function(aWindow) {
   640     return aWindow.Browser.tabs
   641       .filter(tab => !tab.isPrivate && tab.browser.__SS_data)
   642       .map(tab => {
   643         let browser = tab.browser;
   644         let tabData = browser.__SS_data;
   645         if (browser.__SS_extdata)
   646           tabData.extData = browser.__SS_extdata;
   647         return tabData;
   648       });
   649   },
   651   _collectWindowData: function ss__collectWindowData(aWindow) {
   652     // Ignore windows not tracked by SessionStore
   653     if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
   654       return;
   656     let winData = this._windows[aWindow.__SSID];
   658     let index = aWindow.Elements.browsers.selectedIndex;
   659     winData.selected = parseInt(index) + 1; // 1-based
   661     let tabData = this._getTabData(aWindow);
   662     winData.tabs = tabData.concat(this._tabsFromOtherGroups);
   663   },
   665   _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) {
   666     let windowsEnum = Services.wm.getEnumerator("navigator:browser");
   667     while (windowsEnum.hasMoreElements()) {
   668       let window = windowsEnum.getNext();
   669       if (window.__SSID && !window.closed)
   670         aFunc.call(this, window);
   671     }
   672   },
   674   _writeFile: function ss_writeFile(aFile, aData) {
   675     let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
   676     stateString.data = aData;
   677     Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
   679     // Don't touch the file if an observer has deleted all state data
   680     if (!stateString.data)
   681       return;
   683     // Initialize the file output stream.
   684     let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
   685     ostream.init(aFile, 0x02 | 0x08 | 0x20, 0600, ostream.DEFER_OPEN);
   687     // Obtain a converter to convert our data to a UTF-8 encoded input stream.
   688     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
   689     converter.charset = "UTF-8";
   691     // Asynchronously copy the data to the file.
   692     let istream = converter.convertToInputStream(aData);
   693     NetUtil.asyncCopy(istream, ostream, function(rc) {
   694       if (Components.isSuccessCode(rc)) {
   695         if (Services.startup.shuttingDown) {
   696           Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
   697         }
   698         Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
   699       }
   700     });
   701   },
   703   _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
   704 #ifdef MOZ_CRASHREPORTER
   705     try {
   706       let currentURI = aWindow.Browser.selectedBrowser.currentURI.clone();
   707       // if the current URI contains a username/password, remove it
   708       try {
   709         currentURI.userPass = "";
   710       }
   711       catch (ex) { } // ignore failures on about: URIs
   713       CrashReporter.annotateCrashReport("URL", currentURI.spec);
   714     }
   715     catch (ex) {
   716       // don't make noise when crashreporter is built but not enabled
   717       if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED)
   718         Components.utils.reportError("SessionStore:" + ex);
   719     }
   720 #endif
   721   },
   723   getBrowserState: function ss_getBrowserState() {
   724     let data = this._getCurrentState();
   725     return JSON.stringify(data);
   726   },
   728   getClosedTabCount: function ss_getClosedTabCount(aWindow) {
   729     if (!aWindow || !aWindow.__SSID)
   730       return 0; // not a browser window, or not otherwise tracked by SS.
   732     return this._windows[aWindow.__SSID]._closedTabs.length;
   733   },
   735   getClosedTabData: function ss_getClosedTabData(aWindow) {
   736     if (!aWindow.__SSID)
   737       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   739     return JSON.stringify(this._windows[aWindow.__SSID]._closedTabs);
   740   },
   742   undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
   743     if (!aWindow.__SSID)
   744       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   746     let closedTabs = this._windows[aWindow.__SSID]._closedTabs;
   747     if (!closedTabs)
   748       return null;
   750     // default to the most-recently closed tab
   751     aIndex = aIndex || 0;
   752     if (!(aIndex in closedTabs))
   753       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   755     // fetch the data of closed tab, while removing it from the array
   756     let closedTab = closedTabs.splice(aIndex, 1).shift();
   758     // create a new tab and bring to front
   759     let tab = aWindow.Browser.addTab(closedTab.state.entries[closedTab.state.index - 1].url, true);
   761     tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", {
   762       uri: closedTab.state.entries[closedTab.state.index - 1].url,
   763       flags: null,
   764       entries: closedTab.state.entries,
   765       index: closedTab.state.index
   766     });
   768     // Put back the extra data
   769     tab.browser.__SS_extdata = closedTab.extData;
   771     return tab.chromeTab;
   772   },
   774   forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
   775     if (!aWindow.__SSID)
   776       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   778     let closedTabs = this._windows[aWindow.__SSID]._closedTabs;
   780     // default to the most-recently closed tab
   781     aIndex = aIndex || 0;
   782     if (!(aIndex in closedTabs))
   783       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   785     // remove closed tab from the array
   786     closedTabs.splice(aIndex, 1);
   787   },
   789   getTabValue: function ss_getTabValue(aTab, aKey) {
   790     let browser = aTab.linkedBrowser;
   791     let data = browser.__SS_extdata || {};
   792     return data[aKey] || "";
   793   },
   795   setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
   796     let browser = aTab.linkedBrowser;
   798     // Thumbnails are actually stored in the cache, so do the save and update the URI
   799     if (aKey == "thumbnail") {
   800       let file = this._sessionCache.clone();
   801       file.append("thumbnail-" + browser.contentWindowId);
   802       file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
   804       let source = Services.io.newURI(aStringValue, "UTF8", null);
   805       let target = Services.io.newFileURI(file)
   807       let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
   808       persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
   809       persist.saveURI(source, null, null, null, null, file);
   811       aStringValue = target.spec;
   812     }
   814     if (!browser.__SS_extdata)
   815       browser.__SS_extdata = {};
   816     browser.__SS_extdata[aKey] = aStringValue;
   817     this.saveStateDelayed();
   818   },
   820   deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
   821     let browser = aTab.linkedBrowser;
   822     if (browser.__SS_extdata && browser.__SS_extdata[aKey])
   823       delete browser.__SS_extdata[aKey];
   824     else
   825       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   826   },
   828   shouldRestore: function ss_shouldRestore() {
   829     return this._shouldRestore || (3 == Services.prefs.getIntPref("browser.startup.page"));
   830   },
   832   restoreLastSession: function ss_restoreLastSession(aBringToFront) {
   833     let self = this;
   834     function notifyObservers(aMessage) {
   835       self._clearCache();
   836       Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || "");
   837     }
   839     // The previous session data has already been renamed to the backup file
   840     if (!this._sessionFileBackup.exists()) {
   841       notifyObservers("fail")
   842       return;
   843     }
   845     try {
   846       let channel = NetUtil.newChannel(this._sessionFileBackup);
   847       channel.contentType = "application/json";
   848       NetUtil.asyncFetch(channel, function(aStream, aResult) {
   849         if (!Components.isSuccessCode(aResult)) {
   850           Cu.reportError("SessionStore: Could not read from sessionstore.bak file");
   851           notifyObservers("fail");
   852           return;
   853         }
   855         // Read session state file into a string and let observers modify the state before it's being used
   856         let state = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
   857         state.data = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || "";
   858         aStream.close();
   860         Services.obs.notifyObservers(state, "sessionstore-state-read", "");
   862         let data = null;
   863         try {
   864           data = JSON.parse(state.data);
   865         } catch (ex) {
   866           Cu.reportError("SessionStore: Could not parse JSON: " + ex);
   867         }
   869         if (!data || data.windows.length == 0) {
   870           notifyObservers("fail");
   871           return;
   872         }
   874         let window = Services.wm.getMostRecentWindow("navigator:browser");
   876         if (typeof data.selectedWindow == "number") {
   877           this._selectedWindow = data.selectedWindow;
   878         }
   879         let windowIndex = this._selectedWindow - 1;
   880         let tabs = data.windows[windowIndex].tabs;
   881         let selected = data.windows[windowIndex].selected;
   883         let currentGroupId;
   884         try {
   885           currentGroupId = JSON.parse(data.windows[windowIndex].extData["tabview-groups"]).activeGroupId;
   886         } catch (ex) { /* currentGroupId is undefined if user has no tab groups */ }
   888         // Move all window data from sessionstore.js to this._windows.
   889         this._orderedWindows = [];
   890         for (let i = 0; i < data.windows.length; i++) {
   891           let SSID;
   892           if (i != windowIndex) {
   893             SSID = "window" + gUUIDGenerator.generateUUID().toString();
   894             this._windows[SSID] = data.windows[i];
   895           } else {
   896             SSID = window.__SSID;
   897             this._windows[SSID].extData = data.windows[i].extData;
   898             this._windows[SSID]._closedTabs =
   899               this._windows[SSID]._closedTabs.concat(data.windows[i]._closedTabs);
   900           }
   901           this._orderedWindows.push(SSID);
   902         }
   904         if (selected > tabs.length) // Clamp the selected index if it's bogus
   905           selected = 1;
   907         for (let i=0; i<tabs.length; i++) {
   908           let tabData = tabs[i];
   909           let tabGroupId = (typeof currentGroupId == "number") ?
   910             JSON.parse(tabData.extData["tabview-tab"]).groupID : null;
   912           if (tabGroupId && tabGroupId != currentGroupId) {
   913             this._tabsFromOtherGroups.push(tabData);
   914             continue;
   915           }
   917           // We must have selected tabs as soon as possible, so we let all tabs be selected
   918           // until we get the real selected tab. Then we stop selecting tabs. The end result
   919           // is that the right tab is selected, but we also don't get a bunch of errors
   920           let bringToFront = (i + 1 <= selected) && aBringToFront;
   921           let tab = window.Browser.addTab(tabData.entries[tabData.index - 1].url, bringToFront);
   923           // Start a real load for the selected tab
   924           if (i + 1 == selected) {
   925             let json = {
   926               uri: tabData.entries[tabData.index - 1].url,
   927               flags: null,
   928               entries: tabData.entries,
   929               index: tabData.index
   930             };
   931             tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json);
   932           } else {
   933             // Make sure the browser has its session data for the delay reload
   934             tab.browser.__SS_data = tabData;
   935             tab.browser.__SS_restore = true;
   937             // Restore current title
   938             tab.chromeTab.updateTitle(tabData.entries[tabData.index - 1].title);
   939           }
   941           tab.browser.__SS_tabFormData = tabData
   942           tab.browser.__SS_extdata = tabData.extData;
   943         }
   945         notifyObservers();
   946       }.bind(this));
   947     } catch (ex) {
   948       Cu.reportError("SessionStore: Could not read from sessionstore.bak file: " + ex);
   949       notifyObservers("fail");
   950     }
   951   }
   952 };
   954 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);

mercurial