forked from mirrors/gecko-dev
The mobile session store saves the current document resolution in order to restore the previous zoom level when restoring a page. If the display width has changed since the session data was captured (e.g. because the device was rotated), the resolution might have to be scaled appropriately. Currently, the session store does this scaling by itself by comparing the stored and current window widths, however this implementation is slightly simplified and doesn't cover all use cases, which means some pages can be restored at a wrong zoom level after rotation. To correctly cover all cases, the session store would have to compare viewport widths, too. Because the MobileViewportManager doesn't wait for the session store to set the restore resolution, the latter has to call setRestoreResolution() as early as possible in order to guarantee that the restore resolution is set before the first paint of the document. Therefore the session store currently calls this after receiving a LocationChange notification. However at that time, the correct viewport for the current document is not yet available, which means the resolution cannot be recalculated by the session store at that point. Therefore, this patch changes the approach taken and lets the MVM handle all resolution calculations instead. The session store now simply passes the stored previous display dimensions along with the previous document resolution to the MVM, which can then compare them to the current display and viewport widths and scale the resolution appropriately before using it during first paint. MozReview-Commit-ID: IGxWw87yftK --HG-- extra : transplant_source : e%8D%BD%26%D2%C3%8E5%E3%2B%C0t%BA%DB%C1%BBs%3F%13%1F
1648 lines
58 KiB
JavaScript
1648 lines
58 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
const Cr = Components.results;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", "resource://gre/modules/ScrollPosition.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm");
|
|
|
|
function dump(a) {
|
|
Services.console.logStringMessage(a);
|
|
}
|
|
|
|
let loggingEnabled = false;
|
|
|
|
function log(a) {
|
|
if (!loggingEnabled) {
|
|
return;
|
|
}
|
|
Log.d("SessionStore", a);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Session Store
|
|
// -----------------------------------------------------------------------
|
|
|
|
const STATE_STOPPED = 0;
|
|
const STATE_RUNNING = 1;
|
|
const STATE_QUITTING = -1;
|
|
|
|
const PRIVACY_NONE = 0;
|
|
const PRIVACY_ENCRYPTED = 1;
|
|
const PRIVACY_FULL = 2;
|
|
|
|
const PREFS_RESTORE_FROM_CRASH = "browser.sessionstore.resume_from_crash";
|
|
const PREFS_MAX_CRASH_RESUMES = "browser.sessionstore.max_resumed_crashes";
|
|
|
|
function SessionStore() { }
|
|
|
|
SessionStore.prototype = {
|
|
classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"),
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
|
|
Ci.nsIDOMEventListener,
|
|
Ci.nsIObserver,
|
|
Ci.nsISupportsWeakReference]),
|
|
|
|
_windows: {},
|
|
_lastSaveTime: 0,
|
|
_interval: 10000,
|
|
_maxTabsUndo: 5,
|
|
_pendingWrite: 0,
|
|
_scrollSavePending: null,
|
|
|
|
// The index where the most recently closed tab was in the tabs array
|
|
// when it was closed.
|
|
_lastClosedTabIndex: -1,
|
|
|
|
// Whether or not to send notifications for changes to the closed tabs.
|
|
_notifyClosedTabs: false,
|
|
|
|
init: function ss_init() {
|
|
loggingEnabled = Services.prefs.getBoolPref("browser.sessionstore.debug_logging");
|
|
|
|
// Get file references
|
|
this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
|
|
this._sessionFileBackup = this._sessionFile.clone();
|
|
this._sessionFile.append("sessionstore.js");
|
|
this._sessionFileBackup.append("sessionstore.bak");
|
|
|
|
this._loadState = STATE_STOPPED;
|
|
this._startupRestoreFinished = false;
|
|
|
|
this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
|
|
this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
|
|
|
|
// Copy changes in Gecko settings to their Java counterparts,
|
|
// so the startup code can access them
|
|
Services.prefs.addObserver(PREFS_RESTORE_FROM_CRASH, function() {
|
|
SharedPreferences.forApp().setBoolPref(PREFS_RESTORE_FROM_CRASH,
|
|
Services.prefs.getBoolPref(PREFS_RESTORE_FROM_CRASH));
|
|
}, false);
|
|
Services.prefs.addObserver(PREFS_MAX_CRASH_RESUMES, function() {
|
|
SharedPreferences.forApp().setIntPref(PREFS_MAX_CRASH_RESUMES,
|
|
Services.prefs.getIntPref(PREFS_MAX_CRASH_RESUMES));
|
|
}, false);
|
|
},
|
|
|
|
_clearDisk: function ss_clearDisk() {
|
|
OS.File.remove(this._sessionFile.path);
|
|
OS.File.remove(this._sessionFileBackup.path);
|
|
},
|
|
|
|
observe: function ss_observe(aSubject, aTopic, aData) {
|
|
let self = this;
|
|
let observerService = Services.obs;
|
|
switch (aTopic) {
|
|
case "app-startup":
|
|
observerService.addObserver(this, "final-ui-startup", true);
|
|
observerService.addObserver(this, "domwindowopened", true);
|
|
observerService.addObserver(this, "domwindowclosed", true);
|
|
observerService.addObserver(this, "browser:purge-session-history", true);
|
|
observerService.addObserver(this, "quit-application-requested", true);
|
|
observerService.addObserver(this, "quit-application-proceeding", true);
|
|
observerService.addObserver(this, "quit-application", true);
|
|
observerService.addObserver(this, "Session:Restore", true);
|
|
observerService.addObserver(this, "Session:NotifyLocationChange", true);
|
|
observerService.addObserver(this, "application-background", true);
|
|
observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
|
|
observerService.addObserver(this, "ClosedTabs:StopNotifications", true);
|
|
observerService.addObserver(this, "last-pb-context-exited", true);
|
|
observerService.addObserver(this, "Session:RestoreRecentTabs", true);
|
|
observerService.addObserver(this, "Tabs:OpenMultiple", true);
|
|
break;
|
|
case "final-ui-startup":
|
|
observerService.removeObserver(this, "final-ui-startup");
|
|
this.init();
|
|
break;
|
|
case "domwindowopened": {
|
|
let window = aSubject;
|
|
window.addEventListener("load", function() {
|
|
self.onWindowOpen(window);
|
|
window.removeEventListener("load", arguments.callee, false);
|
|
}, false);
|
|
break;
|
|
}
|
|
case "domwindowclosed": // catch closed windows
|
|
this.onWindowClose(aSubject);
|
|
break;
|
|
case "quit-application-requested":
|
|
log("quit-application-requested");
|
|
// Get a current snapshot of all windows
|
|
if (this._pendingWrite) {
|
|
this._forEachBrowserWindow(function(aWindow) {
|
|
self._collectWindowData(aWindow);
|
|
});
|
|
}
|
|
break;
|
|
case "quit-application-proceeding":
|
|
log("quit-application-proceeding");
|
|
// Freeze the data at what we've got (ignoring closing windows)
|
|
this._loadState = STATE_QUITTING;
|
|
break;
|
|
case "quit-application":
|
|
log("quit-application");
|
|
observerService.removeObserver(this, "domwindowopened");
|
|
observerService.removeObserver(this, "domwindowclosed");
|
|
observerService.removeObserver(this, "quit-application-requested");
|
|
observerService.removeObserver(this, "quit-application-proceeding");
|
|
observerService.removeObserver(this, "quit-application");
|
|
|
|
// If a save has been queued, kill the timer and save now
|
|
if (this._saveTimer) {
|
|
this._saveTimer.cancel();
|
|
this._saveTimer = null;
|
|
this.flushPendingState();
|
|
}
|
|
|
|
break;
|
|
case "browser:purge-session-history": // catch sanitization
|
|
this._clearDisk();
|
|
|
|
// Clear all data about closed tabs
|
|
for (let [ssid, win] in Iterator(this._windows))
|
|
win.closedTabs = [];
|
|
|
|
this._lastClosedTabIndex = -1;
|
|
|
|
if (this._loadState == STATE_RUNNING) {
|
|
// Save the purged state immediately
|
|
this.saveState();
|
|
} else if (this._loadState == STATE_QUITTING) {
|
|
this.saveStateDelayed();
|
|
}
|
|
|
|
Services.obs.notifyObservers(null, "sessionstore-state-purge-complete", "");
|
|
if (this._notifyClosedTabs) {
|
|
this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
|
|
}
|
|
break;
|
|
case "timer-callback":
|
|
if (this._loadState == STATE_RUNNING) {
|
|
// Timer call back for delayed saving
|
|
this._saveTimer = null;
|
|
log("timer-callback, pendingWrite = " + this._pendingWrite);
|
|
if (this._pendingWrite) {
|
|
this.saveState();
|
|
}
|
|
}
|
|
break;
|
|
case "Session:Restore": {
|
|
Services.obs.removeObserver(this, "Session:Restore");
|
|
if (aData) {
|
|
// Be ready to handle any restore failures by making sure we have a valid tab opened
|
|
let window = Services.wm.getMostRecentWindow("navigator:browser");
|
|
let restoreCleanup = {
|
|
observe: function (aSubject, aTopic, aData) {
|
|
Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored");
|
|
|
|
if (window.BrowserApp.tabs.length == 0) {
|
|
window.BrowserApp.addTab("about:home", {
|
|
selected: true
|
|
});
|
|
}
|
|
// Normally, _restoreWindow() will have set this to true already,
|
|
// but we want to make sure it's set even in case of a restore failure.
|
|
this._startupRestoreFinished = true;
|
|
log("startupRestoreFinished = true (through notification)");
|
|
}.bind(this)
|
|
};
|
|
Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false);
|
|
|
|
// Do a restore, triggered by Java
|
|
let data = JSON.parse(aData);
|
|
this.restoreLastSession(data.sessionString);
|
|
} else {
|
|
// Not doing a restore; just send restore message
|
|
this._startupRestoreFinished = true;
|
|
log("startupRestoreFinished = true");
|
|
Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
|
|
}
|
|
break;
|
|
}
|
|
case "Session:NotifyLocationChange": {
|
|
let browser = aSubject;
|
|
|
|
if (browser.__SS_restoreReloadPending && this._startupRestoreFinished) {
|
|
delete browser.__SS_restoreReloadPending;
|
|
log("remove restoreReloadPending");
|
|
}
|
|
|
|
if (browser.__SS_restoreDataOnLocationChange) {
|
|
delete browser.__SS_restoreDataOnLocationChange;
|
|
this._restoreZoom(browser.__SS_data.scrolldata, browser);
|
|
}
|
|
break;
|
|
}
|
|
case "Tabs:OpenMultiple": {
|
|
let data = JSON.parse(aData);
|
|
|
|
this._openTabs(data);
|
|
|
|
if (data.shouldNotifyTabsOpenedToJava) {
|
|
Messaging.sendRequest({
|
|
type: "Tabs:TabsOpened"
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case "application-background":
|
|
// We receive this notification when Android's onPause callback is
|
|
// executed. After onPause, the application may be terminated at any
|
|
// point without notice; therefore, we must synchronously write out any
|
|
// pending save state to ensure that this data does not get lost.
|
|
log("application-background");
|
|
this.flushPendingState();
|
|
break;
|
|
case "ClosedTabs:StartNotifications":
|
|
this._notifyClosedTabs = true;
|
|
log("ClosedTabs:StartNotifications");
|
|
this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
|
|
break;
|
|
case "ClosedTabs:StopNotifications":
|
|
this._notifyClosedTabs = false;
|
|
log("ClosedTabs:StopNotifications");
|
|
break;
|
|
case "last-pb-context-exited":
|
|
// Clear private closed tab data when we leave private browsing.
|
|
for (let [, window] in Iterator(this._windows)) {
|
|
window.closedTabs = window.closedTabs.filter(tab => !tab.isPrivate);
|
|
}
|
|
this._lastClosedTabIndex = -1;
|
|
break;
|
|
case "Session:RestoreRecentTabs": {
|
|
let data = JSON.parse(aData);
|
|
this._restoreTabs(data);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
handleEvent: function ss_handleEvent(aEvent) {
|
|
let window = aEvent.currentTarget.ownerDocument.defaultView;
|
|
switch (aEvent.type) {
|
|
case "TabOpen": {
|
|
let browser = aEvent.target;
|
|
log("TabOpen for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
this.onTabAdd(window, browser);
|
|
break;
|
|
}
|
|
case "TabClose": {
|
|
let browser = aEvent.target;
|
|
log("TabClose for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
this.onTabClose(window, browser, aEvent.detail);
|
|
this.onTabRemove(window, browser);
|
|
break;
|
|
}
|
|
case "TabPreZombify": {
|
|
let browser = aEvent.target;
|
|
log("TabPreZombify for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
this.onTabRemove(window, browser, true);
|
|
break;
|
|
}
|
|
case "TabPostZombify": {
|
|
let browser = aEvent.target;
|
|
log("TabPostZombify for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
this.onTabAdd(window, browser, true);
|
|
break;
|
|
}
|
|
case "TabSelect": {
|
|
let browser = aEvent.target;
|
|
log("TabSelect for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
this.onTabSelect(window, browser);
|
|
break;
|
|
}
|
|
case "DOMTitleChanged": {
|
|
// Use DOMTitleChanged to detect page loads over alternatives.
|
|
// onLocationChange happens too early, so we don't have the page title
|
|
// yet; pageshow happens too late, so we could lose session data if the
|
|
// browser were killed.
|
|
let browser = aEvent.currentTarget;
|
|
log("DOMTitleChanged for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
this.onTabLoad(window, browser);
|
|
break;
|
|
}
|
|
case "load": {
|
|
let browser = aEvent.currentTarget;
|
|
|
|
// Skip subframe loads.
|
|
if (browser.contentDocument !== aEvent.originalTarget) {
|
|
return;
|
|
}
|
|
|
|
// Handle restoring the text data into the content and frames.
|
|
// We wait until the main content and all frames are loaded
|
|
// before trying to restore this data.
|
|
log("load for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
if (browser.__SS_restoreDataOnLoad) {
|
|
delete browser.__SS_restoreDataOnLoad;
|
|
this._restoreTextData(browser.__SS_data.formdata, browser);
|
|
}
|
|
break;
|
|
}
|
|
case "pageshow": {
|
|
let browser = aEvent.currentTarget;
|
|
|
|
// Skip subframe pageshows.
|
|
if (browser.contentDocument !== aEvent.originalTarget) {
|
|
return;
|
|
}
|
|
|
|
// Restoring the scroll position needs to happen after the zoom level has been
|
|
// restored, which is done by the MobileViewportManager either on first paint
|
|
// or on load, whichever comes first.
|
|
// In the latter case, our load handler runs before the MVM's one, which is the
|
|
// wrong way around, so we have to use a later event instead.
|
|
log("pageshow for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
if (browser.__SS_restoreDataOnPageshow) {
|
|
delete browser.__SS_restoreDataOnPageshow;
|
|
this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
|
|
} else {
|
|
// We're not restoring, capture the initial scroll position on pageshow.
|
|
this.onTabScroll(window, browser);
|
|
}
|
|
break;
|
|
}
|
|
case "change":
|
|
case "input":
|
|
case "DOMAutoComplete": {
|
|
let browser = aEvent.currentTarget;
|
|
log("TabInput for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
this.onTabInput(window, browser);
|
|
break;
|
|
}
|
|
case "resize":
|
|
case "scroll": {
|
|
let browser = aEvent.currentTarget;
|
|
// Duplicated logging check to avoid calling getTabForBrowser on each scroll event.
|
|
if (loggingEnabled) {
|
|
log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
|
|
}
|
|
if (!this._scrollSavePending) {
|
|
this._scrollSavePending =
|
|
window.setTimeout(() => {
|
|
this._scrollSavePending = null;
|
|
this.onTabScroll(window, browser);
|
|
}, 500);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
onWindowOpen: function ss_onWindowOpen(aWindow) {
|
|
// Return if window has already been initialized
|
|
if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) {
|
|
return;
|
|
}
|
|
|
|
// Ignore non-browser windows and windows opened while shutting down
|
|
if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING) {
|
|
return;
|
|
}
|
|
|
|
// Assign it a unique identifier (timestamp) and create its data object
|
|
aWindow.__SSID = "window" + Date.now();
|
|
this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] };
|
|
|
|
// Perform additional initialization when the first window is loading
|
|
if (this._loadState == STATE_STOPPED) {
|
|
this._loadState = STATE_RUNNING;
|
|
this._lastSaveTime = Date.now();
|
|
}
|
|
|
|
// Add tab change listeners to all already existing tabs
|
|
let tabs = aWindow.BrowserApp.tabs;
|
|
for (let i = 0; i < tabs.length; i++)
|
|
this.onTabAdd(aWindow, tabs[i].browser, true);
|
|
|
|
// Notification of tab add/remove/selection/zombification
|
|
let browsers = aWindow.document.getElementById("browsers");
|
|
browsers.addEventListener("TabOpen", this, true);
|
|
browsers.addEventListener("TabClose", this, true);
|
|
browsers.addEventListener("TabSelect", this, true);
|
|
browsers.addEventListener("TabPreZombify", this, true);
|
|
browsers.addEventListener("TabPostZombify", this, true);
|
|
},
|
|
|
|
onWindowClose: function ss_onWindowClose(aWindow) {
|
|
// Ignore windows not tracked by SessionStore
|
|
if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) {
|
|
return;
|
|
}
|
|
|
|
let browsers = aWindow.document.getElementById("browsers");
|
|
browsers.removeEventListener("TabOpen", this, true);
|
|
browsers.removeEventListener("TabClose", this, true);
|
|
browsers.removeEventListener("TabSelect", this, true);
|
|
browsers.removeEventListener("TabPreZombify", this, true);
|
|
browsers.removeEventListener("TabPostZombify", this, true);
|
|
|
|
if (this._loadState == STATE_RUNNING) {
|
|
// Update all window data for a last time
|
|
this._collectWindowData(aWindow);
|
|
|
|
// Clear this window from the list
|
|
delete this._windows[aWindow.__SSID];
|
|
|
|
// Save the state without this window to disk
|
|
this.saveStateDelayed();
|
|
}
|
|
|
|
let tabs = aWindow.BrowserApp.tabs;
|
|
for (let i = 0; i < tabs.length; i++)
|
|
this.onTabRemove(aWindow, tabs[i].browser, true);
|
|
|
|
delete aWindow.__SSID;
|
|
},
|
|
|
|
onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
|
|
// Use DOMTitleChange to catch the initial load and restore history
|
|
aBrowser.addEventListener("DOMTitleChanged", this, true);
|
|
|
|
// Use load to restore text data
|
|
aBrowser.addEventListener("load", this, true);
|
|
|
|
// Gecko might set the initial zoom level after the JS "load" event,
|
|
// so we have to restore zoom and scroll position after that.
|
|
aBrowser.addEventListener("pageshow", this, true);
|
|
|
|
// Use a combination of events to watch for text data changes
|
|
aBrowser.addEventListener("change", this, true);
|
|
aBrowser.addEventListener("input", this, true);
|
|
aBrowser.addEventListener("DOMAutoComplete", this, true);
|
|
|
|
// Record the current scroll position and zoom level.
|
|
aBrowser.addEventListener("scroll", this, true);
|
|
aBrowser.addEventListener("resize", this, true);
|
|
|
|
log("onTabAdd() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
|
|
", aNoNotification = " + aNoNotification);
|
|
if (!aNoNotification) {
|
|
this.saveStateDelayed();
|
|
}
|
|
this._updateCrashReportURL(aWindow);
|
|
},
|
|
|
|
onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
|
|
// Cleanup event listeners
|
|
aBrowser.removeEventListener("DOMTitleChanged", this, true);
|
|
aBrowser.removeEventListener("load", this, true);
|
|
aBrowser.removeEventListener("pageshow", this, true);
|
|
aBrowser.removeEventListener("change", this, true);
|
|
aBrowser.removeEventListener("input", this, true);
|
|
aBrowser.removeEventListener("DOMAutoComplete", this, true);
|
|
aBrowser.removeEventListener("scroll", this, true);
|
|
aBrowser.removeEventListener("resize", this, true);
|
|
|
|
delete aBrowser.__SS_data;
|
|
|
|
log("onTabRemove() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
|
|
", aNoNotification = " + aNoNotification);
|
|
if (!aNoNotification) {
|
|
this.saveStateDelayed();
|
|
}
|
|
},
|
|
|
|
onTabClose: function ss_onTabClose(aWindow, aBrowser, aTabIndex) {
|
|
if (this._maxTabsUndo == 0) {
|
|
return;
|
|
}
|
|
|
|
if (aWindow.BrowserApp.tabs.length > 0) {
|
|
// Bundle this browser's data and extra data and save in the closedTabs
|
|
// window property
|
|
let data = aBrowser.__SS_data || {};
|
|
data.extData = aBrowser.__SS_extdata || {};
|
|
|
|
this._windows[aWindow.__SSID].closedTabs.unshift(data);
|
|
let length = this._windows[aWindow.__SSID].closedTabs.length;
|
|
if (length > this._maxTabsUndo) {
|
|
this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
|
|
}
|
|
|
|
this._lastClosedTabIndex = aTabIndex;
|
|
|
|
if (this._notifyClosedTabs) {
|
|
this._sendClosedTabsToJava(aWindow);
|
|
}
|
|
|
|
log("onTabClose() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
|
|
let evt = new Event("SSTabCloseProcessed", {"bubbles":true, "cancelable":false});
|
|
aBrowser.dispatchEvent(evt);
|
|
}
|
|
},
|
|
|
|
onTabLoad: function ss_onTabLoad(aWindow, aBrowser) {
|
|
// If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
|
|
// skip any session save activity.
|
|
if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
|
|
return;
|
|
}
|
|
|
|
// Ignore a transient "about:blank"
|
|
if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") {
|
|
return;
|
|
}
|
|
|
|
let history = aBrowser.sessionHistory;
|
|
|
|
// Serialize the tab data
|
|
let entries = [];
|
|
let index = history.index + 1;
|
|
for (let i = 0; i < history.count; i++) {
|
|
let historyEntry = history.getEntryAtIndex(i, false);
|
|
// Don't try to restore wyciwyg URLs
|
|
if (historyEntry.URI.schemeIs("wyciwyg")) {
|
|
// Adjust the index to account for skipped history entries
|
|
if (i <= history.index) {
|
|
index--;
|
|
}
|
|
continue;
|
|
}
|
|
let entry = this._serializeHistoryEntry(historyEntry);
|
|
entries.push(entry);
|
|
}
|
|
let data = { entries: entries, index: index };
|
|
|
|
let formdata;
|
|
let scrolldata;
|
|
if (aBrowser.__SS_data) {
|
|
formdata = aBrowser.__SS_data.formdata;
|
|
scrolldata = aBrowser.__SS_data.scrolldata;
|
|
}
|
|
delete aBrowser.__SS_data;
|
|
|
|
this._collectTabData(aWindow, aBrowser, data);
|
|
if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
|
|
// If the tab has been freshly restored and the "load" or "pageshow"
|
|
// events haven't yet fired, we need to preserve any form data and
|
|
// scroll positions that might have been present.
|
|
aBrowser.__SS_data.formdata = formdata;
|
|
aBrowser.__SS_data.scrolldata = scrolldata;
|
|
} else {
|
|
// When navigating via the forward/back buttons, Gecko restores
|
|
// the form data all by itself and doesn't invoke any input events.
|
|
// As _collectTabData() doesn't save any form data, we need to manually
|
|
// capture it to bridge the time until the next input event arrives.
|
|
this.onTabInput(aWindow, aBrowser);
|
|
}
|
|
|
|
log("onTabLoad() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
|
|
let evt = new Event("SSTabDataUpdated", {"bubbles":true, "cancelable":false});
|
|
aBrowser.dispatchEvent(evt);
|
|
this.saveStateDelayed();
|
|
|
|
this._updateCrashReportURL(aWindow);
|
|
},
|
|
|
|
onTabSelect: function ss_onTabSelect(aWindow, aBrowser) {
|
|
if (this._loadState != STATE_RUNNING) {
|
|
return;
|
|
}
|
|
|
|
let browsers = aWindow.document.getElementById("browsers");
|
|
let index = browsers.selectedIndex;
|
|
this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based
|
|
|
|
let tabId = aWindow.BrowserApp.getTabForBrowser(aBrowser).id;
|
|
|
|
// Restore the resurrected browser
|
|
if (aBrowser.__SS_restore) {
|
|
let data = aBrowser.__SS_data;
|
|
this._restoreTab(data, aBrowser);
|
|
|
|
delete aBrowser.__SS_restore;
|
|
aBrowser.removeAttribute("pending");
|
|
log("onTabSelect() restored zombie tab " + tabId);
|
|
}
|
|
|
|
log("onTabSelect() ran for tab " + tabId);
|
|
this.saveStateDelayed();
|
|
this._updateCrashReportURL(aWindow);
|
|
|
|
// If the selected tab has changed while listening for closed tab
|
|
// notifications, we may have switched between different private browsing
|
|
// modes.
|
|
if (this._notifyClosedTabs) {
|
|
this._sendClosedTabsToJava(aWindow);
|
|
}
|
|
},
|
|
|
|
onTabInput: function ss_onTabInput(aWindow, aBrowser) {
|
|
// If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
|
|
// skip any session save activity.
|
|
if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
|
|
return;
|
|
}
|
|
|
|
// Don't bother trying to save text data if we don't have history yet
|
|
let data = aBrowser.__SS_data;
|
|
if (!data || data.entries.length == 0) {
|
|
return;
|
|
}
|
|
|
|
// Start with storing the main content
|
|
let content = aBrowser.contentWindow;
|
|
|
|
// If the main content document has an associated URL that we are not
|
|
// allowed to store data for, bail out. We explicitly discard data for any
|
|
// children as well even if storing data for those frames would be allowed.
|
|
if (!this.checkPrivacyLevel(content.document.documentURI)) {
|
|
return;
|
|
}
|
|
|
|
// Store the main content
|
|
let formdata = FormData.collect(content) || {};
|
|
|
|
// Loop over direct child frames, and store the text data
|
|
let children = [];
|
|
for (let i = 0; i < content.frames.length; i++) {
|
|
let frame = content.frames[i];
|
|
if (!this.checkPrivacyLevel(frame.document.documentURI)) {
|
|
continue;
|
|
}
|
|
|
|
let result = FormData.collect(frame);
|
|
if (result && Object.keys(result).length) {
|
|
children[i] = result;
|
|
}
|
|
}
|
|
|
|
// If any frame had text data, add it to the main form data
|
|
if (children.length) {
|
|
formdata.children = children;
|
|
}
|
|
|
|
// If we found any form data, main content or frames, let's save it
|
|
if (Object.keys(formdata).length) {
|
|
data.formdata = formdata;
|
|
log("onTabInput() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
|
|
this.saveStateDelayed();
|
|
}
|
|
},
|
|
|
|
onTabScroll: function ss_onTabScroll(aWindow, aBrowser) {
|
|
// If we've been called directly, cancel any pending timeouts.
|
|
if (this._scrollSavePending) {
|
|
aWindow.clearTimeout(this._scrollSavePending);
|
|
this._scrollSavePending = null;
|
|
log("onTabScroll() clearing pending timeout");
|
|
}
|
|
|
|
// If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
|
|
// skip any session save activity.
|
|
if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
|
|
return;
|
|
}
|
|
|
|
// Don't bother trying to save scroll positions if we don't have history yet.
|
|
let data = aBrowser.__SS_data;
|
|
if (!data || data.entries.length == 0) {
|
|
return;
|
|
}
|
|
|
|
// Neither bother if we're yet to restore the previous scroll position.
|
|
if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
|
|
return;
|
|
}
|
|
|
|
// Start with storing the main content.
|
|
let content = aBrowser.contentWindow;
|
|
|
|
// Store the main content.
|
|
let scrolldata = ScrollPosition.collect(content) || {};
|
|
|
|
// Loop over direct child frames, and store the scroll positions.
|
|
let children = [];
|
|
for (let i = 0; i < content.frames.length; i++) {
|
|
let frame = content.frames[i];
|
|
|
|
let result = ScrollPosition.collect(frame);
|
|
if (result && Object.keys(result).length) {
|
|
children[i] = result;
|
|
}
|
|
}
|
|
|
|
// If any frame had scroll positions, add them to the main scroll data.
|
|
if (children.length) {
|
|
scrolldata.children = children;
|
|
}
|
|
|
|
// Save the current document resolution.
|
|
let zoom = { value: 1 };
|
|
content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
|
|
Ci.nsIDOMWindowUtils).getResolution(zoom);
|
|
scrolldata.zoom = {};
|
|
scrolldata.zoom.resolution = zoom.value;
|
|
log("onTabScroll() zoom level: " + zoom.value);
|
|
|
|
// Save some data that'll help in adjusting the zoom level
|
|
// when restoring in a different screen orientation.
|
|
scrolldata.zoom.displaySize = this._getContentViewerSize(content);
|
|
log("onTabScroll() displayWidth: " + scrolldata.zoom.displaySize.width);
|
|
|
|
// Save zoom and scroll data.
|
|
data.scrolldata = scrolldata;
|
|
log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
|
|
let evt = new Event("SSTabScrollCaptured", {"bubbles":true, "cancelable":false});
|
|
aBrowser.dispatchEvent(evt);
|
|
this.saveStateDelayed();
|
|
},
|
|
|
|
_getContentViewerSize: function ss_getContentViewerSize(aWindow) {
|
|
let displaySize = {};
|
|
let width = {}, height = {};
|
|
aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
|
|
Ci.nsIDOMWindowUtils).getContentViewerSize(width, height);
|
|
|
|
displaySize.width = width.value;
|
|
displaySize.height = height.value;
|
|
|
|
return displaySize;
|
|
},
|
|
|
|
saveStateDelayed: function ss_saveStateDelayed() {
|
|
if (!this._saveTimer) {
|
|
// Interval until the next disk operation is allowed
|
|
let minimalDelay = this._lastSaveTime + this._interval - Date.now();
|
|
|
|
// If we have to wait, set a timer, otherwise saveState directly
|
|
let delay = Math.max(minimalDelay, 2000);
|
|
if (delay > 0) {
|
|
this._pendingWrite++;
|
|
this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
log("saveStateDelayed() timer delay = " + delay +
|
|
", incrementing _pendingWrite to " + this._pendingWrite);
|
|
} else {
|
|
log("saveStateDelayed() no delay");
|
|
this.saveState();
|
|
}
|
|
}
|
|
log("saveStateDelayed() timer already running, taking no action");
|
|
},
|
|
|
|
saveState: function ss_saveState() {
|
|
this._pendingWrite++;
|
|
log("saveState(), incrementing _pendingWrite to " + this._pendingWrite);
|
|
this._saveState(true);
|
|
},
|
|
|
|
// Immediately and synchronously writes any pending state to disk.
|
|
flushPendingState: function ss_flushPendingState() {
|
|
log("flushPendingState(), _pendingWrite = " + this._pendingWrite);
|
|
if (this._pendingWrite) {
|
|
this._saveState(false);
|
|
}
|
|
},
|
|
|
|
_saveState: function ss_saveState(aAsync) {
|
|
log("_saveState(aAsync = " + aAsync + ")");
|
|
// Kill any queued timer and save immediately
|
|
if (this._saveTimer) {
|
|
this._saveTimer.cancel();
|
|
this._saveTimer = null;
|
|
log("_saveState() killed queued timer");
|
|
}
|
|
|
|
let data = this._getCurrentState();
|
|
let normalData = { windows: [] };
|
|
let privateData = { windows: [] };
|
|
log("_saveState() current state collected");
|
|
|
|
for (let winIndex = 0; winIndex < data.windows.length; ++winIndex) {
|
|
let win = data.windows[winIndex];
|
|
let normalWin = {};
|
|
for (let prop in win) {
|
|
normalWin[prop] = data[prop];
|
|
}
|
|
normalWin.tabs = [];
|
|
|
|
// Save normal closed tabs. Forget about private closed tabs.
|
|
normalWin.closedTabs = win.closedTabs.filter(tab => !tab.isPrivate);
|
|
|
|
normalData.windows.push(normalWin);
|
|
privateData.windows.push({ tabs: [] });
|
|
|
|
// Split the session data into private and non-private data objects.
|
|
// Non-private session data will be saved to disk, and private session
|
|
// data will be sent to Java for Android to hold it in memory.
|
|
for (let i = 0; i < win.tabs.length; ++i) {
|
|
let tab = win.tabs[i];
|
|
let savedWin = tab.isPrivate ? privateData.windows[winIndex] : normalData.windows[winIndex];
|
|
savedWin.tabs.push(tab);
|
|
if (win.selected == i + 1) {
|
|
savedWin.selected = savedWin.tabs.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write only non-private data to disk
|
|
if (normalData.windows[0] && normalData.windows[0].tabs) {
|
|
log("_saveState() writing normal data, " +
|
|
normalData.windows[0].tabs.length + " tabs in window[0]");
|
|
} else {
|
|
log("_saveState() writing empty normal data");
|
|
}
|
|
this._writeFile(this._sessionFile, normalData, aAsync);
|
|
|
|
// If we have private data, send it to Java; otherwise, send null to
|
|
// indicate that there is no private data
|
|
Messaging.sendRequest({
|
|
type: "PrivateBrowsing:Data",
|
|
session: (privateData.windows.length > 0 && privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null
|
|
});
|
|
|
|
this._lastSaveTime = Date.now();
|
|
},
|
|
|
|
_getCurrentState: function ss_getCurrentState() {
|
|
let self = this;
|
|
this._forEachBrowserWindow(function(aWindow) {
|
|
self._collectWindowData(aWindow);
|
|
});
|
|
|
|
let data = { windows: [] };
|
|
for (let index in this._windows) {
|
|
data.windows.push(this._windows[index]);
|
|
}
|
|
|
|
return data;
|
|
},
|
|
|
|
_collectTabData: function ss__collectTabData(aWindow, aBrowser, aHistory) {
|
|
// If this browser is being restored, skip any session save activity
|
|
if (aBrowser.__SS_restore) {
|
|
return;
|
|
}
|
|
|
|
aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
|
|
|
|
let tabData = {};
|
|
tabData.entries = aHistory.entries;
|
|
tabData.index = aHistory.index;
|
|
tabData.attributes = { image: aBrowser.mIconURL };
|
|
tabData.desktopMode = aWindow.BrowserApp.getTabForBrowser(aBrowser).desktopMode;
|
|
tabData.isPrivate = aBrowser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing;
|
|
|
|
aBrowser.__SS_data = tabData;
|
|
},
|
|
|
|
_collectWindowData: function ss__collectWindowData(aWindow) {
|
|
// Ignore windows not tracked by SessionStore
|
|
if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) {
|
|
return;
|
|
}
|
|
|
|
let winData = this._windows[aWindow.__SSID];
|
|
winData.tabs = [];
|
|
|
|
let browsers = aWindow.document.getElementById("browsers");
|
|
let index = browsers.selectedIndex;
|
|
winData.selected = parseInt(index) + 1; // 1-based
|
|
|
|
let tabs = aWindow.BrowserApp.tabs;
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
let browser = tabs[i].browser;
|
|
if (browser.__SS_data) {
|
|
let tabData = browser.__SS_data;
|
|
if (browser.__SS_extdata) {
|
|
tabData.extData = browser.__SS_extdata;
|
|
}
|
|
winData.tabs.push(tabData);
|
|
}
|
|
}
|
|
},
|
|
|
|
_forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) {
|
|
let windowsEnum = Services.wm.getEnumerator("navigator:browser");
|
|
while (windowsEnum.hasMoreElements()) {
|
|
let window = windowsEnum.getNext();
|
|
if (window.__SSID && !window.closed) {
|
|
aFunc.call(this, window);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Writes the session state to a disk file, while doing some telemetry and notification
|
|
* bookkeeping.
|
|
* @param aFile nsIFile used for saving the session
|
|
* @param aData JSON session state
|
|
* @param aAsync boolelan used to determine the method of saving the state
|
|
*/
|
|
_writeFile: function ss_writeFile(aFile, aData, aAsync) {
|
|
TelemetryStopwatch.start("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
|
|
let state = JSON.stringify(aData);
|
|
TelemetryStopwatch.finish("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
|
|
|
|
// Convert data string to a utf-8 encoded array buffer
|
|
let buffer = new TextEncoder().encode(state);
|
|
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_FILE_SIZE_BYTES").add(buffer.byteLength);
|
|
|
|
Services.obs.notifyObservers(null, "sessionstore-state-write", "");
|
|
let startWriteMs = Cu.now();
|
|
|
|
log("_writeFile(aAsync = " + aAsync + "), _pendingWrite = " + this._pendingWrite);
|
|
let pendingWrite = this._pendingWrite;
|
|
this._write(aFile, buffer, aAsync).then(() => {
|
|
let stopWriteMs = Cu.now();
|
|
|
|
// Make sure this._pendingWrite is the same value it was before we
|
|
// fired off the async write. If the count is different, another write
|
|
// is pending, so we shouldn't reset this._pendingWrite yet.
|
|
if (pendingWrite === this._pendingWrite) {
|
|
this._pendingWrite = 0;
|
|
}
|
|
|
|
log("_writeFile() _write() returned, _pendingWrite = " + this._pendingWrite);
|
|
|
|
// We don't use a stopwatch here since the calls are async and stopwatches can only manage
|
|
// a single timer per histogram.
|
|
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_WRITE_FILE_MS").add(Math.round(stopWriteMs - startWriteMs));
|
|
Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Writes the session state to a disk file, using async or sync methods
|
|
* @param aFile nsIFile used for saving the session
|
|
* @param aBuffer UTF-8 encoded ArrayBuffer of the session state
|
|
* @param aAsync boolelan used to determine the method of saving the state
|
|
* @return Promise that resolves when the file has been written
|
|
*/
|
|
_write: function ss_write(aFile, aBuffer, aAsync) {
|
|
// Use async file writer and just return it's promise
|
|
if (aAsync) {
|
|
log("_write() writing asynchronously");
|
|
return OS.File.writeAtomic(aFile.path, aBuffer, { tmpPath: aFile.path + ".tmp" });
|
|
}
|
|
|
|
// Convert buffer to an encoded string and sync write to disk
|
|
let bytes = String.fromCharCode.apply(null, new Uint16Array(aBuffer));
|
|
let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
|
|
stream.init(aFile, 0x02 | 0x08 | 0x20, 0o666, 0);
|
|
stream.write(bytes, bytes.length);
|
|
stream.close();
|
|
log("_write() writing synchronously");
|
|
|
|
// Return a resolved promise to make the caller happy
|
|
return Promise.resolve();
|
|
},
|
|
|
|
_updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
|
|
let crashReporterBuilt = "nsICrashReporter" in Ci && Services.appinfo instanceof Ci.nsICrashReporter;
|
|
if (!crashReporterBuilt) {
|
|
return;
|
|
}
|
|
|
|
if (!aWindow.BrowserApp.selectedBrowser) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let currentURI = aWindow.BrowserApp.selectedBrowser.currentURI.clone();
|
|
// if the current URI contains a username/password, remove it
|
|
try {
|
|
currentURI.userPass = "";
|
|
} catch (ex) { } // ignore failures on about: URIs
|
|
|
|
Services.appinfo.annotateCrashReport("URL", currentURI.spec);
|
|
} catch (ex) {
|
|
// don't make noise when crashreporter is built but not enabled
|
|
if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
|
|
Cu.reportError("SessionStore:" + ex);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Determines whether a given session history entry has been added dynamically.
|
|
*/
|
|
isDynamic: function(aEntry) {
|
|
// aEntry.isDynamicallyAdded() is true for dynamically added
|
|
// <iframe> and <frameset>, but also for <html> (the root of the
|
|
// document) so we use aEntry.parent to ensure that we're not looking
|
|
// at the root of the document
|
|
return aEntry.parent && aEntry.isDynamicallyAdded();
|
|
},
|
|
|
|
/**
|
|
* Get an object that is a serialized representation of a History entry.
|
|
*/
|
|
_serializeHistoryEntry: function _serializeHistoryEntry(aEntry) {
|
|
let entry = { url: aEntry.URI.spec };
|
|
|
|
if (aEntry.title && aEntry.title != entry.url) {
|
|
entry.title = aEntry.title;
|
|
}
|
|
|
|
if (!(aEntry instanceof Ci.nsISHEntry)) {
|
|
return entry;
|
|
}
|
|
|
|
let cacheKey = aEntry.cacheKey;
|
|
if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) {
|
|
entry.cacheKey = cacheKey.data;
|
|
}
|
|
|
|
entry.ID = aEntry.ID;
|
|
entry.docshellID = aEntry.docshellID;
|
|
|
|
if (aEntry.referrerURI) {
|
|
entry.referrer = aEntry.referrerURI.spec;
|
|
}
|
|
|
|
if (aEntry.originalURI) {
|
|
entry.originalURI = aEntry.originalURI.spec;
|
|
}
|
|
|
|
if (aEntry.loadReplace) {
|
|
entry.loadReplace = aEntry.loadReplace;
|
|
}
|
|
|
|
if (aEntry.contentType) {
|
|
entry.contentType = aEntry.contentType;
|
|
}
|
|
|
|
if (aEntry.scrollRestorationIsManual) {
|
|
entry.scrollRestorationIsManual = true;
|
|
} else {
|
|
let x = {}, y = {};
|
|
aEntry.getScrollPosition(x, y);
|
|
if (x.value != 0 || y.value != 0) {
|
|
entry.scroll = x.value + "," + y.value;
|
|
}
|
|
}
|
|
|
|
if (aEntry.owner) {
|
|
try {
|
|
let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIObjectOutputStream);
|
|
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
|
pipe.init(false, false, 0, 0xffffffff, null);
|
|
binaryStream.setOutputStream(pipe.outputStream);
|
|
binaryStream.writeCompoundObject(aEntry.owner, Ci.nsISupports, true);
|
|
binaryStream.close();
|
|
|
|
// Now we want to read the data from the pipe's input end and encode it.
|
|
let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
|
|
scriptableStream.setInputStream(pipe.inputStream);
|
|
let ownerBytes = scriptableStream.readByteArray(scriptableStream.available());
|
|
// We can stop doing base64 encoding once our serialization into JSON
|
|
// is guaranteed to handle all chars in strings, including embedded
|
|
// nulls.
|
|
entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes));
|
|
} catch (e) { dump(e); }
|
|
}
|
|
|
|
entry.docIdentifier = aEntry.BFCacheEntry.ID;
|
|
|
|
if (aEntry.stateData != null) {
|
|
entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
|
|
entry.structuredCloneVersion = aEntry.stateData.formatVersion;
|
|
}
|
|
|
|
if (!(aEntry instanceof Ci.nsISHContainer)) {
|
|
return entry;
|
|
}
|
|
|
|
if (aEntry.childCount > 0) {
|
|
let children = [];
|
|
for (let i = 0; i < aEntry.childCount; i++) {
|
|
let child = aEntry.GetChildAt(i);
|
|
|
|
if (child && !this.isDynamic(child)) {
|
|
// don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
|
|
if (child.URI.schemeIs("wyciwyg")) {
|
|
children = [];
|
|
break;
|
|
}
|
|
children.push(this._serializeHistoryEntry(child));
|
|
}
|
|
}
|
|
|
|
if (children.length) {
|
|
entry.children = children;
|
|
}
|
|
}
|
|
|
|
return entry;
|
|
},
|
|
|
|
_deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) {
|
|
let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry);
|
|
|
|
shEntry.setURI(Services.io.newURI(aEntry.url, null, null));
|
|
shEntry.setTitle(aEntry.title || aEntry.url);
|
|
if (aEntry.subframe) {
|
|
shEntry.setIsSubFrame(aEntry.subframe || false);
|
|
}
|
|
shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
|
|
if (aEntry.contentType) {
|
|
shEntry.contentType = aEntry.contentType;
|
|
}
|
|
if (aEntry.referrer) {
|
|
shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null);
|
|
}
|
|
|
|
if (aEntry.originalURI) {
|
|
shEntry.originalURI = Services.io.newURI(aEntry.originalURI, null, null);
|
|
}
|
|
|
|
if (aEntry.loadReplace) {
|
|
shEntry.loadReplace = aEntry.loadReplace;
|
|
}
|
|
|
|
if (aEntry.cacheKey) {
|
|
let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32);
|
|
cacheKey.data = aEntry.cacheKey;
|
|
shEntry.cacheKey = cacheKey;
|
|
}
|
|
|
|
if (aEntry.ID) {
|
|
// get a new unique ID for this frame (since the one from the last
|
|
// start might already be in use)
|
|
let id = aIdMap[aEntry.ID] || 0;
|
|
if (!id) {
|
|
for (id = Date.now(); id in aIdMap.used; id++);
|
|
aIdMap[aEntry.ID] = id;
|
|
aIdMap.used[id] = true;
|
|
}
|
|
shEntry.ID = id;
|
|
}
|
|
|
|
if (aEntry.docshellID) {
|
|
shEntry.docshellID = aEntry.docshellID;
|
|
}
|
|
|
|
if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
|
|
shEntry.stateData =
|
|
Cc["@mozilla.org/docshell/structured-clone-container;1"].
|
|
createInstance(Ci.nsIStructuredCloneContainer);
|
|
|
|
shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion);
|
|
}
|
|
|
|
if (aEntry.scrollRestorationIsManual) {
|
|
shEntry.scrollRestorationIsManual = true;
|
|
} else if (aEntry.scroll) {
|
|
let scrollPos = aEntry.scroll.split(",");
|
|
scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
|
|
shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
|
|
}
|
|
|
|
let childDocIdents = {};
|
|
if (aEntry.docIdentifier) {
|
|
// If we have a serialized document identifier, try to find an SHEntry
|
|
// which matches that doc identifier and adopt that SHEntry's
|
|
// BFCacheEntry. If we don't find a match, insert shEntry as the match
|
|
// for the document identifier.
|
|
let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
|
|
if (!matchingEntry) {
|
|
matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
|
|
aDocIdentMap[aEntry.docIdentifier] = matchingEntry;
|
|
} else {
|
|
shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
|
|
childDocIdents = matchingEntry.childDocIdents;
|
|
}
|
|
}
|
|
|
|
if (aEntry.owner_b64) {
|
|
let ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
|
let binaryData = atob(aEntry.owner_b64);
|
|
ownerInput.setData(binaryData, binaryData.length);
|
|
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIObjectInputStream);
|
|
binaryStream.setInputStream(ownerInput);
|
|
try { // Catch possible deserialization exceptions
|
|
shEntry.owner = binaryStream.readObject(true);
|
|
} catch (ex) { dump(ex); }
|
|
}
|
|
|
|
if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
|
|
for (let i = 0; i < aEntry.children.length; i++) {
|
|
if (!aEntry.children[i].url) {
|
|
continue;
|
|
}
|
|
|
|
// We're getting sessionrestore.js files with a cycle in the
|
|
// doc-identifier graph, likely due to bug 698656. (That is, we have
|
|
// an entry where doc identifier A is an ancestor of doc identifier B,
|
|
// and another entry where doc identifier B is an ancestor of A.)
|
|
//
|
|
// If we were to respect these doc identifiers, we'd create a cycle in
|
|
// the SHEntries themselves, which causes the docshell to loop forever
|
|
// when it looks for the root SHEntry.
|
|
//
|
|
// So as a hack to fix this, we restrict the scope of a doc identifier
|
|
// to be a node's siblings and cousins, and pass childDocIdents, not
|
|
// aDocIdents, to _deserializeHistoryEntry. That is, we say that two
|
|
// SHEntries with the same doc identifier have the same document iff
|
|
// they have the same parent or their parents have the same document.
|
|
|
|
shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i);
|
|
}
|
|
}
|
|
|
|
return shEntry;
|
|
},
|
|
|
|
// This function iterates through a list of urls opening a new tab for each.
|
|
_openTabs: function ss_openTabs(aData) {
|
|
let window = Services.wm.getMostRecentWindow("navigator:browser");
|
|
for (let i = 0; i < aData.urls.length; i++) {
|
|
let url = aData.urls[i];
|
|
let params = {
|
|
selected: (i == aData.urls.length - 1),
|
|
isPrivate: false,
|
|
desktopMode: false,
|
|
};
|
|
|
|
let tab = window.BrowserApp.addTab(url, params);
|
|
}
|
|
},
|
|
|
|
// This function iterates through a list of tab data restoring session for each of them.
|
|
_restoreTabs: function ss_restoreTabs(aData) {
|
|
let window = Services.wm.getMostRecentWindow("navigator:browser");
|
|
for (let i = 0; i < aData.tabs.length; i++) {
|
|
let tabData = JSON.parse(aData.tabs[i]);
|
|
let params = {
|
|
selected: (i == aData.tabs.length - 1),
|
|
isPrivate: tabData.isPrivate,
|
|
desktopMode: tabData.desktopMode,
|
|
};
|
|
|
|
let tab = window.BrowserApp.addTab(tabData.entries[tabData.index - 1].url, params);
|
|
tab.browser.__SS_data = tabData;
|
|
tab.browser.__SS_extdata = tabData.extData;
|
|
this._restoreTab(tabData, tab.browser);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Don't save sensitive data if the user doesn't want to
|
|
* (distinguishes between encrypted and non-encrypted sites)
|
|
*/
|
|
checkPrivacyLevel: function ss_checkPrivacyLevel(aURL) {
|
|
let isHTTPS = aURL.startsWith("https:");
|
|
let pref = "browser.sessionstore.privacy_level";
|
|
return Services.prefs.getIntPref(pref) < (isHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
|
|
},
|
|
|
|
/**
|
|
* Starts the restoration process for a browser. History is restored at this
|
|
* point, but text data must be delayed until the content loads.
|
|
*/
|
|
_restoreTab: function ss_restoreTab(aTabData, aBrowser) {
|
|
// aTabData shouldn't be empty here, but if it is,
|
|
// _restoreHistory() will crash otherwise.
|
|
if (!aTabData || aTabData.entries.length == 0) {
|
|
Cu.reportError("SessionStore.js: Error trying to restore tab with empty tabdata");
|
|
return;
|
|
}
|
|
this._restoreHistory(aTabData, aBrowser.sessionHistory);
|
|
|
|
// Various bits of state can only be restored if page loading has progressed far enough:
|
|
// The MobileViewportManager needs to be told as early as possible about
|
|
// our desired zoom level so it can take it into account during the
|
|
// initial document resolution calculation.
|
|
aBrowser.__SS_restoreDataOnLocationChange = true;
|
|
// Restoring saved form data requires the input fields to be available,
|
|
// so we have to wait for the content to load.
|
|
aBrowser.__SS_restoreDataOnLoad = true;
|
|
// Restoring the scroll position depends on the document resolution having been set,
|
|
// which is only guaranteed to have happened *after* we receive the load event.
|
|
aBrowser.__SS_restoreDataOnPageshow = true;
|
|
},
|
|
|
|
/**
|
|
* Takes serialized history data and create news entries into the given
|
|
* nsISessionHistory object.
|
|
*/
|
|
_restoreHistory: function ss_restoreHistory(aTabData, aHistory) {
|
|
if (aHistory.count > 0) {
|
|
aHistory.PurgeHistory(aHistory.count);
|
|
}
|
|
aHistory.QueryInterface(Ci.nsISHistoryInternal);
|
|
|
|
// Helper hashes for ensuring unique frame IDs and unique document
|
|
// identifiers.
|
|
let idMap = { used: {} };
|
|
let docIdentMap = {};
|
|
|
|
for (let i = 0; i < aTabData.entries.length; i++) {
|
|
if (!aTabData.entries[i].url) {
|
|
continue;
|
|
}
|
|
aHistory.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true);
|
|
}
|
|
|
|
// We need to force set the active history item and cause it to reload since
|
|
// we stop the load above
|
|
let activeIndex = (aTabData.index || aTabData.entries.length) - 1;
|
|
aHistory.getEntryAtIndex(activeIndex, true);
|
|
|
|
try {
|
|
aHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
|
|
} catch (e) {
|
|
// This will throw if the current entry is an error page.
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Takes serialized form text data and restores it into the given browser.
|
|
*/
|
|
_restoreTextData: function ss_restoreTextData(aFormData, aBrowser) {
|
|
if (aFormData) {
|
|
log("_restoreTextData()");
|
|
FormData.restoreTree(aBrowser.contentWindow, aFormData);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Restores the zoom level of the window. This needs to be called before
|
|
* first paint/load (whichever comes first) to take any effect.
|
|
*/
|
|
_restoreZoom: function ss_restoreZoom(aScrollData, aBrowser) {
|
|
if (aScrollData && aScrollData.zoom && aScrollData.zoom.displaySize) {
|
|
log("_restoreZoom(), resolution: " + aScrollData.zoom.resolution +
|
|
", old displayWidth: " + aScrollData.zoom.displaySize.width);
|
|
|
|
let utils = aBrowser.contentWindow.QueryInterface(
|
|
Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
|
// Restore zoom level.
|
|
utils.setRestoreResolution(aScrollData.zoom.resolution,
|
|
aScrollData.zoom.displaySize.width,
|
|
aScrollData.zoom.displaySize.height);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Takes serialized scroll positions and restores them into the given browser.
|
|
*/
|
|
_restoreScrollPosition: function ss_restoreScrollPosition(aScrollData, aBrowser) {
|
|
if (aScrollData) {
|
|
log("_restoreScrollPosition()");
|
|
ScrollPosition.restoreTree(aBrowser.contentWindow, aScrollData);
|
|
}
|
|
},
|
|
|
|
getBrowserState: function ss_getBrowserState() {
|
|
return this._getCurrentState();
|
|
},
|
|
|
|
_restoreWindow: function ss_restoreWindow(aData) {
|
|
let state;
|
|
try {
|
|
state = JSON.parse(aData);
|
|
} catch (e) {
|
|
throw "Invalid session JSON: " + aData;
|
|
}
|
|
|
|
// To do a restore, we must have at least one window with one tab
|
|
if (!state || state.windows.length == 0 || !state.windows[0].tabs || state.windows[0].tabs.length == 0) {
|
|
throw "Invalid session JSON: " + aData;
|
|
}
|
|
|
|
let window = Services.wm.getMostRecentWindow("navigator:browser");
|
|
|
|
let tabs = state.windows[0].tabs;
|
|
let selected = state.windows[0].selected;
|
|
log("_restoreWindow() selected tab in aData is " + selected + " of " + tabs.length)
|
|
if (selected == null || selected > tabs.length) { // Clamp the selected index if it's bogus
|
|
log("_restoreWindow() resetting selected tab");
|
|
selected = 1;
|
|
}
|
|
log("restoreWindow() window.BrowserApp.selectedTab is " + window.BrowserApp.selectedTab.id);
|
|
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
let tabData = tabs[i];
|
|
let entry = tabData.entries[tabData.index - 1];
|
|
|
|
// Use stubbed tab if we've already created it; otherwise, make a new tab
|
|
let tab;
|
|
if (tabData.tabId == null) {
|
|
let params = {
|
|
selected: (selected == i+1),
|
|
delayLoad: true,
|
|
title: entry.title,
|
|
desktopMode: (tabData.desktopMode == true),
|
|
isPrivate: (tabData.isPrivate == true)
|
|
};
|
|
tab = window.BrowserApp.addTab(entry.url, params);
|
|
} else {
|
|
tab = window.BrowserApp.getTabForId(tabData.tabId);
|
|
delete tabData.tabId;
|
|
|
|
// Don't restore tab if user has closed it
|
|
if (tab == null) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
tab.browser.__SS_data = tabData;
|
|
tab.browser.__SS_extdata = tabData.extData;
|
|
|
|
if (window.BrowserApp.selectedTab == tab) {
|
|
this._restoreTab(tabData, tab.browser);
|
|
|
|
// We can now lift the general ban on tab data capturing,
|
|
// but we still need to protect the foreground tab until we're
|
|
// sure it's actually reloading after history restoring has finished.
|
|
tab.browser.__SS_restoreReloadPending = true;
|
|
this._startupRestoreFinished = true;
|
|
log("startupRestoreFinished = true");
|
|
|
|
delete tab.browser.__SS_restore;
|
|
tab.browser.removeAttribute("pending");
|
|
} else {
|
|
// Mark the browser for delay loading
|
|
tab.browser.__SS_restore = true;
|
|
tab.browser.setAttribute("pending", "true");
|
|
}
|
|
}
|
|
|
|
// Restore the closed tabs array on the current window.
|
|
if (state.windows[0].closedTabs) {
|
|
this._windows[window.__SSID].closedTabs = state.windows[0].closedTabs;
|
|
log("_restoreWindow() loaded " + state.windows[0].closedTabs.length + " closed tabs");
|
|
}
|
|
},
|
|
|
|
getClosedTabCount: function ss_getClosedTabCount(aWindow) {
|
|
if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) {
|
|
return 0; // not a browser window, or not otherwise tracked by SS.
|
|
}
|
|
|
|
return this._windows[aWindow.__SSID].closedTabs.length;
|
|
},
|
|
|
|
getClosedTabs: function ss_getClosedTabs(aWindow) {
|
|
if (!aWindow.__SSID) {
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
return this._windows[aWindow.__SSID].closedTabs;
|
|
},
|
|
|
|
undoCloseTab: function ss_undoCloseTab(aWindow, aCloseTabData) {
|
|
if (!aWindow.__SSID) {
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
let closedTabs = this._windows[aWindow.__SSID].closedTabs;
|
|
if (!closedTabs) {
|
|
return null;
|
|
}
|
|
|
|
// If the tab data is in the closedTabs array, remove it.
|
|
closedTabs.find(function (tabData, i) {
|
|
if (tabData == aCloseTabData) {
|
|
closedTabs.splice(i, 1);
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// create a new tab and bring to front
|
|
let params = {
|
|
selected: true,
|
|
isPrivate: aCloseTabData.isPrivate,
|
|
desktopMode: aCloseTabData.desktopMode,
|
|
tabIndex: this._lastClosedTabIndex
|
|
};
|
|
let tab = aWindow.BrowserApp.addTab(aCloseTabData.entries[aCloseTabData.index - 1].url, params);
|
|
tab.browser.__SS_data = aCloseTabData;
|
|
tab.browser.__SS_extdata = aCloseTabData.extData;
|
|
this._restoreTab(aCloseTabData, tab.browser);
|
|
|
|
this._lastClosedTabIndex = -1;
|
|
|
|
if (this._notifyClosedTabs) {
|
|
this._sendClosedTabsToJava(aWindow);
|
|
}
|
|
|
|
return tab.browser;
|
|
},
|
|
|
|
forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
|
|
if (!aWindow.__SSID) {
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
let closedTabs = this._windows[aWindow.__SSID].closedTabs;
|
|
|
|
// default to the most-recently closed tab
|
|
aIndex = aIndex || 0;
|
|
if (!(aIndex in closedTabs)) {
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
// remove closed tab from the array
|
|
closedTabs.splice(aIndex, 1);
|
|
|
|
// Forget the last closed tab index if we're forgetting the last closed tab.
|
|
if (aIndex == 0) {
|
|
this._lastClosedTabIndex = -1;
|
|
}
|
|
if (this._notifyClosedTabs) {
|
|
this._sendClosedTabsToJava(aWindow);
|
|
}
|
|
},
|
|
|
|
_sendClosedTabsToJava: function ss_sendClosedTabsToJava(aWindow) {
|
|
if (!aWindow.__SSID) {
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
let closedTabs = this._windows[aWindow.__SSID].closedTabs;
|
|
let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aWindow.BrowserApp.selectedBrowser);
|
|
|
|
let tabs = closedTabs
|
|
.filter(tab => tab.isPrivate == isPrivate)
|
|
.map(function (tab) {
|
|
// Get the url and title for the last entry in the session history.
|
|
let lastEntry = tab.entries[tab.entries.length - 1];
|
|
return {
|
|
url: lastEntry.url,
|
|
title: lastEntry.title || "",
|
|
data: tab
|
|
};
|
|
});
|
|
|
|
log("sending " + tabs.length + " closed tabs to Java");
|
|
Messaging.sendRequest({
|
|
type: "ClosedTabs:Data",
|
|
tabs: tabs
|
|
});
|
|
},
|
|
|
|
getTabValue: function ss_getTabValue(aTab, aKey) {
|
|
let browser = aTab.browser;
|
|
let data = browser.__SS_extdata || {};
|
|
return data[aKey] || "";
|
|
},
|
|
|
|
setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
|
|
let browser = aTab.browser;
|
|
if (!browser.__SS_extdata) {
|
|
browser.__SS_extdata = {};
|
|
}
|
|
browser.__SS_extdata[aKey] = aStringValue;
|
|
this.saveStateDelayed();
|
|
},
|
|
|
|
deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
|
|
let browser = aTab.browser;
|
|
if (browser.__SS_extdata && aKey in browser.__SS_extdata) {
|
|
delete browser.__SS_extdata[aKey];
|
|
this.saveStateDelayed();
|
|
}
|
|
},
|
|
|
|
restoreLastSession: Task.async(function* (aSessionString) {
|
|
let notifyMessage = "";
|
|
|
|
try {
|
|
this._restoreWindow(aSessionString);
|
|
} catch (e) {
|
|
Cu.reportError("SessionStore: " + e);
|
|
notifyMessage = "fail";
|
|
}
|
|
|
|
Services.obs.notifyObservers(null, "sessionstore-windows-restored", notifyMessage);
|
|
}),
|
|
|
|
removeWindow: function ss_removeWindow(aWindow) {
|
|
if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) {
|
|
return;
|
|
}
|
|
|
|
delete this._windows[aWindow.__SSID];
|
|
delete aWindow.__SSID;
|
|
|
|
if (this._loadState == STATE_RUNNING) {
|
|
// Save the purged state immediately
|
|
this.saveState();
|
|
} else if (this._loadState == STATE_QUITTING) {
|
|
this.saveStateDelayed();
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);
|