4792 lines
166 KiB
JavaScript
4792 lines
166 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/. */
|
|
|
|
this.EXPORTED_SYMBOLS = ["SessionStore"];
|
|
|
|
const Cu = Components.utils;
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
|
|
const STATE_STOPPED = 0;
|
|
const STATE_RUNNING = 1;
|
|
const STATE_QUITTING = -1;
|
|
|
|
const STATE_STOPPED_STR = "stopped";
|
|
const STATE_RUNNING_STR = "running";
|
|
|
|
const TAB_STATE_NEEDS_RESTORE = 1;
|
|
const TAB_STATE_RESTORING = 2;
|
|
|
|
const PRIVACY_NONE = 0;
|
|
const PRIVACY_ENCRYPTED = 1;
|
|
const PRIVACY_FULL = 2;
|
|
|
|
const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
|
|
const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
|
|
|
|
// Default maximum number of tabs to restore simultaneously. Controlled by
|
|
// the browser.sessionstore.max_concurrent_tabs pref.
|
|
const DEFAULT_MAX_CONCURRENT_TAB_RESTORES = 3;
|
|
|
|
// global notifications observed
|
|
const OBSERVING = [
|
|
"domwindowopened", "domwindowclosed",
|
|
"quit-application-requested", "quit-application-granted",
|
|
"browser-lastwindow-close-granted",
|
|
"quit-application", "browser:purge-session-history",
|
|
"browser:purge-domain-data"
|
|
];
|
|
|
|
// XUL Window properties to (re)store
|
|
// Restored in restoreDimensions()
|
|
const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
|
|
|
|
// Hideable window features to (re)store
|
|
// Restored in restoreWindowFeatures()
|
|
const WINDOW_HIDEABLE_FEATURES = [
|
|
"menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
|
|
];
|
|
|
|
const MESSAGES = [
|
|
// The content script tells us that its form data (or that of one of its
|
|
// subframes) might have changed. This can be the contents or values of
|
|
// standard form fields or of ContentEditables.
|
|
"SessionStore:input",
|
|
|
|
// The content script has received a pageshow event. This happens when a
|
|
// page is loaded from bfcache without any network activity, i.e. when
|
|
// clicking the back or forward button.
|
|
"SessionStore:pageshow"
|
|
];
|
|
|
|
// These are tab events that we listen to.
|
|
const TAB_EVENTS = [
|
|
"TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
|
|
"TabUnpinned"
|
|
];
|
|
|
|
#ifndef XP_WIN
|
|
#define BROKEN_WM_Z_ORDER
|
|
#endif
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm", this);
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
|
// debug.js adds NS_ASSERT. cf. bug 669196
|
|
Cu.import("resource://gre/modules/debug.js", this);
|
|
Cu.import("resource://gre/modules/osfile.jsm", this);
|
|
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
|
|
Cu.import("resource://gre/modules/Promise.jsm", this);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
|
|
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
|
|
XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
|
|
"@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
|
|
|
|
// List of docShell capabilities to (re)store. These are automatically
|
|
// retrieved from a given docShell if not already collected before.
|
|
// This is made so they're automatically in sync with all nsIDocShell.allow*
|
|
// properties.
|
|
var gDocShellCapabilities = (function() {
|
|
let caps;
|
|
|
|
return docShell => {
|
|
if (!caps) {
|
|
let keys = Object.keys(docShell);
|
|
caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5));
|
|
}
|
|
|
|
return caps;
|
|
};
|
|
})();
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
|
"resource://gre/modules/NetUtil.jsm");
|
|
|
|
#ifdef MOZ_DEVTOOLS
|
|
XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
|
|
"resource://devtools/client/scratchpad/scratchpad-manager.jsm");
|
|
|
|
Object.defineProperty(this, "HUDService", {
|
|
get: function() {
|
|
let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools;
|
|
return devtools.require("devtools/client/webconsole/hudservice").HUDService;
|
|
},
|
|
configurable: true,
|
|
enumerable: true
|
|
});
|
|
#endif
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
|
|
"resource:///modules/sessionstore/DocumentUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
|
|
"resource:///modules/sessionstore/SessionStorage.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
|
|
"resource:///modules/sessionstore/_SessionFile.jsm");
|
|
|
|
function debug(aMsg) {
|
|
aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
|
|
Services.console.logStringMessage(aMsg);
|
|
}
|
|
|
|
this.SessionStore = {
|
|
get promiseInitialized() {
|
|
return SessionStoreInternal.promiseInitialized.promise;
|
|
},
|
|
|
|
get canRestoreLastSession() {
|
|
return SessionStoreInternal.canRestoreLastSession;
|
|
},
|
|
|
|
set canRestoreLastSession(val) {
|
|
SessionStoreInternal.canRestoreLastSession = val;
|
|
},
|
|
|
|
init: function(aWindow) {
|
|
return SessionStoreInternal.init(aWindow);
|
|
},
|
|
|
|
getBrowserState: function() {
|
|
return SessionStoreInternal.getBrowserState();
|
|
},
|
|
|
|
setBrowserState: function(aState) {
|
|
SessionStoreInternal.setBrowserState(aState);
|
|
},
|
|
|
|
getWindowState: function(aWindow) {
|
|
return SessionStoreInternal.getWindowState(aWindow);
|
|
},
|
|
|
|
setWindowState: function(aWindow, aState, aOverwrite) {
|
|
SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
|
|
},
|
|
|
|
getTabState: function(aTab) {
|
|
return SessionStoreInternal.getTabState(aTab);
|
|
},
|
|
|
|
setTabState: function(aTab, aState) {
|
|
SessionStoreInternal.setTabState(aTab, aState);
|
|
},
|
|
|
|
duplicateTab: function(aWindow, aTab, aDelta) {
|
|
return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
|
|
},
|
|
|
|
getClosedTabCount: function(aWindow) {
|
|
return SessionStoreInternal.getClosedTabCount(aWindow);
|
|
},
|
|
|
|
getClosedTabData: function(aWindow) {
|
|
return SessionStoreInternal.getClosedTabData(aWindow);
|
|
},
|
|
|
|
undoCloseTab: function(aWindow, aIndex) {
|
|
return SessionStoreInternal.undoCloseTab(aWindow, aIndex);
|
|
},
|
|
|
|
forgetClosedTab: function(aWindow, aIndex) {
|
|
return SessionStoreInternal.forgetClosedTab(aWindow, aIndex);
|
|
},
|
|
|
|
getClosedWindowCount: function() {
|
|
return SessionStoreInternal.getClosedWindowCount();
|
|
},
|
|
|
|
getClosedWindowData: function() {
|
|
return SessionStoreInternal.getClosedWindowData();
|
|
},
|
|
|
|
undoCloseWindow: function(aIndex) {
|
|
return SessionStoreInternal.undoCloseWindow(aIndex);
|
|
},
|
|
|
|
forgetClosedWindow: function(aIndex) {
|
|
return SessionStoreInternal.forgetClosedWindow(aIndex);
|
|
},
|
|
|
|
getWindowValue: function(aWindow, aKey) {
|
|
return SessionStoreInternal.getWindowValue(aWindow, aKey);
|
|
},
|
|
|
|
setWindowValue: function(aWindow, aKey, aStringValue) {
|
|
SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue);
|
|
},
|
|
|
|
deleteWindowValue: function(aWindow, aKey) {
|
|
SessionStoreInternal.deleteWindowValue(aWindow, aKey);
|
|
},
|
|
|
|
getTabValue: function(aTab, aKey) {
|
|
return SessionStoreInternal.getTabValue(aTab, aKey);
|
|
},
|
|
|
|
setTabValue: function(aTab, aKey, aStringValue) {
|
|
SessionStoreInternal.setTabValue(aTab, aKey, aStringValue);
|
|
},
|
|
|
|
deleteTabValue: function(aTab, aKey) {
|
|
SessionStoreInternal.deleteTabValue(aTab, aKey);
|
|
},
|
|
|
|
persistTabAttribute: function(aName) {
|
|
SessionStoreInternal.persistTabAttribute(aName);
|
|
},
|
|
|
|
restoreLastSession: function() {
|
|
SessionStoreInternal.restoreLastSession();
|
|
},
|
|
|
|
checkPrivacyLevel: function(aIsHTTPS, aUseDefaultPref) {
|
|
return SessionStoreInternal.checkPrivacyLevel(aIsHTTPS, aUseDefaultPref);
|
|
}
|
|
};
|
|
|
|
// Freeze the SessionStore object. We don't want anyone to modify it.
|
|
Object.freeze(SessionStore);
|
|
|
|
var SessionStoreInternal = {
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsIDOMEventListener,
|
|
Ci.nsIObserver,
|
|
Ci.nsISupportsWeakReference
|
|
]),
|
|
|
|
// set default load state
|
|
_loadState: STATE_STOPPED,
|
|
|
|
// During the initial restore and setBrowserState calls tracks the number of
|
|
// windows yet to be restored
|
|
_restoreCount: -1,
|
|
|
|
// whether a setBrowserState call is in progress
|
|
_browserSetState: false,
|
|
|
|
// time in milliseconds (Date.now()) when the session was last written to file
|
|
_lastSaveTime: 0,
|
|
|
|
// time in milliseconds when the session was started (saved across sessions),
|
|
// defaults to now if no session was restored or timestamp doesn't exist
|
|
_sessionStartTime: Date.now(),
|
|
|
|
// states for all currently opened windows
|
|
_windows: {},
|
|
|
|
// internal states for all open windows (data we need to associate,
|
|
// but not write to disk)
|
|
_internalWindows: {},
|
|
|
|
// states for all recently closed windows
|
|
_closedWindows: [],
|
|
|
|
// not-"dirty" windows usually don't need to have their data updated
|
|
_dirtyWindows: {},
|
|
|
|
// collection of session states yet to be restored
|
|
_statesToRestore: {},
|
|
|
|
// counts the number of crashes since the last clean start
|
|
_recentCrashes: 0,
|
|
|
|
// whether the last window was closed and should be restored
|
|
_restoreLastWindow: false,
|
|
|
|
// number of tabs currently restoring
|
|
_tabsRestoringCount: 0,
|
|
|
|
// max number of tabs to restore concurrently
|
|
_maxConcurrentTabRestores: DEFAULT_MAX_CONCURRENT_TAB_RESTORES,
|
|
|
|
// whether restored tabs load cached versions or force a reload
|
|
_cacheBehavior: 0,
|
|
|
|
// The state from the previous session (after restoring pinned tabs). This
|
|
// state is persisted and passed through to the next session during an app
|
|
// restart to make the third party add-on warning not trash the deferred
|
|
// session
|
|
_lastSessionState: null,
|
|
|
|
// When starting Firefox with a single private window, this is the place
|
|
// where we keep the session we actually wanted to restore in case the user
|
|
// decides to later open a non-private window as well.
|
|
_deferredInitialState: null,
|
|
|
|
// A promise resolved once initialization is complete
|
|
_promiseInitialization: Promise.defer(),
|
|
|
|
// Whether session has been initialized
|
|
_sessionInitialized: false,
|
|
|
|
// True if session store is disabled by multi-process browsing.
|
|
// See bug 516755.
|
|
_disabledForMultiProcess: false,
|
|
|
|
// The original "sessionstore.resume_session_once" preference value before it
|
|
// was modified by saveState. saveState will set the
|
|
// "sessionstore.resume_session_once" to true when the
|
|
// the "sessionstore.resume_from_crash" preference is false (crash recovery
|
|
// is disabled) so that pinned tabs will be restored in the case of a
|
|
// crash. This variable is used to restore the original value so the
|
|
// previous session is not always restored when
|
|
// "sessionstore.resume_from_crash" is true.
|
|
_resume_session_once_on_shutdown: null,
|
|
|
|
/**
|
|
* A promise fulfilled once initialization is complete.
|
|
*/
|
|
get promiseInitialized() {
|
|
return this._promiseInitialization;
|
|
},
|
|
|
|
/* ........ Public Getters .............. */
|
|
get canRestoreLastSession() {
|
|
return this._lastSessionState;
|
|
},
|
|
|
|
set canRestoreLastSession(val) {
|
|
this._lastSessionState = null;
|
|
},
|
|
|
|
/* ........ Global Event Handlers .............. */
|
|
|
|
/**
|
|
* Initialize the component
|
|
*/
|
|
initService: function() {
|
|
if (this._sessionInitialized) {
|
|
return;
|
|
}
|
|
OBSERVING.forEach(function(aTopic) {
|
|
Services.obs.addObserver(this, aTopic, true);
|
|
}, this);
|
|
|
|
this._initPrefs();
|
|
|
|
this._disabledForMultiProcess = false;
|
|
|
|
// this pref is only read at startup, so no need to observe it
|
|
this._sessionhistory_max_entries =
|
|
this._prefBranch.getIntPref("sessionhistory.max_entries");
|
|
|
|
gSessionStartup.onceInitialized.then(
|
|
this.initSession.bind(this)
|
|
);
|
|
},
|
|
|
|
initSession: function() {
|
|
let ss = gSessionStartup;
|
|
try {
|
|
if (ss.doRestore() ||
|
|
ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
|
|
this._initialState = ss.state;
|
|
}
|
|
catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok
|
|
|
|
if (this._initialState) {
|
|
try {
|
|
// If we're doing a DEFERRED session, then we want to pull pinned tabs
|
|
// out so they can be restored.
|
|
if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
|
|
let [iniState, remainingState] = this._prepDataForDeferredRestore(this._initialState);
|
|
// If we have a iniState with windows, that means that we have windows
|
|
// with app tabs to restore.
|
|
if (iniState.windows.length)
|
|
this._initialState = iniState;
|
|
else
|
|
this._initialState = null;
|
|
if (remainingState.windows.length)
|
|
this._lastSessionState = remainingState;
|
|
}
|
|
else {
|
|
// Get the last deferred session in case the user still wants to
|
|
// restore it
|
|
this._lastSessionState = this._initialState.lastSessionState;
|
|
|
|
let lastSessionCrashed =
|
|
this._initialState.session && this._initialState.session.state &&
|
|
this._initialState.session.state == STATE_RUNNING_STR;
|
|
if (lastSessionCrashed) {
|
|
this._recentCrashes = (this._initialState.session &&
|
|
this._initialState.session.recentCrashes || 0) + 1;
|
|
|
|
if (this._needsRestorePage(this._initialState, this._recentCrashes)) {
|
|
// replace the crashed session with a restore-page-only session
|
|
let pageData = {
|
|
url: "about:sessionrestore",
|
|
formdata: {
|
|
id: { "sessionData": this._initialState },
|
|
xpath: {}
|
|
}
|
|
};
|
|
this._initialState = { windows: [{ tabs: [{ entries: [pageData] }] }] };
|
|
}
|
|
}
|
|
|
|
// Load the session start time from the previous state
|
|
this._sessionStartTime = this._initialState.session &&
|
|
this._initialState.session.startTime ||
|
|
this._sessionStartTime;
|
|
|
|
// make sure that at least the first window doesn't have anything hidden
|
|
delete this._initialState.windows[0].hidden;
|
|
// Since nothing is hidden in the first window, it cannot be a popup
|
|
delete this._initialState.windows[0].isPopup;
|
|
// We don't want to minimize and then open a window at startup.
|
|
if (this._initialState.windows[0].sizemode == "minimized")
|
|
this._initialState.windows[0].sizemode = "normal";
|
|
// clear any lastSessionWindowID attributes since those don't matter
|
|
// during normal restore
|
|
this._initialState.windows.forEach(function(aWindow) {
|
|
delete aWindow.__lastSessionWindowID;
|
|
});
|
|
}
|
|
}
|
|
catch (ex) { debug("The session file is invalid: " + ex); }
|
|
}
|
|
|
|
// A Lazy getter for the sessionstore.js backup promise.
|
|
XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function() {
|
|
return _SessionFile.createBackupCopy();
|
|
});
|
|
|
|
// at this point, we've as good as resumed the session, so we can
|
|
// clear the resume_session_once flag, if it's set
|
|
if (this._loadState != STATE_QUITTING &&
|
|
this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
|
|
this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
|
|
|
|
this._initEncoding();
|
|
|
|
// Session is ready.
|
|
this._sessionInitialized = true;
|
|
this._promiseInitialization.resolve();
|
|
},
|
|
|
|
_initEncoding : function() {
|
|
// The (UTF-8) encoder used to write to files.
|
|
XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function() {
|
|
return new TextEncoder();
|
|
});
|
|
},
|
|
|
|
_initPrefs : function() {
|
|
XPCOMUtils.defineLazyGetter(this, "_prefBranch", function() {
|
|
return Services.prefs.getBranch("browser.");
|
|
});
|
|
|
|
// minimal interval between two save operations (in milliseconds)
|
|
XPCOMUtils.defineLazyGetter(this, "_interval", function() {
|
|
// used often, so caching/observing instead of fetching on-demand
|
|
this._prefBranch.addObserver("sessionstore.interval", this, true);
|
|
return this._prefBranch.getIntPref("sessionstore.interval");
|
|
});
|
|
|
|
// when crash recovery is disabled, session data is not written to disk
|
|
XPCOMUtils.defineLazyGetter(this, "_resume_from_crash", function() {
|
|
// get crash recovery state from prefs and allow for proper reaction to state changes
|
|
this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true);
|
|
return this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
|
|
});
|
|
|
|
this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
|
|
this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
|
|
|
|
this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
|
|
this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
|
|
|
|
// Straight-up collect the following one-time prefs
|
|
this._maxConcurrentTabRestores =
|
|
Services.prefs.getIntPref("browser.sessionstore.max_concurrent_tabs");
|
|
// ensure a sane value for concurrency, ignore and set default otherwise
|
|
if (this._maxConcurrentTabRestores < 1 || this._maxConcurrentTabRestores > 10) {
|
|
this._maxConcurrentTabRestores = DEFAULT_MAX_CONCURRENT_TAB_RESTORES;
|
|
}
|
|
this._cacheBehavior =
|
|
Services.prefs.getIntPref("browser.sessionstore.cache_behavior");
|
|
|
|
},
|
|
|
|
_initWindow: function(aWindow) {
|
|
if (aWindow) {
|
|
this.onLoad(aWindow);
|
|
} else if (this._loadState == STATE_STOPPED) {
|
|
// If init is being called with a null window, it's possible that we
|
|
// just want to tell sessionstore that a session is live (as is the case
|
|
// with starting Firefox with -private, for example; see bug 568816),
|
|
// so we should mark the load state as running to make sure that
|
|
// things like setBrowserState calls will succeed in restoring the session.
|
|
this._loadState = STATE_RUNNING;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Start tracking a window.
|
|
*
|
|
* This function also initializes the component if it is not
|
|
* initialized yet.
|
|
*/
|
|
init: function(aWindow) {
|
|
let self = this;
|
|
this.initService();
|
|
return this._promiseInitialization.promise.then(
|
|
function onSuccess() {
|
|
self._initWindow(aWindow);
|
|
}
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Called on application shutdown, after notifications:
|
|
* quit-application-granted, quit-application
|
|
*/
|
|
_uninit: function() {
|
|
// save all data for session resuming
|
|
if (this._sessionInitialized)
|
|
this.saveState(true);
|
|
|
|
// clear out priority queue in case it's still holding refs
|
|
TabRestoreQueue.reset();
|
|
|
|
// Make sure to break our cycle with the save timer
|
|
if (this._saveTimer) {
|
|
this._saveTimer.cancel();
|
|
this._saveTimer = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle notifications
|
|
*/
|
|
observe: function(aSubject, aTopic, aData) {
|
|
if (this._disabledForMultiProcess)
|
|
return;
|
|
|
|
switch (aTopic) {
|
|
case "domwindowopened": // catch new windows
|
|
this.onOpen(aSubject);
|
|
break;
|
|
case "domwindowclosed": // catch closed windows
|
|
this.onClose(aSubject);
|
|
break;
|
|
case "quit-application-requested":
|
|
this.onQuitApplicationRequested();
|
|
break;
|
|
case "quit-application-granted":
|
|
this.onQuitApplicationGranted();
|
|
break;
|
|
case "browser-lastwindow-close-granted":
|
|
this.onLastWindowCloseGranted();
|
|
break;
|
|
case "quit-application":
|
|
this.onQuitApplication(aData);
|
|
break;
|
|
case "browser:purge-session-history": // catch sanitization
|
|
this.onPurgeSessionHistory();
|
|
break;
|
|
case "browser:purge-domain-data":
|
|
this.onPurgeDomainData(aData);
|
|
break;
|
|
case "nsPref:changed": // catch pref changes
|
|
this.onPrefChange(aData);
|
|
break;
|
|
case "timer-callback": // timer call back for delayed saving
|
|
this.onTimerCallback();
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This method handles incoming messages sent by the session store content
|
|
* script and thus enables communication with OOP tabs.
|
|
*/
|
|
receiveMessage: function(aMessage) {
|
|
var browser = aMessage.target;
|
|
var win = browser.ownerDocument.defaultView;
|
|
|
|
switch (aMessage.name) {
|
|
case "SessionStore:pageshow":
|
|
this.onTabLoad(win, browser);
|
|
break;
|
|
case "SessionStore:input":
|
|
this.onTabInput(win, browser);
|
|
break;
|
|
default:
|
|
debug("received unknown message '" + aMessage.name + "'");
|
|
break;
|
|
}
|
|
|
|
this._clearRestoringWindows();
|
|
},
|
|
|
|
/* ........ Window Event Handlers .............. */
|
|
|
|
/**
|
|
* Implement nsIDOMEventListener for handling various window and tab events
|
|
*/
|
|
handleEvent: function(aEvent) {
|
|
if (this._disabledForMultiProcess)
|
|
return;
|
|
|
|
var win = aEvent.currentTarget.ownerDocument.defaultView;
|
|
switch (aEvent.type) {
|
|
case "load":
|
|
// If __SS_restore_data is set, then we need to restore the document
|
|
// (form data, scrolling, etc.). This will only happen when a tab is
|
|
// first restored.
|
|
let browser = aEvent.currentTarget;
|
|
if (browser.__SS_restore_data)
|
|
this.restoreDocument(win, browser, aEvent);
|
|
this.onTabLoad(win, browser);
|
|
break;
|
|
case "TabOpen":
|
|
this.onTabAdd(win, aEvent.originalTarget);
|
|
break;
|
|
case "TabClose":
|
|
// aEvent.detail determines if the tab was closed by moving to a different window
|
|
if (!aEvent.detail)
|
|
this.onTabClose(win, aEvent.originalTarget);
|
|
this.onTabRemove(win, aEvent.originalTarget);
|
|
break;
|
|
case "TabSelect":
|
|
this.onTabSelect(win);
|
|
break;
|
|
case "TabShow":
|
|
this.onTabShow(win, aEvent.originalTarget);
|
|
break;
|
|
case "TabHide":
|
|
this.onTabHide(win, aEvent.originalTarget);
|
|
break;
|
|
case "TabPinned":
|
|
case "TabUnpinned":
|
|
this.saveStateDelayed(win);
|
|
break;
|
|
}
|
|
|
|
this._clearRestoringWindows();
|
|
},
|
|
|
|
/**
|
|
* If it's the first window load since app start...
|
|
* - determine if we're reloading after a crash or a forced-restart
|
|
* - restore window state
|
|
* - restart downloads
|
|
* Set up event listeners for this window's tabs
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
onLoad: function(aWindow) {
|
|
// return if window has already been initialized
|
|
if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi])
|
|
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)
|
|
aWindow.__SSi = "window" + Date.now();
|
|
|
|
// and create its data object
|
|
this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
|
|
|
|
// and create its internal data object
|
|
this._internalWindows[aWindow.__SSi] = { hosts: {} }
|
|
|
|
let isPrivateWindow = false;
|
|
if (PrivateBrowsingUtils.isWindowPrivate(aWindow))
|
|
this._windows[aWindow.__SSi].isPrivate = isPrivateWindow = true;
|
|
if (!this._isWindowLoaded(aWindow))
|
|
this._windows[aWindow.__SSi]._restoring = true;
|
|
if (!aWindow.toolbar.visible)
|
|
this._windows[aWindow.__SSi].isPopup = true;
|
|
|
|
// perform additional initialization when the first window is loading
|
|
if (this._loadState == STATE_STOPPED) {
|
|
this._loadState = STATE_RUNNING;
|
|
this._lastSaveTime = Date.now();
|
|
|
|
// restore a crashed session resp. resume the last session if requested
|
|
if (this._initialState) {
|
|
if (isPrivateWindow) {
|
|
// We're starting with a single private window. Save the state we
|
|
// actually wanted to restore so that we can do it later in case
|
|
// the user opens another, non-private window.
|
|
this._deferredInitialState = gSessionStartup.state;
|
|
delete this._initialState;
|
|
|
|
// Nothing to restore now, notify observers things are complete.
|
|
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
|
|
} else {
|
|
// make sure that the restored tabs are first in the window
|
|
this._initialState._firstTabs = true;
|
|
this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0;
|
|
this.restoreWindow(aWindow, this._initialState,
|
|
this._isCmdLineEmpty(aWindow, this._initialState));
|
|
delete this._initialState;
|
|
|
|
// _loadState changed from "stopped" to "running"
|
|
// force a save operation so that crashes happening during startup are correctly counted
|
|
this.saveState(true);
|
|
}
|
|
}
|
|
else {
|
|
// Nothing to restore, notify observers things are complete.
|
|
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
|
|
|
|
// the next delayed save request should execute immediately
|
|
this._lastSaveTime -= this._interval;
|
|
}
|
|
}
|
|
// this window was opened by _openWindowWithState
|
|
else if (!this._isWindowLoaded(aWindow)) {
|
|
let followUp = this._statesToRestore[aWindow.__SS_restoreID].windows.length == 1;
|
|
this.restoreWindow(aWindow, this._statesToRestore[aWindow.__SS_restoreID], true, followUp);
|
|
}
|
|
// The user opened another, non-private window after starting up with
|
|
// a single private one. Let's restore the session we actually wanted to
|
|
// restore at startup.
|
|
else if (this._deferredInitialState && !isPrivateWindow &&
|
|
aWindow.toolbar.visible) {
|
|
|
|
this._deferredInitialState._firstTabs = true;
|
|
this._restoreCount = this._deferredInitialState.windows ?
|
|
this._deferredInitialState.windows.length : 0;
|
|
this.restoreWindow(aWindow, this._deferredInitialState, false);
|
|
this._deferredInitialState = null;
|
|
}
|
|
else if (this._restoreLastWindow && aWindow.toolbar.visible &&
|
|
this._closedWindows.length && !isPrivateWindow) {
|
|
|
|
// default to the most-recently closed window
|
|
// don't use popup windows
|
|
let closedWindowState = null;
|
|
let closedWindowIndex;
|
|
for (let i = 0; i < this._closedWindows.length; i++) {
|
|
// Take the first non-popup, point our object at it, and break out.
|
|
if (!this._closedWindows[i].isPopup) {
|
|
closedWindowState = this._closedWindows[i];
|
|
closedWindowIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (closedWindowState) {
|
|
let newWindowState;
|
|
#ifndef XP_MACOSX
|
|
if (!this._doResumeSession()) {
|
|
#endif
|
|
// We want to split the window up into pinned tabs and unpinned tabs.
|
|
// Pinned tabs should be restored. If there are any remaining tabs,
|
|
// they should be added back to _closedWindows.
|
|
// We'll cheat a little bit and reuse _prepDataForDeferredRestore
|
|
// even though it wasn't built exactly for this.
|
|
let [appTabsState, normalTabsState] =
|
|
this._prepDataForDeferredRestore({ windows: [closedWindowState] });
|
|
|
|
// These are our pinned tabs, which we should restore
|
|
if (appTabsState.windows.length) {
|
|
newWindowState = appTabsState.windows[0];
|
|
delete newWindowState.__lastSessionWindowID;
|
|
}
|
|
|
|
// In case there were no unpinned tabs, remove the window from _closedWindows
|
|
if (!normalTabsState.windows.length) {
|
|
this._closedWindows.splice(closedWindowIndex, 1);
|
|
}
|
|
// Or update _closedWindows with the modified state
|
|
else {
|
|
delete normalTabsState.windows[0].__lastSessionWindowID;
|
|
this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
|
|
}
|
|
#ifndef XP_MACOSX
|
|
}
|
|
else {
|
|
// If we're just restoring the window, make sure it gets removed from
|
|
// _closedWindows.
|
|
this._closedWindows.splice(closedWindowIndex, 1);
|
|
newWindowState = closedWindowState;
|
|
delete newWindowState.hidden;
|
|
}
|
|
#endif
|
|
if (newWindowState) {
|
|
// Ensure that the window state isn't hidden
|
|
this._restoreCount = 1;
|
|
let state = { windows: [newWindowState] };
|
|
this.restoreWindow(aWindow, state, this._isCmdLineEmpty(aWindow, state));
|
|
}
|
|
}
|
|
// we actually restored the session just now.
|
|
this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
|
|
}
|
|
if (this._restoreLastWindow && aWindow.toolbar.visible) {
|
|
// always reset (if not a popup window)
|
|
// we don't want to restore a window directly after, for example,
|
|
// undoCloseWindow was executed.
|
|
this._restoreLastWindow = false;
|
|
}
|
|
|
|
var tabbrowser = aWindow.gBrowser;
|
|
|
|
// add tab change listeners to all already existing tabs
|
|
for (let i = 0; i < tabbrowser.tabs.length; i++) {
|
|
this.onTabAdd(aWindow, tabbrowser.tabs[i], true);
|
|
}
|
|
// notification of tab add/remove/selection/show/hide
|
|
TAB_EVENTS.forEach(function(aEvent) {
|
|
tabbrowser.tabContainer.addEventListener(aEvent, this, true);
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* On window open
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
onOpen: function(aWindow) {
|
|
var _this = this;
|
|
aWindow.addEventListener("load", function(aEvent) {
|
|
aEvent.currentTarget.removeEventListener("load", arguments.callee, false);
|
|
_this.onLoad(aEvent.currentTarget);
|
|
}, false);
|
|
return;
|
|
},
|
|
|
|
/**
|
|
* On window close...
|
|
* - remove event listeners from tabs
|
|
* - save all window data
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
onClose: function(aWindow) {
|
|
// this window was about to be restored - conserve its original data, if any
|
|
let isFullyLoaded = this._isWindowLoaded(aWindow);
|
|
if (!isFullyLoaded) {
|
|
if (!aWindow.__SSi)
|
|
aWindow.__SSi = "window" + Date.now();
|
|
this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
|
|
delete this._statesToRestore[aWindow.__SS_restoreID];
|
|
delete aWindow.__SS_restoreID;
|
|
}
|
|
|
|
// ignore windows not tracked by SessionStore
|
|
if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
|
|
return;
|
|
}
|
|
|
|
// notify that the session store will stop tracking this window so that
|
|
// extensions can store any data about this window in session store before
|
|
// that's not possible anymore
|
|
let event = aWindow.document.createEvent("Events");
|
|
event.initEvent("SSWindowClosing", true, false);
|
|
aWindow.dispatchEvent(event);
|
|
|
|
if (this.windowToFocus && this.windowToFocus == aWindow) {
|
|
delete this.windowToFocus;
|
|
}
|
|
|
|
var tabbrowser = aWindow.gBrowser;
|
|
|
|
TAB_EVENTS.forEach(function(aEvent) {
|
|
tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
|
|
}, this);
|
|
|
|
// remove the progress listener for this window
|
|
tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener);
|
|
|
|
let winData = this._windows[aWindow.__SSi];
|
|
if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down
|
|
// update all window data for a last time
|
|
this._collectWindowData(aWindow);
|
|
|
|
if (isFullyLoaded) {
|
|
winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label;
|
|
winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
|
|
tabbrowser.selectedTab);
|
|
let windows = {};
|
|
windows[aWindow.__SSi] = winData;
|
|
this._updateCookies(windows);
|
|
}
|
|
|
|
#ifndef XP_MACOSX
|
|
// Until we decide otherwise elsewhere, this window is part of a series
|
|
// of closing windows to quit.
|
|
winData._shouldRestore = true;
|
|
#endif
|
|
|
|
// Save the window if it has multiple tabs or a single saveable tab and
|
|
// it's not private.
|
|
if (!winData.isPrivate && (winData.tabs.length > 1 ||
|
|
(winData.tabs.length == 1 && this._shouldSaveTabState(winData.tabs[0])))) {
|
|
// we don't want to save the busy state
|
|
delete winData.busy;
|
|
|
|
this._closedWindows.unshift(winData);
|
|
this._capClosedWindows();
|
|
}
|
|
|
|
// clear this window from the list
|
|
delete this._windows[aWindow.__SSi];
|
|
delete this._internalWindows[aWindow.__SSi];
|
|
|
|
// save the state without this window to disk
|
|
this.saveStateDelayed();
|
|
}
|
|
|
|
for (let i = 0; i < tabbrowser.tabs.length; i++) {
|
|
this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
|
|
}
|
|
|
|
// Cache the window state until it is completely gone.
|
|
DyingWindowCache.set(aWindow, winData);
|
|
|
|
delete aWindow.__SSi;
|
|
},
|
|
|
|
/**
|
|
* On quit application requested
|
|
*/
|
|
onQuitApplicationRequested: function() {
|
|
// get a current snapshot of all windows
|
|
this._forEachBrowserWindow(function(aWindow) {
|
|
this._collectWindowData(aWindow);
|
|
});
|
|
// we must cache this because _getMostRecentBrowserWindow will always
|
|
// return null by the time quit-application occurs
|
|
var activeWindow = this._getMostRecentBrowserWindow();
|
|
if (activeWindow)
|
|
this.activeWindowSSiCache = activeWindow.__SSi || "";
|
|
this._dirtyWindows = [];
|
|
},
|
|
|
|
/**
|
|
* On quit application granted
|
|
*/
|
|
onQuitApplicationGranted: function() {
|
|
// freeze the data at what we've got (ignoring closing windows)
|
|
this._loadState = STATE_QUITTING;
|
|
},
|
|
|
|
/**
|
|
* On last browser window close
|
|
*/
|
|
onLastWindowCloseGranted: function() {
|
|
// last browser window is quitting.
|
|
// remember to restore the last window when another browser window is opened
|
|
// do not account for pref(resume_session_once) at this point, as it might be
|
|
// set by another observer getting this notice after us
|
|
this._restoreLastWindow = true;
|
|
},
|
|
|
|
/**
|
|
* On quitting application
|
|
* @param aData
|
|
* String type of quitting
|
|
*/
|
|
onQuitApplication: function(aData) {
|
|
if (aData == "restart") {
|
|
this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
|
|
// The browser:purge-session-history notification fires after the
|
|
// quit-application notification so unregister the
|
|
// browser:purge-session-history notification to prevent clearing
|
|
// session data on disk on a restart. It is also unnecessary to
|
|
// perform any other sanitization processing on a restart as the
|
|
// browser is about to exit anyway.
|
|
Services.obs.removeObserver(this, "browser:purge-session-history");
|
|
}
|
|
else if (this._resume_session_once_on_shutdown != null) {
|
|
// if the sessionstore.resume_session_once preference was changed by
|
|
// saveState because crash recovery is disabled then restore the
|
|
// preference back to the value it was prior to that. This will prevent
|
|
// SessionStore from always restoring the session when crash recovery is
|
|
// disabled.
|
|
this._prefBranch.setBoolPref("sessionstore.resume_session_once",
|
|
this._resume_session_once_on_shutdown);
|
|
}
|
|
|
|
if (aData != "restart") {
|
|
// Throw away the previous session on shutdown
|
|
this._lastSessionState = null;
|
|
}
|
|
|
|
this._loadState = STATE_QUITTING; // just to be sure
|
|
this._uninit();
|
|
},
|
|
|
|
/**
|
|
* On purge of session history
|
|
*/
|
|
onPurgeSessionHistory: function() {
|
|
var _this = this;
|
|
_SessionFile.wipe();
|
|
// If the browser is shutting down, simply return after clearing the
|
|
// session data on disk as this notification fires after the
|
|
// quit-application notification so the browser is about to exit.
|
|
if (this._loadState == STATE_QUITTING)
|
|
return;
|
|
this._lastSessionState = null;
|
|
let openWindows = {};
|
|
this._forEachBrowserWindow(function(aWindow) {
|
|
Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
|
|
delete aTab.linkedBrowser.__SS_data;
|
|
delete aTab.linkedBrowser.__SS_tabStillLoading;
|
|
delete aTab.linkedBrowser.__SS_formDataSaved;
|
|
delete aTab.linkedBrowser.__SS_hostSchemeData;
|
|
if (aTab.linkedBrowser.__SS_restoreState)
|
|
this._resetTabRestoringState(aTab);
|
|
}, this);
|
|
openWindows[aWindow.__SSi] = true;
|
|
});
|
|
// also clear all data about closed tabs and windows
|
|
for (let ix in this._windows) {
|
|
if (ix in openWindows) {
|
|
this._windows[ix]._closedTabs = [];
|
|
}
|
|
else {
|
|
delete this._windows[ix];
|
|
delete this._internalWindows[ix];
|
|
}
|
|
}
|
|
// also clear all data about closed windows
|
|
this._closedWindows = [];
|
|
// give the tabbrowsers a chance to clear their histories first
|
|
var win = this._getMostRecentBrowserWindow();
|
|
if (win)
|
|
win.setTimeout(function() { _this.saveState(true); }, 0);
|
|
else if (this._loadState == STATE_RUNNING)
|
|
this.saveState(true);
|
|
// Delete the private browsing backed up state, if any
|
|
if ("_stateBackup" in this)
|
|
delete this._stateBackup;
|
|
|
|
this._clearRestoringWindows();
|
|
},
|
|
|
|
/**
|
|
* On purge of domain data
|
|
* @param aData
|
|
* String domain data
|
|
*/
|
|
onPurgeDomainData: function(aData) {
|
|
// does a session history entry contain a url for the given domain?
|
|
function containsDomain(aEntry) {
|
|
try {
|
|
if (this._getURIFromString(aEntry.url).host.hasRootDomain(aData))
|
|
return true;
|
|
}
|
|
catch (ex) { /* url had no host at all */ }
|
|
return aEntry.children && aEntry.children.some(containsDomain, this);
|
|
}
|
|
// remove all closed tabs containing a reference to the given domain
|
|
for (let ix in this._windows) {
|
|
let closedTabs = this._windows[ix]._closedTabs;
|
|
for (let i = closedTabs.length - 1; i >= 0; i--) {
|
|
if (closedTabs[i].state.entries.some(containsDomain, this))
|
|
closedTabs.splice(i, 1);
|
|
}
|
|
}
|
|
// remove all open & closed tabs containing a reference to the given
|
|
// domain in closed windows
|
|
for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) {
|
|
let closedTabs = this._closedWindows[ix]._closedTabs;
|
|
let openTabs = this._closedWindows[ix].tabs;
|
|
let openTabCount = openTabs.length;
|
|
for (let i = closedTabs.length - 1; i >= 0; i--)
|
|
if (closedTabs[i].state.entries.some(containsDomain, this))
|
|
closedTabs.splice(i, 1);
|
|
for (let j = openTabs.length - 1; j >= 0; j--) {
|
|
if (openTabs[j].entries.some(containsDomain, this)) {
|
|
openTabs.splice(j, 1);
|
|
if (this._closedWindows[ix].selected > j)
|
|
this._closedWindows[ix].selected--;
|
|
}
|
|
}
|
|
if (openTabs.length == 0) {
|
|
this._closedWindows.splice(ix, 1);
|
|
}
|
|
else if (openTabs.length != openTabCount) {
|
|
// Adjust the window's title if we removed an open tab
|
|
let selectedTab = openTabs[this._closedWindows[ix].selected - 1];
|
|
// some duplication from restoreHistory - make sure we get the correct title
|
|
let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1;
|
|
if (activeIndex >= selectedTab.entries.length)
|
|
activeIndex = selectedTab.entries.length - 1;
|
|
this._closedWindows[ix].title = selectedTab.entries[activeIndex].title;
|
|
}
|
|
}
|
|
if (this._loadState == STATE_RUNNING)
|
|
this.saveState(true);
|
|
|
|
this._clearRestoringWindows();
|
|
},
|
|
|
|
/**
|
|
* On preference change
|
|
* @param aData
|
|
* String preference changed
|
|
*/
|
|
onPrefChange: function(aData) {
|
|
switch (aData) {
|
|
// if the user decreases the max number of closed tabs they want
|
|
// preserved update our internal states to match that max
|
|
case "sessionstore.max_tabs_undo":
|
|
this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
|
|
for (let ix in this._windows) {
|
|
this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
|
|
}
|
|
break;
|
|
case "sessionstore.max_windows_undo":
|
|
this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
|
|
this._capClosedWindows();
|
|
break;
|
|
case "sessionstore.interval":
|
|
this._interval = this._prefBranch.getIntPref("sessionstore.interval");
|
|
// reset timer and save
|
|
if (this._saveTimer) {
|
|
this._saveTimer.cancel();
|
|
this._saveTimer = null;
|
|
}
|
|
this.saveStateDelayed(null, -1);
|
|
break;
|
|
case "sessionstore.resume_from_crash":
|
|
this._resume_from_crash = this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
|
|
// restore original resume_session_once preference if set in saveState
|
|
if (this._resume_session_once_on_shutdown != null) {
|
|
this._prefBranch.setBoolPref("sessionstore.resume_session_once",
|
|
this._resume_session_once_on_shutdown);
|
|
this._resume_session_once_on_shutdown = null;
|
|
}
|
|
// either create the file with crash recovery information or remove it
|
|
// (when _loadState is not STATE_RUNNING, that file is used for session resuming instead)
|
|
if (!this._resume_from_crash)
|
|
_SessionFile.wipe();
|
|
this.saveState(true);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* On timer callback
|
|
*/
|
|
onTimerCallback: function() {
|
|
this._saveTimer = null;
|
|
this.saveState();
|
|
},
|
|
|
|
/**
|
|
* set up listeners for a new tab
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aTab
|
|
* Tab reference
|
|
* @param aNoNotification
|
|
* bool Do not save state if we're updating an existing tab
|
|
*/
|
|
onTabAdd: function(aWindow, aTab, aNoNotification) {
|
|
let browser = aTab.linkedBrowser;
|
|
browser.addEventListener("load", this, true);
|
|
|
|
let mm = browser.messageManager;
|
|
MESSAGES.forEach(msg => mm.addMessageListener(msg, this));
|
|
|
|
if (!aNoNotification) {
|
|
this.saveStateDelayed(aWindow);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* remove listeners for a tab
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aTab
|
|
* Tab reference
|
|
* @param aNoNotification
|
|
* bool Do not save state if we're updating an existing tab
|
|
*/
|
|
onTabRemove: function(aWindow, aTab, aNoNotification) {
|
|
let browser = aTab.linkedBrowser;
|
|
browser.removeEventListener("load", this, true);
|
|
|
|
let mm = browser.messageManager;
|
|
MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
|
|
|
|
delete browser.__SS_data;
|
|
delete browser.__SS_tabStillLoading;
|
|
delete browser.__SS_formDataSaved;
|
|
delete browser.__SS_hostSchemeData;
|
|
|
|
// If this tab was in the middle of restoring or still needs to be restored,
|
|
// we need to reset that state. If the tab was restoring, we will attempt to
|
|
// restore the next tab.
|
|
let previousState = browser.__SS_restoreState;
|
|
if (previousState) {
|
|
this._resetTabRestoringState(aTab);
|
|
if (previousState == TAB_STATE_RESTORING)
|
|
this.restoreNextTab();
|
|
}
|
|
|
|
if (!aNoNotification) {
|
|
this.saveStateDelayed(aWindow);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* When a tab closes, collect its properties
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aTab
|
|
* Tab reference
|
|
*/
|
|
onTabClose: function(aWindow, aTab) {
|
|
// notify the tabbrowser that the tab state will be retrieved for the last time
|
|
// (so that extension authors can easily set data on soon-to-be-closed tabs)
|
|
var event = aWindow.document.createEvent("Events");
|
|
event.initEvent("SSTabClosing", true, false);
|
|
aTab.dispatchEvent(event);
|
|
|
|
// don't update our internal state if we don't have to
|
|
if (this._max_tabs_undo == 0) {
|
|
return;
|
|
}
|
|
|
|
// make sure that the tab related data is up-to-date
|
|
var tabState = this._collectTabData(aTab);
|
|
this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState);
|
|
|
|
// store closed-tab data for undo
|
|
if (this._shouldSaveTabState(tabState)) {
|
|
let tabTitle = aTab.label;
|
|
let tabbrowser = aWindow.gBrowser;
|
|
tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
|
|
|
|
this._windows[aWindow.__SSi]._closedTabs.unshift({
|
|
state: tabState,
|
|
title: tabTitle,
|
|
image: tabbrowser.getIcon(aTab),
|
|
pos: aTab._tPos
|
|
});
|
|
var length = this._windows[aWindow.__SSi]._closedTabs.length;
|
|
if (length > this._max_tabs_undo)
|
|
this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* When a tab loads, save state.
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aBrowser
|
|
* Browser reference
|
|
*/
|
|
onTabLoad: function(aWindow, aBrowser) {
|
|
// react on "load" and solitary "pageshow" events (the first "pageshow"
|
|
// following "load" is too late for deleting the data caches)
|
|
// It's possible to get a load event after calling stop on a browser (when
|
|
// overwriting tabs). We want to return early if the tab hasn't been restored yet.
|
|
if (aBrowser.__SS_restoreState &&
|
|
aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
|
|
return;
|
|
}
|
|
|
|
delete aBrowser.__SS_data;
|
|
delete aBrowser.__SS_tabStillLoading;
|
|
delete aBrowser.__SS_formDataSaved;
|
|
this.saveStateDelayed(aWindow);
|
|
|
|
},
|
|
|
|
/**
|
|
* Called when a browser sends the "input" notification
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aBrowser
|
|
* Browser reference
|
|
*/
|
|
onTabInput: function(aWindow, aBrowser) {
|
|
// deleting __SS_formDataSaved will cause us to recollect form data
|
|
delete aBrowser.__SS_formDataSaved;
|
|
|
|
this.saveStateDelayed(aWindow, 3000);
|
|
},
|
|
|
|
/**
|
|
* When a tab is selected, save session data
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
onTabSelect: function(aWindow) {
|
|
if (this._loadState == STATE_RUNNING) {
|
|
this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
|
|
|
|
let tab = aWindow.gBrowser.selectedTab;
|
|
// If __SS_restoreState is still on the browser and it is
|
|
// TAB_STATE_NEEDS_RESTORE, then then we haven't restored
|
|
// this tab yet. Explicitly call restoreTab to kick off the restore.
|
|
if (tab.linkedBrowser.__SS_restoreState &&
|
|
tab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
|
|
this.restoreTab(tab);
|
|
|
|
}
|
|
},
|
|
|
|
onTabShow: function(aWindow, aTab) {
|
|
// If the tab hasn't been restored yet, move it into the right bucket
|
|
if (aTab.linkedBrowser.__SS_restoreState &&
|
|
aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
|
|
TabRestoreQueue.hiddenToVisible(aTab);
|
|
|
|
// let's kick off tab restoration again to ensure this tab gets restored
|
|
// with "restore_hidden_tabs" == false (now that it has become visible)
|
|
this.restoreNextTab();
|
|
}
|
|
|
|
// Default delay of 2 seconds gives enough time to catch multiple TabShow
|
|
// events due to changing groups in Panorama.
|
|
this.saveStateDelayed(aWindow);
|
|
},
|
|
|
|
onTabHide: function(aWindow, aTab) {
|
|
// If the tab hasn't been restored yet, move it into the right bucket
|
|
if (aTab.linkedBrowser.__SS_restoreState &&
|
|
aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
|
|
TabRestoreQueue.visibleToHidden(aTab);
|
|
}
|
|
|
|
// Default delay of 2 seconds gives enough time to catch multiple TabHide
|
|
// events due to changing groups in Panorama.
|
|
this.saveStateDelayed(aWindow);
|
|
},
|
|
|
|
/* ........ nsISessionStore API .............. */
|
|
|
|
getBrowserState: function() {
|
|
return this._toJSONString(this._getCurrentState());
|
|
},
|
|
|
|
setBrowserState: function(aState) {
|
|
this._handleClosedWindows();
|
|
|
|
try {
|
|
var state = JSON.parse(aState);
|
|
}
|
|
catch (ex) { /* invalid state object - don't restore anything */ }
|
|
if (!state || !state.windows)
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
this._browserSetState = true;
|
|
|
|
// Make sure the priority queue is emptied out
|
|
this._resetRestoringState();
|
|
|
|
var window = this._getMostRecentBrowserWindow();
|
|
if (!window) {
|
|
this._restoreCount = 1;
|
|
this._openWindowWithState(state);
|
|
return;
|
|
}
|
|
|
|
// close all other browser windows
|
|
this._forEachBrowserWindow(function(aWindow) {
|
|
if (aWindow != window) {
|
|
aWindow.close();
|
|
this.onClose(aWindow);
|
|
}
|
|
});
|
|
|
|
// make sure closed window data isn't kept
|
|
this._closedWindows = [];
|
|
|
|
// determine how many windows are meant to be restored
|
|
this._restoreCount = state.windows ? state.windows.length : 0;
|
|
|
|
// restore to the given state
|
|
this.restoreWindow(window, state, true);
|
|
},
|
|
|
|
getWindowState: function(aWindow) {
|
|
if ("__SSi" in aWindow) {
|
|
return this._toJSONString(this._getWindowState(aWindow));
|
|
}
|
|
|
|
if (DyingWindowCache.has(aWindow)) {
|
|
let data = DyingWindowCache.get(aWindow);
|
|
return this._toJSONString({ windows: [data] });
|
|
}
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
},
|
|
|
|
setWindowState: function(aWindow, aState, aOverwrite) {
|
|
if (!aWindow.__SSi)
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
this.restoreWindow(aWindow, aState, aOverwrite);
|
|
},
|
|
|
|
getTabState: function(aTab) {
|
|
if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
var tabState = this._collectTabData(aTab);
|
|
|
|
var window = aTab.ownerDocument.defaultView;
|
|
this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState);
|
|
|
|
return this._toJSONString(tabState);
|
|
},
|
|
|
|
setTabState: function(aTab, aState) {
|
|
var tabState = JSON.parse(aState);
|
|
if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
var window = aTab.ownerDocument.defaultView;
|
|
this._setWindowStateBusy(window);
|
|
this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0);
|
|
},
|
|
|
|
duplicateTab: function(aWindow, aTab, aDelta) {
|
|
if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi ||
|
|
!aWindow.getBrowser)
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
var tabState = this._collectTabData(aTab, true);
|
|
var sourceWindow = aTab.ownerDocument.defaultView;
|
|
this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true);
|
|
tabState.index += aDelta;
|
|
tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
|
|
tabState.pinned = false;
|
|
|
|
this._setWindowStateBusy(aWindow);
|
|
let newTab = aTab == aWindow.gBrowser.selectedTab ?
|
|
aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) :
|
|
aWindow.gBrowser.addTab();
|
|
|
|
this.restoreHistoryPrecursor(aWindow, [newTab], [tabState], 0, 0, 0,
|
|
true /* Load this tab right away. */);
|
|
|
|
return newTab;
|
|
},
|
|
|
|
getClosedTabCount: function(aWindow) {
|
|
if ("__SSi" in aWindow) {
|
|
return this._windows[aWindow.__SSi]._closedTabs.length;
|
|
}
|
|
|
|
if (DyingWindowCache.has(aWindow)) {
|
|
return DyingWindowCache.get(aWindow)._closedTabs.length;
|
|
}
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
},
|
|
|
|
getClosedTabData: function(aWindow) {
|
|
if ("__SSi" in aWindow) {
|
|
return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs);
|
|
}
|
|
|
|
if (DyingWindowCache.has(aWindow)) {
|
|
let data = DyingWindowCache.get(aWindow);
|
|
return this._toJSONString(data._closedTabs);
|
|
}
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
},
|
|
|
|
undoCloseTab: function(aWindow, aIndex) {
|
|
if (!aWindow.__SSi)
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
var closedTabs = this._windows[aWindow.__SSi]._closedTabs;
|
|
|
|
// default to the most-recently closed tab
|
|
aIndex = aIndex || 0;
|
|
if (!(aIndex in closedTabs))
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
// fetch the data of closed tab, while removing it from the array
|
|
let closedTab = closedTabs.splice(aIndex, 1).shift();
|
|
let closedTabState = closedTab.state;
|
|
|
|
this._setWindowStateBusy(aWindow);
|
|
// create a new tab
|
|
let tabbrowser = aWindow.gBrowser;
|
|
let tab = tabbrowser.addTab();
|
|
|
|
// restore tab content
|
|
this.restoreHistoryPrecursor(aWindow, [tab], [closedTabState], 1, 0, 0);
|
|
|
|
// restore the tab's position
|
|
tabbrowser.moveTabTo(tab, closedTab.pos);
|
|
|
|
// focus the tab's content area (bug 342432)
|
|
tab.linkedBrowser.focus();
|
|
|
|
return tab;
|
|
},
|
|
|
|
forgetClosedTab: function(aWindow, aIndex) {
|
|
if (!aWindow.__SSi)
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
var closedTabs = this._windows[aWindow.__SSi]._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);
|
|
},
|
|
|
|
getClosedWindowCount: function() {
|
|
return this._closedWindows.length;
|
|
},
|
|
|
|
getClosedWindowData: function() {
|
|
return this._toJSONString(this._closedWindows);
|
|
},
|
|
|
|
undoCloseWindow: function(aIndex) {
|
|
if (!(aIndex in this._closedWindows))
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
// reopen the window
|
|
let state = { windows: this._closedWindows.splice(aIndex, 1) };
|
|
let window = this._openWindowWithState(state);
|
|
this.windowToFocus = window;
|
|
return window;
|
|
},
|
|
|
|
forgetClosedWindow: function(aIndex) {
|
|
// default to the most-recently closed window
|
|
aIndex = aIndex || 0;
|
|
if (!(aIndex in this._closedWindows))
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
|
|
// remove closed window from the array
|
|
this._closedWindows.splice(aIndex, 1);
|
|
},
|
|
|
|
getWindowValue: function(aWindow, aKey) {
|
|
if ("__SSi" in aWindow) {
|
|
var data = this._windows[aWindow.__SSi].extData || {};
|
|
return data[aKey] || "";
|
|
}
|
|
|
|
if (DyingWindowCache.has(aWindow)) {
|
|
let data = DyingWindowCache.get(aWindow).extData || {};
|
|
return data[aKey] || "";
|
|
}
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
},
|
|
|
|
setWindowValue: function(aWindow, aKey, aStringValue) {
|
|
if (aWindow.__SSi) {
|
|
if (!this._windows[aWindow.__SSi].extData) {
|
|
this._windows[aWindow.__SSi].extData = {};
|
|
}
|
|
this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
|
|
this.saveStateDelayed(aWindow);
|
|
}
|
|
else {
|
|
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
},
|
|
|
|
deleteWindowValue: function(aWindow, aKey) {
|
|
if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
|
|
this._windows[aWindow.__SSi].extData[aKey])
|
|
delete this._windows[aWindow.__SSi].extData[aKey];
|
|
},
|
|
|
|
getTabValue: function(aTab, aKey) {
|
|
let data = {};
|
|
if (aTab.__SS_extdata) {
|
|
data = aTab.__SS_extdata;
|
|
}
|
|
else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
|
|
// If the tab hasn't been fully restored, get the data from the to-be-restored data
|
|
data = aTab.linkedBrowser.__SS_data.extData;
|
|
}
|
|
return data[aKey] || "";
|
|
},
|
|
|
|
setTabValue: function(aTab, aKey, aStringValue) {
|
|
// If the tab hasn't been restored, then set the data there, otherwise we
|
|
// could lose newly added data.
|
|
let saveTo;
|
|
if (aTab.__SS_extdata) {
|
|
saveTo = aTab.__SS_extdata;
|
|
}
|
|
else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
|
|
saveTo = aTab.linkedBrowser.__SS_data.extData;
|
|
}
|
|
else {
|
|
aTab.__SS_extdata = {};
|
|
saveTo = aTab.__SS_extdata;
|
|
}
|
|
saveTo[aKey] = aStringValue;
|
|
this.saveStateDelayed(aTab.ownerDocument.defaultView);
|
|
},
|
|
|
|
deleteTabValue: function(aTab, aKey) {
|
|
// We want to make sure that if data is accessed early, we attempt to delete
|
|
// that data from __SS_data as well. Otherwise we'll throw in cases where
|
|
// data can be set or read.
|
|
let deleteFrom;
|
|
if (aTab.__SS_extdata) {
|
|
deleteFrom = aTab.__SS_extdata;
|
|
}
|
|
else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
|
|
deleteFrom = aTab.linkedBrowser.__SS_data.extData;
|
|
}
|
|
|
|
if (deleteFrom && deleteFrom[aKey])
|
|
delete deleteFrom[aKey];
|
|
},
|
|
|
|
persistTabAttribute: function(aName) {
|
|
if (TabAttributes.persist(aName)) {
|
|
this.saveStateDelayed();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Restores the session state stored in _lastSessionState. This will attempt
|
|
* to merge data into the current session. If a window was opened at startup
|
|
* with pinned tab(s), then the remaining data from the previous session for
|
|
* that window will be opened into that winddow. Otherwise new windows will
|
|
* be opened.
|
|
*/
|
|
restoreLastSession: function() {
|
|
// Use the public getter since it also checks PB mode
|
|
if (!this.canRestoreLastSession)
|
|
throw (Components.returnCode = Cr.NS_ERROR_FAILURE);
|
|
|
|
// First collect each window with its id...
|
|
let windows = {};
|
|
this._forEachBrowserWindow(function(aWindow) {
|
|
if (aWindow.__SS_lastSessionWindowID)
|
|
windows[aWindow.__SS_lastSessionWindowID] = aWindow;
|
|
});
|
|
|
|
let lastSessionState = this._lastSessionState;
|
|
|
|
// This shouldn't ever be the case...
|
|
if (!lastSessionState.windows.length)
|
|
throw (Components.returnCode = Cr.NS_ERROR_UNEXPECTED);
|
|
|
|
// We're technically doing a restore, so set things up so we send the
|
|
// notification when we're done. We want to send "sessionstore-browser-state-restored".
|
|
this._restoreCount = lastSessionState.windows.length;
|
|
this._browserSetState = true;
|
|
|
|
// We want to re-use the last opened window instead of opening a new one in
|
|
// the case where it's "empty" and not associated with a window in the session.
|
|
// We will do more processing via _prepWindowToRestoreInto if we need to use
|
|
// the lastWindow.
|
|
let lastWindow = this._getMostRecentBrowserWindow();
|
|
let canUseLastWindow = lastWindow &&
|
|
!lastWindow.__SS_lastSessionWindowID;
|
|
|
|
// Restore into windows or open new ones as needed.
|
|
for (let i = 0; i < lastSessionState.windows.length; i++) {
|
|
let winState = lastSessionState.windows[i];
|
|
let lastSessionWindowID = winState.__lastSessionWindowID;
|
|
// delete lastSessionWindowID so we don't add that to the window again
|
|
delete winState.__lastSessionWindowID;
|
|
|
|
// See if we can use an open window. First try one that is associated with
|
|
// the state we're trying to restore and then fallback to the last selected
|
|
// window.
|
|
let windowToUse = windows[lastSessionWindowID];
|
|
if (!windowToUse && canUseLastWindow) {
|
|
windowToUse = lastWindow;
|
|
canUseLastWindow = false;
|
|
}
|
|
|
|
let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse);
|
|
|
|
// If there's a window already open that we can restore into, use that
|
|
if (canUseWindow) {
|
|
// Since we're not overwriting existing tabs, we want to merge _closedTabs,
|
|
// putting existing ones first. Then make sure we're respecting the max pref.
|
|
if (winState._closedTabs && winState._closedTabs.length) {
|
|
let curWinState = this._windows[windowToUse.__SSi];
|
|
curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
|
|
curWinState._closedTabs.splice(this._prefBranch.getIntPref("sessionstore.max_tabs_undo"), curWinState._closedTabs.length);
|
|
}
|
|
|
|
// Restore into that window - pretend it's a followup since we'll already
|
|
// have a focused window.
|
|
//XXXzpao This is going to merge extData together (taking what was in
|
|
// winState over what is in the window already. The hack we have
|
|
// in _preWindowToRestoreInto will prevent most (all?) Panorama
|
|
// weirdness but we will still merge other extData.
|
|
// Bug 588217 should make this go away by merging the group data.
|
|
this.restoreWindow(windowToUse, { windows: [winState] }, canOverwriteTabs, true);
|
|
}
|
|
else {
|
|
this._openWindowWithState({ windows: [winState] });
|
|
}
|
|
}
|
|
|
|
// Merge closed windows from this session with ones from last session
|
|
if (lastSessionState._closedWindows) {
|
|
this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
|
|
this._capClosedWindows();
|
|
}
|
|
|
|
#ifdef MOZ_DEVTOOLS
|
|
// Scratchpad
|
|
if (lastSessionState.scratchpads) {
|
|
ScratchpadManager.restoreSession(lastSessionState.scratchpads);
|
|
}
|
|
|
|
// The Browser Console
|
|
if (lastSessionState.browserConsole) {
|
|
HUDService.restoreBrowserConsoleSession();
|
|
}
|
|
#endif
|
|
|
|
// Set data that persists between sessions
|
|
this._recentCrashes = lastSessionState.session &&
|
|
lastSessionState.session.recentCrashes || 0;
|
|
this._sessionStartTime = lastSessionState.session &&
|
|
lastSessionState.session.startTime ||
|
|
this._sessionStartTime;
|
|
|
|
this._lastSessionState = null;
|
|
},
|
|
|
|
/**
|
|
* See if aWindow is usable for use when restoring a previous session via
|
|
* restoreLastSession. If usable, prepare it for use.
|
|
*
|
|
* @param aWindow
|
|
* the window to inspect & prepare
|
|
* @returns [canUseWindow, canOverwriteTabs]
|
|
* canUseWindow: can the window be used to restore into
|
|
* canOverwriteTabs: all of the current tabs are home pages and we
|
|
* can overwrite them
|
|
*/
|
|
_prepWindowToRestoreInto: function(aWindow) {
|
|
if (!aWindow)
|
|
return [false, false];
|
|
|
|
let event = aWindow.document.createEvent("Events");
|
|
event.initEvent("SSRestoreIntoWindow", true, true);
|
|
|
|
// Check if we can use the window.
|
|
if (!aWindow.dispatchEvent(event))
|
|
return [false, false];
|
|
|
|
// We might be able to overwrite the existing tabs instead of just adding
|
|
// the previous session's tabs to the end. This will be set if possible.
|
|
let canOverwriteTabs = false;
|
|
|
|
// Look at the open tabs in comparison to home pages. If all the tabs are
|
|
// home pages then we'll end up overwriting all of them. Otherwise we'll
|
|
// just close the tabs that match home pages. Tabs with the about:blank
|
|
// URI will always be overwritten.
|
|
let homePages = ["about:blank"];
|
|
let removableTabs = [];
|
|
let tabbrowser = aWindow.gBrowser;
|
|
let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs;
|
|
let startupPref = this._prefBranch.getIntPref("startup.page");
|
|
if (startupPref == 1)
|
|
homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|"));
|
|
|
|
for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) {
|
|
let tab = tabbrowser.tabs[i];
|
|
if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) {
|
|
removableTabs.push(tab);
|
|
}
|
|
}
|
|
|
|
if (tabbrowser.tabs.length == removableTabs.length) {
|
|
canOverwriteTabs = true;
|
|
}
|
|
else {
|
|
// If we're not overwriting all of the tabs, then close the home tabs.
|
|
for (let i = removableTabs.length - 1; i >= 0; i--) {
|
|
tabbrowser.removeTab(removableTabs.pop(), { animate: false });
|
|
}
|
|
}
|
|
|
|
return [true, canOverwriteTabs];
|
|
},
|
|
|
|
/* ........ Saving Functionality .............. */
|
|
|
|
/**
|
|
* Store all session data for a window
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
_saveWindowHistory: function(aWindow) {
|
|
var tabbrowser = aWindow.gBrowser;
|
|
var tabs = tabbrowser.tabs;
|
|
var tabsData = this._windows[aWindow.__SSi].tabs = [];
|
|
|
|
for (var i = 0; i < tabs.length; i++)
|
|
tabsData.push(this._collectTabData(tabs[i]));
|
|
|
|
this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1;
|
|
},
|
|
|
|
/**
|
|
* Collect data related to a single tab
|
|
* @param aTab
|
|
* tabbrowser tab
|
|
* @param aFullData
|
|
* always return privacy sensitive data (use with care)
|
|
* @returns object
|
|
*/
|
|
_collectTabData: function(aTab, aFullData) {
|
|
var tabData = { entries: [], lastAccessed: aTab.lastAccessed };
|
|
var browser = aTab.linkedBrowser;
|
|
|
|
if (!browser || !browser.currentURI)
|
|
// can happen when calling this function right after .addTab()
|
|
return tabData;
|
|
else if (browser.__SS_data && browser.__SS_tabStillLoading) {
|
|
// use the data to be restored when the tab hasn't been completely loaded
|
|
tabData = browser.__SS_data;
|
|
if (aTab.pinned)
|
|
tabData.pinned = true;
|
|
else
|
|
delete tabData.pinned;
|
|
tabData.hidden = aTab.hidden;
|
|
|
|
// If __SS_extdata is set then we'll use that since it might be newer.
|
|
if (aTab.__SS_extdata)
|
|
tabData.extData = aTab.__SS_extdata;
|
|
// If it exists but is empty then a key was likely deleted. In that case just
|
|
// delete extData.
|
|
if (tabData.extData && !Object.keys(tabData.extData).length)
|
|
delete tabData.extData;
|
|
return tabData;
|
|
}
|
|
|
|
var history = null;
|
|
try {
|
|
history = browser.sessionHistory;
|
|
}
|
|
catch (ex) { } // this could happen if we catch a tab during (de)initialization
|
|
|
|
// Limit number of back/forward button history entries to save
|
|
let oldest, newest;
|
|
let maxSerializeBack = this._prefBranch.getIntPref("sessionstore.max_serialize_back");
|
|
if (maxSerializeBack >= 0) {
|
|
oldest = Math.max(0, history.index - maxSerializeBack);
|
|
} else { // History.getEntryAtIndex(0, ...) is the oldest.
|
|
oldest = 0;
|
|
}
|
|
let maxSerializeFwd = this._prefBranch.getIntPref("sessionstore.max_serialize_forward");
|
|
if (maxSerializeFwd >= 0) {
|
|
newest = Math.min(history.count - 1, history.index + maxSerializeFwd);
|
|
} else { // History.getEntryAtIndex(history.count - 1, ...) is the newest.
|
|
newest = history.count - 1;
|
|
}
|
|
|
|
// XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse
|
|
// data even when we shouldn't (e.g. Back, different anchor)
|
|
// Warning: this is required to save form data and scrolling position!
|
|
if (history && browser.__SS_data &&
|
|
browser.__SS_data.entries[history.index] &&
|
|
browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
|
|
history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
|
|
try {
|
|
tabData.entries = browser.__SS_data.entries.slice(oldest, newest + 1);
|
|
}
|
|
catch (ex) {
|
|
// No errors are expected above, but we use try-catch to keep sessionstore.js safe
|
|
NS_ASSERT(false, "SessionStore failed to slice history from browser.__SS_data");
|
|
}
|
|
|
|
// Set the one-based index of the currently active tab, ensuring it isn't out of bounds
|
|
tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length);
|
|
}
|
|
else if (history && history.count > 0) {
|
|
browser.__SS_hostSchemeData = [];
|
|
try {
|
|
for (var j = oldest; j <= newest; j++) {
|
|
let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
|
|
aFullData, aTab.pinned, browser.__SS_hostSchemeData);
|
|
tabData.entries.push(entry);
|
|
}
|
|
}
|
|
catch (ex) {
|
|
// In some cases, getEntryAtIndex will throw. This seems to be due to
|
|
// history.count being higher than it should be. By doing this in a
|
|
// try-catch, we'll update history to where it breaks, assert for
|
|
// non-release builds, and still save sessionstore.js.
|
|
NS_ASSERT(false, "SessionStore failed gathering complete history " +
|
|
"for the focused window/tab. See bug 669196.");
|
|
}
|
|
|
|
// Set the one-based index of the currently active tab, ensuring it isn't out of bounds
|
|
tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length);
|
|
|
|
// make sure not to cache privacy sensitive data which shouldn't get out
|
|
if (!aFullData)
|
|
browser.__SS_data = tabData;
|
|
}
|
|
else if (browser.currentURI.spec != "about:blank" ||
|
|
browser.contentDocument.body.hasChildNodes()) {
|
|
tabData.entries[0] = { url: browser.currentURI.spec };
|
|
tabData.index = 1;
|
|
}
|
|
|
|
// If there is a userTypedValue set, then either the user has typed something
|
|
// in the URL bar, or a new tab was opened with a URI to load. userTypedClear
|
|
// is used to indicate whether the tab was in some sort of loading state with
|
|
// userTypedValue.
|
|
if (browser.userTypedValue) {
|
|
tabData.userTypedValue = browser.userTypedValue;
|
|
// We always used to keep track of the loading state as an integer, where
|
|
// '0' indicated the user had typed since the last load (or no load was
|
|
// ongoing), and any positive value indicated we had started a load since
|
|
// the last time the user typed in the URL bar. Mimic this to keep the
|
|
// session store representation in sync, even though we now represent this
|
|
// more explicitly:
|
|
tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() ? 1 : 0;
|
|
} else {
|
|
delete tabData.userTypedValue;
|
|
delete tabData.userTypedClear;
|
|
}
|
|
|
|
if (aTab.pinned)
|
|
tabData.pinned = true;
|
|
else
|
|
delete tabData.pinned;
|
|
tabData.hidden = aTab.hidden;
|
|
|
|
var disallow = [];
|
|
for (let cap of gDocShellCapabilities(browser.docShell))
|
|
if (!browser.docShell["allow" + cap])
|
|
disallow.push(cap);
|
|
if (disallow.length > 0)
|
|
tabData.disallow = disallow.join(",");
|
|
else if (tabData.disallow)
|
|
delete tabData.disallow;
|
|
|
|
// Save tab attributes.
|
|
tabData.attributes = TabAttributes.get(aTab);
|
|
|
|
// Store the tab icon.
|
|
let tabbrowser = aTab.ownerDocument.defaultView.gBrowser;
|
|
tabData.image = tabbrowser.getIcon(aTab);
|
|
|
|
if (aTab.__SS_extdata)
|
|
tabData.extData = aTab.__SS_extdata;
|
|
else if (tabData.extData)
|
|
delete tabData.extData;
|
|
|
|
if (history && browser.docShell instanceof Ci.nsIDocShell) {
|
|
let storageData = SessionStorage.serialize(browser.docShell, aFullData)
|
|
if (Object.keys(storageData).length)
|
|
tabData.storage = storageData;
|
|
}
|
|
|
|
return tabData;
|
|
},
|
|
|
|
/**
|
|
* Get an object that is a serialized representation of a History entry
|
|
* Used for data storage
|
|
* @param aEntry
|
|
* nsISHEntry instance
|
|
* @param aFullData
|
|
* always return privacy sensitive data (use with care)
|
|
* @param aIsPinned
|
|
* the tab is pinned and should be treated differently for privacy
|
|
* @param aHostSchemeData
|
|
* an array of objects with host & scheme keys
|
|
* @returns object
|
|
*/
|
|
_serializeHistoryEntry:
|
|
function(aEntry, aFullData, aIsPinned, aHostSchemeData) {
|
|
var entry = { url: aEntry.URI.spec };
|
|
|
|
try {
|
|
// throwing is expensive, we know that about: pages will throw
|
|
if (entry.url.indexOf("about:") != 0)
|
|
aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme });
|
|
}
|
|
catch (ex) {
|
|
// We just won't attempt to get cookies for this entry.
|
|
}
|
|
|
|
if (aEntry.title && aEntry.title != entry.url) {
|
|
entry.title = aEntry.title;
|
|
}
|
|
if (aEntry.isSubFrame) {
|
|
entry.subframe = true;
|
|
}
|
|
if (!(aEntry instanceof Ci.nsISHEntry)) {
|
|
return entry;
|
|
}
|
|
|
|
var cacheKey = aEntry.cacheKey;
|
|
if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
|
|
cacheKey.data != 0) {
|
|
// XXXbz would be better to have cache keys implement
|
|
// nsISerializable or something.
|
|
entry.cacheKey = cacheKey.data;
|
|
}
|
|
entry.ID = aEntry.ID;
|
|
entry.docshellID = aEntry.docshellID;
|
|
|
|
if (aEntry.referrerURI)
|
|
entry.referrer = aEntry.referrerURI.spec;
|
|
|
|
if (aEntry.srcdocData)
|
|
entry.srcdocData = aEntry.srcdocData;
|
|
|
|
if (aEntry.isSrcdocEntry)
|
|
entry.isSrcdocEntry = aEntry.isSrcdocEntry;
|
|
|
|
if (aEntry.contentType)
|
|
entry.contentType = aEntry.contentType;
|
|
|
|
var x = {}, y = {};
|
|
aEntry.getScrollPosition(x, y);
|
|
if (x.value != 0 || y.value != 0)
|
|
entry.scroll = x.value + "," + y.value;
|
|
|
|
try {
|
|
var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata");
|
|
if (aEntry.postData && (aFullData || prefPostdata &&
|
|
this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) {
|
|
aEntry.postData.QueryInterface(Ci.nsISeekableStream).
|
|
seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
|
var stream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
createInstance(Ci.nsIBinaryInputStream);
|
|
stream.setInputStream(aEntry.postData);
|
|
var postBytes = stream.readByteArray(stream.available());
|
|
var postdata = String.fromCharCode.apply(null, postBytes);
|
|
if (aFullData || prefPostdata == -1 ||
|
|
postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <=
|
|
prefPostdata) {
|
|
// We can stop doing base64 encoding once our serialization into JSON
|
|
// is guaranteed to handle all chars in strings, including embedded
|
|
// nulls.
|
|
entry.postdata_b64 = btoa(postdata);
|
|
}
|
|
}
|
|
}
|
|
catch (ex) { debug(ex); } // POSTDATA is tricky - especially since some extensions don't get it right
|
|
|
|
if (aEntry.triggeringPrincipal) {
|
|
// Not catching anything specific here, just possible errors
|
|
// from writeCompoundObject and the like.
|
|
try {
|
|
var binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
|
|
createInstance(Ci.nsIObjectOutputStream);
|
|
var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
|
pipe.init(false, false, 0, 0xffffffff, null);
|
|
binaryStream.setOutputStream(pipe.outputStream);
|
|
binaryStream.writeCompoundObject(aEntry.triggeringPrincipal, Ci.nsIPrincipal, true);
|
|
binaryStream.close();
|
|
|
|
// Now we want to read the data from the pipe's input end and encode it.
|
|
var scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
createInstance(Ci.nsIBinaryInputStream);
|
|
scriptableStream.setInputStream(pipe.inputStream);
|
|
var triggeringPrincipalBytes =
|
|
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.triggeringPrincipal_b64 = btoa(String.fromCharCode.apply(null, triggeringPrincipalBytes));
|
|
}
|
|
catch (ex) { debug(ex); }
|
|
}
|
|
|
|
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 (var i = 0; i < aEntry.childCount; i++) {
|
|
var child = aEntry.GetChildAt(i);
|
|
|
|
if (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, aFullData,
|
|
aIsPinned, aHostSchemeData));
|
|
}
|
|
}
|
|
|
|
if (children.length)
|
|
entry.children = children;
|
|
}
|
|
|
|
return entry;
|
|
},
|
|
|
|
/**
|
|
* go through all tabs and store the current scroll positions
|
|
* and innerHTML content of WYSIWYG editors
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
_updateTextAndScrollData: function(aWindow) {
|
|
var browsers = aWindow.gBrowser.browsers;
|
|
this._windows[aWindow.__SSi].tabs.forEach(function(tabData, i) {
|
|
try {
|
|
this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData);
|
|
}
|
|
catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time)
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* go through all frames and store the current scroll positions
|
|
* and innerHTML content of WYSIWYG editors
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aBrowser
|
|
* single browser reference
|
|
* @param aTabData
|
|
* tabData object to add the information to
|
|
* @param aFullData
|
|
* always return privacy sensitive data (use with care)
|
|
*/
|
|
_updateTextAndScrollDataForTab:
|
|
function(aWindow, aBrowser, aTabData, aFullData) {
|
|
// we shouldn't update data for incompletely initialized tabs
|
|
if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading)
|
|
return;
|
|
|
|
var tabIndex = (aTabData.index || aTabData.entries.length) - 1;
|
|
// entry data needn't exist for tabs just initialized with an incomplete session state
|
|
if (!aTabData.entries[tabIndex])
|
|
return;
|
|
|
|
let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
|
|
this._getSelectedPageStyle(aBrowser.contentWindow);
|
|
if (selectedPageStyle)
|
|
aTabData.pageStyle = selectedPageStyle;
|
|
else if (aTabData.pageStyle)
|
|
delete aTabData.pageStyle;
|
|
|
|
this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow,
|
|
aTabData.entries[tabIndex],
|
|
!aBrowser.__SS_formDataSaved, aFullData,
|
|
!!aTabData.pinned);
|
|
aBrowser.__SS_formDataSaved = true;
|
|
if (aBrowser.currentURI.spec == "about:config")
|
|
aTabData.entries[tabIndex].formdata = {
|
|
id: {
|
|
"textbox": aBrowser.contentDocument.getElementById("textbox").value
|
|
},
|
|
xpath: {}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* go through all subframes and store all form data, the current
|
|
* scroll positions and innerHTML content of WYSIWYG editors
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aContent
|
|
* frame reference
|
|
* @param aData
|
|
* part of a tabData object to add the information to
|
|
* @param aUpdateFormData
|
|
* update all form data for this tab
|
|
* @param aFullData
|
|
* always return privacy sensitive data (use with care)
|
|
* @param aIsPinned
|
|
* the tab is pinned and should be treated differently for privacy
|
|
*/
|
|
_updateTextAndScrollDataForFrame:
|
|
function(aWindow, aContent, aData,
|
|
aUpdateFormData, aFullData, aIsPinned) {
|
|
for (var i = 0; i < aContent.frames.length; i++) {
|
|
if (aData.children && aData.children[i])
|
|
this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i],
|
|
aData.children[i], aUpdateFormData,
|
|
aFullData, aIsPinned);
|
|
}
|
|
var isHTTPS = this._getURIFromString((aContent.parent || aContent).
|
|
document.location.href).schemeIs("https");
|
|
let isAboutSR = aContent.top.document.location.href == "about:sessionrestore";
|
|
if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
|
|
if (aFullData || aUpdateFormData) {
|
|
let formData = DocumentUtils.getFormData(aContent.document);
|
|
|
|
// We want to avoid saving data for about:sessionrestore as a string.
|
|
// Since it's stored in the form as stringified JSON, stringifying further
|
|
// causes an explosion of escape characters. cf. bug 467409
|
|
if (formData && isAboutSR) {
|
|
formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
|
|
}
|
|
|
|
if (Object.keys(formData.id).length ||
|
|
Object.keys(formData.xpath).length) {
|
|
aData.formdata = formData;
|
|
} else if (aData.formdata) {
|
|
delete aData.formdata;
|
|
}
|
|
}
|
|
|
|
// designMode is undefined e.g. for XUL documents (as about:config)
|
|
if ((aContent.document.designMode || "") == "on" && aContent.document.body)
|
|
aData.innerHTML = aContent.document.body.innerHTML;
|
|
}
|
|
|
|
// get scroll position from nsIDOMWindowUtils, since it allows avoiding a
|
|
// flush of layout
|
|
let domWindowUtils = aContent.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
let scrollX = {}, scrollY = {};
|
|
domWindowUtils.getScrollXY(false, scrollX, scrollY);
|
|
aData.scroll = scrollX.value + "," + scrollY.value;
|
|
},
|
|
|
|
/**
|
|
* determine the title of the currently enabled style sheet (if any)
|
|
* and recurse through the frameset if necessary
|
|
* @param aContent is a frame reference
|
|
* @returns the title style sheet determined to be enabled (empty string if none)
|
|
*/
|
|
_getSelectedPageStyle: function(aContent) {
|
|
const forScreen = /(?:^|,)\s*(?:all|screen)\s*(?:,|$)/i;
|
|
for (let i = 0; i < aContent.document.styleSheets.length; i++) {
|
|
let ss = aContent.document.styleSheets[i];
|
|
let media = ss.media.mediaText;
|
|
if (!ss.disabled && ss.title && (!media || forScreen.test(media)))
|
|
return ss.title
|
|
}
|
|
for (let i = 0; i < aContent.frames.length; i++) {
|
|
let selectedPageStyle = this._getSelectedPageStyle(aContent.frames[i]);
|
|
if (selectedPageStyle)
|
|
return selectedPageStyle;
|
|
}
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* extract the base domain from a history entry and its children
|
|
* @param aEntry
|
|
* the history entry, serialized
|
|
* @param aHosts
|
|
* the hash that will be used to store hosts eg, { hostname: true }
|
|
* @param aCheckPrivacy
|
|
* should we check the privacy level for https
|
|
* @param aIsPinned
|
|
* is the entry we're evaluating for a pinned tab; used only if
|
|
* aCheckPrivacy
|
|
*/
|
|
_extractHostsForCookiesFromEntry:
|
|
function(aEntry, aHosts, aCheckPrivacy, aIsPinned) {
|
|
|
|
let host = aEntry._host,
|
|
scheme = aEntry._scheme;
|
|
|
|
// If host & scheme aren't defined, then we are likely here in the startup
|
|
// process via _splitCookiesFromWindow. In that case, we'll turn aEntry.url
|
|
// into an nsIURI and get host/scheme from that. This will throw for about:
|
|
// urls in which case we don't need to do anything.
|
|
if (!host && !scheme) {
|
|
try {
|
|
let uri = this._getURIFromString(aEntry.url);
|
|
host = uri.host;
|
|
scheme = uri.scheme;
|
|
this._extractHostsForCookiesFromHostScheme(host, scheme, aHosts, aCheckPrivacy, aIsPinned);
|
|
}
|
|
catch(ex) { }
|
|
}
|
|
|
|
if (aEntry.children) {
|
|
aEntry.children.forEach(function(entry) {
|
|
this._extractHostsForCookiesFromEntry(entry, aHosts, aCheckPrivacy, aIsPinned);
|
|
}, this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* extract the base domain from a host & scheme
|
|
* @param aHost
|
|
* the host of a uri (usually via nsIURI.host)
|
|
* @param aScheme
|
|
* the scheme of a uri (usually via nsIURI.scheme)
|
|
* @param aHosts
|
|
* the hash that will be used to store hosts eg, { hostname: true }
|
|
* @param aCheckPrivacy
|
|
* should we check the privacy level for https
|
|
* @param aIsPinned
|
|
* is the entry we're evaluating for a pinned tab; used only if
|
|
* aCheckPrivacy
|
|
*/
|
|
_extractHostsForCookiesFromHostScheme:
|
|
function(aHost, aScheme, aHosts, aCheckPrivacy, aIsPinned) {
|
|
// host and scheme may not be set (for about: urls for example), in which
|
|
// case testing scheme will be sufficient.
|
|
if (/https?/.test(aScheme) && !aHosts[aHost] &&
|
|
(!aCheckPrivacy ||
|
|
this.checkPrivacyLevel(aScheme == "https", aIsPinned))) {
|
|
// By setting this to true or false, we can determine when looking at
|
|
// the host in _updateCookies if we should check for privacy.
|
|
aHosts[aHost] = aIsPinned;
|
|
}
|
|
else if (aScheme == "file") {
|
|
aHosts[aHost] = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* store all hosts for a URL
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
_updateCookieHosts: function(aWindow) {
|
|
var hosts = this._internalWindows[aWindow.__SSi].hosts = {};
|
|
|
|
// Since _updateCookiesHosts is only ever called for open windows during a
|
|
// session, we can call into _extractHostsForCookiesFromHostScheme directly
|
|
// using data that is attached to each browser.
|
|
for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) {
|
|
let tab = aWindow.gBrowser.tabs[i];
|
|
let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
|
|
for (let j = 0; j < hostSchemeData.length; j++) {
|
|
this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
|
|
hostSchemeData[j].scheme,
|
|
hosts, true, tab.pinned);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Serialize cookie data
|
|
* @param aWindows
|
|
* JS object containing window data references
|
|
* { id: winData, etc. }
|
|
*/
|
|
_updateCookies: function(aWindows) {
|
|
function addCookieToHash(aHash, aHost, aPath, aName, aCookie) {
|
|
// lazily build up a 3-dimensional hash, with
|
|
// aHost, aPath, and aName as keys
|
|
if (!aHash[aHost])
|
|
aHash[aHost] = {};
|
|
if (!aHash[aHost][aPath])
|
|
aHash[aHost][aPath] = {};
|
|
aHash[aHost][aPath][aName] = aCookie;
|
|
}
|
|
|
|
var jscookies = {};
|
|
var _this = this;
|
|
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision
|
|
var MAX_EXPIRY = Math.pow(2, 62);
|
|
|
|
for (let [id, window] in Iterator(aWindows)) {
|
|
window.cookies = [];
|
|
let internalWindow = this._internalWindows[id];
|
|
if (!internalWindow.hosts)
|
|
return;
|
|
for (var [host, isPinned] in Iterator(internalWindow.hosts)) {
|
|
let list;
|
|
try {
|
|
list = Services.cookies.getCookiesFromHost(host, {});
|
|
}
|
|
catch (ex) {
|
|
debug("getCookiesFromHost failed. Host: " + host);
|
|
}
|
|
while (list && list.hasMoreElements()) {
|
|
var cookie = list.getNext().QueryInterface(Ci.nsICookie2);
|
|
// window._hosts will only have hosts with the right privacy rules,
|
|
// so there is no need to do anything special with this call to
|
|
// checkPrivacyLevel.
|
|
if (cookie.isSession && _this.checkPrivacyLevel(cookie.isSecure, isPinned)) {
|
|
// use the cookie's host, path, and name as keys into a hash,
|
|
// to make sure we serialize each cookie only once
|
|
if (!(cookie.host in jscookies &&
|
|
cookie.path in jscookies[cookie.host] &&
|
|
cookie.name in jscookies[cookie.host][cookie.path])) {
|
|
var jscookie = { "host": cookie.host, "value": cookie.value };
|
|
// only add attributes with non-default values (saving a few bits)
|
|
if (cookie.path) jscookie.path = cookie.path;
|
|
if (cookie.name) jscookie.name = cookie.name;
|
|
if (cookie.isSecure) jscookie.secure = true;
|
|
if (cookie.isHttpOnly) jscookie.httponly = true;
|
|
if (cookie.expiry < MAX_EXPIRY) jscookie.expiry = cookie.expiry;
|
|
|
|
addCookieToHash(jscookies, cookie.host, cookie.path, cookie.name, jscookie);
|
|
}
|
|
window.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't include empty cookie sections
|
|
if (!window.cookies.length)
|
|
delete window.cookies;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Store window dimensions, visibility, sidebar
|
|
* @param aWindow
|
|
* Window reference
|
|
*/
|
|
_updateWindowFeatures: function(aWindow) {
|
|
var winData = this._windows[aWindow.__SSi];
|
|
|
|
WINDOW_ATTRIBUTES.forEach(function(aAttr) {
|
|
winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
|
|
}, this);
|
|
|
|
var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) {
|
|
return aWindow[aItem] && !aWindow[aItem].visible;
|
|
});
|
|
if (hidden.length != 0)
|
|
winData.hidden = hidden.join(",");
|
|
else if (winData.hidden)
|
|
delete winData.hidden;
|
|
|
|
var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand");
|
|
if (sidebar)
|
|
winData.sidebar = sidebar;
|
|
else if (winData.sidebar)
|
|
delete winData.sidebar;
|
|
},
|
|
|
|
/**
|
|
* gather session data as object
|
|
* @param aUpdateAll
|
|
* Bool update all windows
|
|
* @param aPinnedOnly
|
|
* Bool collect pinned tabs only
|
|
* @returns object
|
|
*/
|
|
_getCurrentState: function(aUpdateAll, aPinnedOnly) {
|
|
this._handleClosedWindows();
|
|
|
|
var activeWindow = this._getMostRecentBrowserWindow();
|
|
|
|
if (this._loadState == STATE_RUNNING) {
|
|
// update the data for all windows with activities since the last save operation
|
|
this._forEachBrowserWindow(function(aWindow) {
|
|
if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore
|
|
return;
|
|
if (aUpdateAll || this._dirtyWindows[aWindow.__SSi] || aWindow == activeWindow) {
|
|
this._collectWindowData(aWindow);
|
|
}
|
|
else { // always update the window features (whose change alone never triggers a save operation)
|
|
this._updateWindowFeatures(aWindow);
|
|
}
|
|
});
|
|
this._dirtyWindows = [];
|
|
}
|
|
|
|
// collect the data for all windows
|
|
var total = [], windows = {}, ids = [];
|
|
var nonPopupCount = 0;
|
|
var ix;
|
|
for (ix in this._windows) {
|
|
if (this._windows[ix]._restoring) // window data is still in _statesToRestore
|
|
continue;
|
|
total.push(this._windows[ix]);
|
|
ids.push(ix);
|
|
windows[ix] = this._windows[ix];
|
|
if (!this._windows[ix].isPopup)
|
|
nonPopupCount++;
|
|
}
|
|
this._updateCookies(windows);
|
|
|
|
// collect the data for all windows yet to be restored
|
|
for (ix in this._statesToRestore) {
|
|
for each (let winData in this._statesToRestore[ix].windows) {
|
|
total.push(winData);
|
|
if (!winData.isPopup)
|
|
nonPopupCount++;
|
|
}
|
|
}
|
|
|
|
// shallow copy this._closedWindows to preserve current state
|
|
let lastClosedWindowsCopy = this._closedWindows.slice();
|
|
|
|
#ifndef XP_MACOSX
|
|
// If no non-popup browser window remains open, return the state of the last
|
|
// closed window(s). We only want to do this when we're actually "ending"
|
|
// the session.
|
|
//XXXzpao We should do this for _restoreLastWindow == true, but that has
|
|
// its own check for popups. c.f. bug 597619
|
|
if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 &&
|
|
this._loadState == STATE_QUITTING) {
|
|
// prepend the last non-popup browser window, so that if the user loads more tabs
|
|
// at startup we don't accidentally add them to a popup window
|
|
do {
|
|
total.unshift(lastClosedWindowsCopy.shift())
|
|
} while (total[0].isPopup && lastClosedWindowsCopy.length > 0)
|
|
}
|
|
#endif
|
|
|
|
if (aPinnedOnly) {
|
|
// perform a deep copy so that existing session variables are not changed.
|
|
total = JSON.parse(this._toJSONString(total));
|
|
total = total.filter(function(win) {
|
|
win.tabs = win.tabs.filter(function(tab) tab.pinned);
|
|
// remove closed tabs
|
|
win._closedTabs = [];
|
|
// correct selected tab index if it was stripped out
|
|
if (win.selected > win.tabs.length)
|
|
win.selected = 1;
|
|
return win.tabs.length > 0;
|
|
});
|
|
if (total.length == 0)
|
|
return null;
|
|
|
|
lastClosedWindowsCopy = [];
|
|
}
|
|
|
|
if (activeWindow) {
|
|
this.activeWindowSSiCache = activeWindow.__SSi || "";
|
|
}
|
|
ix = ids.indexOf(this.activeWindowSSiCache);
|
|
// We don't want to restore focus to a minimized window or a window which had all its
|
|
// tabs stripped out (doesn't exist).
|
|
if (ix != -1 && total[ix] && total[ix].sizemode == "minimized")
|
|
ix = -1;
|
|
|
|
let session = {
|
|
state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR,
|
|
lastUpdate: Date.now(),
|
|
startTime: this._sessionStartTime,
|
|
recentCrashes: this._recentCrashes
|
|
};
|
|
|
|
var scratchpads = null;
|
|
var browserConsole = null;
|
|
#ifdef MOZ_DEVTOOLS
|
|
// Scratchpad
|
|
// get open Scratchpad window states too
|
|
scratchpads = ScratchpadManager.getSessionState();
|
|
|
|
// The Browser Console
|
|
browserConsole = HUDService.getBrowserConsoleSessionState();
|
|
#endif
|
|
|
|
return {
|
|
windows: total,
|
|
selectedWindow: ix + 1,
|
|
_closedWindows: lastClosedWindowsCopy,
|
|
#ifdef MOZ_DEVTOOLS
|
|
session: session,
|
|
scratchpads: scratchpads,
|
|
browserConsole: browserConsole
|
|
#else
|
|
session: session
|
|
#endif
|
|
};
|
|
},
|
|
|
|
/**
|
|
* serialize session data for a window
|
|
* @param aWindow
|
|
* Window reference
|
|
* @returns string
|
|
*/
|
|
_getWindowState: function(aWindow) {
|
|
if (!this._isWindowLoaded(aWindow))
|
|
return this._statesToRestore[aWindow.__SS_restoreID];
|
|
|
|
if (this._loadState == STATE_RUNNING) {
|
|
this._collectWindowData(aWindow);
|
|
}
|
|
|
|
var winData = this._windows[aWindow.__SSi];
|
|
let windows = {};
|
|
windows[aWindow.__SSi] = winData;
|
|
this._updateCookies(windows);
|
|
|
|
return { windows: [winData] };
|
|
},
|
|
|
|
_collectWindowData: function(aWindow) {
|
|
if (!this._isWindowLoaded(aWindow))
|
|
return;
|
|
|
|
// update the internal state data for this window
|
|
this._saveWindowHistory(aWindow);
|
|
this._updateTextAndScrollData(aWindow);
|
|
this._updateCookieHosts(aWindow);
|
|
this._updateWindowFeatures(aWindow);
|
|
|
|
// Make sure we keep __SS_lastSessionWindowID around for cases like entering
|
|
// or leaving PB mode.
|
|
if (aWindow.__SS_lastSessionWindowID)
|
|
this._windows[aWindow.__SSi].__lastSessionWindowID =
|
|
aWindow.__SS_lastSessionWindowID;
|
|
|
|
this._dirtyWindows[aWindow.__SSi] = false;
|
|
},
|
|
|
|
/* ........ Restoring Functionality .............. */
|
|
|
|
/**
|
|
* restore features to a single window
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aState
|
|
* JS object or its eval'able source
|
|
* @param aOverwriteTabs
|
|
* bool overwrite existing tabs w/ new ones
|
|
* @param aFollowUp
|
|
* bool this isn't the restoration of the first window
|
|
*/
|
|
restoreWindow: function(aWindow, aState, aOverwriteTabs, aFollowUp) {
|
|
if (!aFollowUp) {
|
|
this.windowToFocus = aWindow;
|
|
}
|
|
// initialize window if necessary
|
|
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
|
|
this.onLoad(aWindow);
|
|
|
|
try {
|
|
var root = typeof aState == "string" ? JSON.parse(aState) : aState;
|
|
if (!root.windows[0]) {
|
|
this._sendRestoreCompletedNotifications();
|
|
return; // nothing to restore
|
|
}
|
|
}
|
|
catch (ex) { // invalid state object - don't restore anything
|
|
debug(ex);
|
|
this._sendRestoreCompletedNotifications();
|
|
return;
|
|
}
|
|
|
|
// We're not returning from this before we end up calling restoreHistoryPrecursor
|
|
// for this window, so make sure we send the SSWindowStateBusy event.
|
|
this._setWindowStateBusy(aWindow);
|
|
|
|
if (root._closedWindows)
|
|
this._closedWindows = root._closedWindows;
|
|
|
|
var winData;
|
|
if (!root.selectedWindow || root.selectedWindow > root.windows.length) {
|
|
root.selectedWindow = 0;
|
|
}
|
|
|
|
// open new windows for all further window entries of a multi-window session
|
|
// (unless they don't contain any tab data)
|
|
for (var w = 1; w < root.windows.length; w++) {
|
|
winData = root.windows[w];
|
|
if (winData && winData.tabs && winData.tabs[0]) {
|
|
var window = this._openWindowWithState({ windows: [winData] });
|
|
if (w == root.selectedWindow - 1) {
|
|
this.windowToFocus = window;
|
|
}
|
|
}
|
|
}
|
|
winData = root.windows[0];
|
|
if (!winData.tabs) {
|
|
winData.tabs = [];
|
|
}
|
|
// don't restore a single blank tab when we've had an external
|
|
// URL passed in for loading at startup (cf. bug 357419)
|
|
else if (root._firstTabs && !aOverwriteTabs && winData.tabs.length == 1 &&
|
|
(!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) {
|
|
winData.tabs = [];
|
|
}
|
|
|
|
var tabbrowser = aWindow.gBrowser;
|
|
var openTabCount = aOverwriteTabs ? tabbrowser.browsers.length : -1;
|
|
var newTabCount = winData.tabs.length;
|
|
var tabs = [];
|
|
|
|
// disable smooth scrolling while adding, moving, removing and selecting tabs
|
|
var tabstrip = tabbrowser.tabContainer.mTabstrip;
|
|
var smoothScroll = tabstrip.smoothScroll;
|
|
tabstrip.smoothScroll = false;
|
|
|
|
// unpin all tabs to ensure they are not reordered in the next loop
|
|
if (aOverwriteTabs) {
|
|
for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--)
|
|
tabbrowser.unpinTab(tabbrowser.tabs[t]);
|
|
}
|
|
|
|
// make sure that the selected tab won't be closed in order to
|
|
// prevent unnecessary flickering
|
|
if (aOverwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount)
|
|
tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1);
|
|
|
|
let numVisibleTabs = 0;
|
|
|
|
for (var t = 0; t < newTabCount; t++) {
|
|
tabs.push(t < openTabCount ?
|
|
tabbrowser.tabs[t] :
|
|
tabbrowser.addTab("about:blank",
|
|
{skipAnimation: true,
|
|
skipBackgroundNotify: true}));
|
|
// when resuming at startup: add additionally requested pages to the end
|
|
if (!aOverwriteTabs && root._firstTabs) {
|
|
tabbrowser.moveTabTo(tabs[t], t);
|
|
}
|
|
|
|
if (winData.tabs[t].pinned)
|
|
tabbrowser.pinTab(tabs[t]);
|
|
|
|
if (winData.tabs[t].hidden) {
|
|
tabbrowser.hideTab(tabs[t]);
|
|
}
|
|
else {
|
|
tabbrowser.showTab(tabs[t]);
|
|
numVisibleTabs++;
|
|
}
|
|
}
|
|
|
|
// if all tabs to be restored are hidden, make the first one visible
|
|
if (!numVisibleTabs && winData.tabs.length) {
|
|
winData.tabs[0].hidden = false;
|
|
tabbrowser.showTab(tabs[0]);
|
|
}
|
|
|
|
// If overwriting tabs, we want to reset each tab's "restoring" state. Since
|
|
// we're overwriting those tabs, they should no longer be restoring. The
|
|
// tabs will be rebuilt and marked if they need to be restored after loading
|
|
// state (in restoreHistoryPrecursor).
|
|
if (aOverwriteTabs) {
|
|
for (let i = 0; i < tabbrowser.tabs.length; i++) {
|
|
if (tabbrowser.browsers[i].__SS_restoreState)
|
|
this._resetTabRestoringState(tabbrowser.tabs[i]);
|
|
}
|
|
}
|
|
|
|
// We want to set up a counter on the window that indicates how many tabs
|
|
// in this window are unrestored. This will be used in restoreNextTab to
|
|
// determine if gRestoreTabsProgressListener should be removed from the window.
|
|
// If we aren't overwriting existing tabs, then we want to add to the existing
|
|
// count in case there are still tabs restoring.
|
|
if (!aWindow.__SS_tabsToRestore)
|
|
aWindow.__SS_tabsToRestore = 0;
|
|
if (aOverwriteTabs)
|
|
aWindow.__SS_tabsToRestore = newTabCount;
|
|
else
|
|
aWindow.__SS_tabsToRestore += newTabCount;
|
|
|
|
// We want to correlate the window with data from the last session, so
|
|
// assign another id if we have one. Otherwise clear so we don't do
|
|
// anything with it.
|
|
delete aWindow.__SS_lastSessionWindowID;
|
|
if (winData.__lastSessionWindowID)
|
|
aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
|
|
|
|
// when overwriting tabs, remove all superflous ones
|
|
if (aOverwriteTabs && newTabCount < openTabCount) {
|
|
Array.slice(tabbrowser.tabs, newTabCount, openTabCount)
|
|
.forEach(tabbrowser.removeTab, tabbrowser);
|
|
}
|
|
|
|
if (aOverwriteTabs) {
|
|
this.restoreWindowFeatures(aWindow, winData);
|
|
delete this._windows[aWindow.__SSi].extData;
|
|
}
|
|
if (winData.cookies) {
|
|
this.restoreCookies(winData.cookies);
|
|
}
|
|
if (winData.extData) {
|
|
if (!this._windows[aWindow.__SSi].extData) {
|
|
this._windows[aWindow.__SSi].extData = {};
|
|
}
|
|
for (var key in winData.extData) {
|
|
this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
|
|
}
|
|
}
|
|
if (aOverwriteTabs || root._firstTabs) {
|
|
this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || [];
|
|
}
|
|
|
|
this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs,
|
|
(aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0);
|
|
|
|
#ifdef MOZ_DEVTOOLS
|
|
if (aState.scratchpads) {
|
|
ScratchpadManager.restoreSession(aState.scratchpads);
|
|
}
|
|
|
|
// The Browser Console
|
|
if (aState.browserConsole) {
|
|
HUDService.restoreBrowserConsoleSession();
|
|
}
|
|
|
|
#endif
|
|
// set smoothScroll back to the original value
|
|
tabstrip.smoothScroll = smoothScroll;
|
|
|
|
this._sendRestoreCompletedNotifications();
|
|
},
|
|
|
|
/**
|
|
* Sets the tabs restoring order with the following priority:
|
|
* Selected tab, pinned tabs, optimized visible tabs, other visible tabs and
|
|
* hidden tabs.
|
|
* @param aTabBrowser
|
|
* Tab browser object
|
|
* @param aTabs
|
|
* Array of tab references
|
|
* @param aTabData
|
|
* Array of tab data
|
|
* @param aSelectedTab
|
|
* Index of selected tab (1 is first tab, 0 no selected tab)
|
|
*/
|
|
_setTabsRestoringOrder : function(
|
|
aTabBrowser, aTabs, aTabData, aSelectedTab) {
|
|
|
|
// Store the selected tab. Need to substract one to get the index in aTabs.
|
|
let selectedTab;
|
|
if (aSelectedTab > 0 && aTabs[aSelectedTab - 1]) {
|
|
selectedTab = aTabs[aSelectedTab - 1];
|
|
}
|
|
|
|
// Store the pinned tabs and hidden tabs.
|
|
let pinnedTabs = [];
|
|
let pinnedTabsData = [];
|
|
let hiddenTabs = [];
|
|
let hiddenTabsData = [];
|
|
if (aTabs.length > 1) {
|
|
for (let t = aTabs.length - 1; t >= 0; t--) {
|
|
if (aTabData[t].pinned) {
|
|
pinnedTabs.unshift(aTabs.splice(t, 1)[0]);
|
|
pinnedTabsData.unshift(aTabData.splice(t, 1)[0]);
|
|
} else if (aTabData[t].hidden) {
|
|
hiddenTabs.unshift(aTabs.splice(t, 1)[0]);
|
|
hiddenTabsData.unshift(aTabData.splice(t, 1)[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optimize the visible tabs only if there is a selected tab.
|
|
if (selectedTab) {
|
|
let selectedTabIndex = aTabs.indexOf(selectedTab);
|
|
if (selectedTabIndex > 0) {
|
|
let scrollSize = aTabBrowser.tabContainer.mTabstrip.scrollClientSize;
|
|
let tabWidth = aTabs[0].getBoundingClientRect().width;
|
|
let maxVisibleTabs = Math.ceil(scrollSize / tabWidth);
|
|
if (maxVisibleTabs < aTabs.length) {
|
|
let firstVisibleTab = 0;
|
|
let nonVisibleTabsCount = aTabs.length - maxVisibleTabs;
|
|
if (nonVisibleTabsCount >= selectedTabIndex) {
|
|
// Selected tab is leftmost since we scroll to it when possible.
|
|
firstVisibleTab = selectedTabIndex;
|
|
} else {
|
|
// Selected tab is rightmost or no more room to scroll right.
|
|
firstVisibleTab = nonVisibleTabsCount;
|
|
}
|
|
aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs);
|
|
aTabData =
|
|
aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge the stored tabs in order.
|
|
aTabs = pinnedTabs.concat(aTabs, hiddenTabs);
|
|
aTabData = pinnedTabsData.concat(aTabData, hiddenTabsData);
|
|
|
|
// Load the selected tab to the first position and select it.
|
|
if (selectedTab) {
|
|
let selectedTabIndex = aTabs.indexOf(selectedTab);
|
|
if (selectedTabIndex > 0) {
|
|
aTabs = aTabs.splice(selectedTabIndex, 1).concat(aTabs);
|
|
aTabData = aTabData.splice(selectedTabIndex, 1).concat(aTabData);
|
|
}
|
|
aTabBrowser.selectedTab = selectedTab;
|
|
}
|
|
|
|
return [aTabs, aTabData];
|
|
},
|
|
|
|
/**
|
|
* Manage history restoration for a window
|
|
* @param aWindow
|
|
* Window to restore the tabs into
|
|
* @param aTabs
|
|
* Array of tab references
|
|
* @param aTabData
|
|
* Array of tab data
|
|
* @param aSelectTab
|
|
* Index of selected tab
|
|
* @param aIx
|
|
* Index of the next tab to check readyness for
|
|
* @param aCount
|
|
* Counter for number of times delaying b/c browser or history aren't ready
|
|
* @param aRestoreImmediately
|
|
* Flag to indicate whether the given set of tabs aTabs should be
|
|
* restored/loaded immediately even if restore_on_demand = true
|
|
*/
|
|
restoreHistoryPrecursor:
|
|
function(aWindow, aTabs, aTabData, aSelectTab,
|
|
aIx, aCount, aRestoreImmediately = false) {
|
|
var tabbrowser = aWindow.gBrowser;
|
|
|
|
// make sure that all browsers and their histories are available
|
|
// - if one's not, resume this check in 100ms (repeat at most 10 times)
|
|
for (var t = aIx; t < aTabs.length; t++) {
|
|
try {
|
|
if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) {
|
|
throw new Error();
|
|
}
|
|
}
|
|
catch (ex) { // in case browser or history aren't ready yet
|
|
if (aCount < 10) {
|
|
var restoreHistoryFunc = function(self) {
|
|
self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab,
|
|
aIx, aCount + 1, aRestoreImmediately);
|
|
}
|
|
aWindow.setTimeout(restoreHistoryFunc, 100, this);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!this._isWindowLoaded(aWindow)) {
|
|
// from now on, the data will come from the actual window
|
|
delete this._statesToRestore[aWindow.__SS_restoreID];
|
|
delete aWindow.__SS_restoreID;
|
|
delete this._windows[aWindow.__SSi]._restoring;
|
|
|
|
// It's important to set the window state to dirty so that
|
|
// we collect their data for the first time when saving state.
|
|
this._dirtyWindows[aWindow.__SSi] = true;
|
|
}
|
|
|
|
if (aTabs.length == 0) {
|
|
// this is normally done in restoreHistory() but as we're returning early
|
|
// here we need to take care of it.
|
|
this._setWindowStateReady(aWindow);
|
|
return;
|
|
}
|
|
|
|
// Sets the tabs restoring order.
|
|
[aTabs, aTabData] =
|
|
this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab);
|
|
|
|
// Prepare the tabs so that they can be properly restored. We'll pin/unpin
|
|
// and show/hide tabs as necessary. We'll also set the labels, user typed
|
|
// value, and attach a copy of the tab's data in case we close it before
|
|
// it's been restored.
|
|
for (t = 0; t < aTabs.length; t++) {
|
|
let tab = aTabs[t];
|
|
let browser = tabbrowser.getBrowserForTab(tab);
|
|
let tabData = aTabData[t];
|
|
|
|
if (tabData.pinned)
|
|
tabbrowser.pinTab(tab);
|
|
else
|
|
tabbrowser.unpinTab(tab);
|
|
|
|
if (tabData.hidden)
|
|
tabbrowser.hideTab(tab);
|
|
else
|
|
tabbrowser.showTab(tab);
|
|
|
|
if ("attributes" in tabData) {
|
|
// Ensure that we persist tab attributes restored from previous sessions.
|
|
Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a));
|
|
}
|
|
|
|
browser.__SS_tabStillLoading = true;
|
|
|
|
// keep the data around to prevent dataloss in case
|
|
// a tab gets closed before it's been properly restored
|
|
browser.__SS_data = tabData;
|
|
browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
|
|
browser.setAttribute("pending", "true");
|
|
tab.setAttribute("pending", "true");
|
|
|
|
// Make sure that set/getTabValue will set/read the correct data by
|
|
// wiping out any current value in tab.__SS_extdata.
|
|
delete tab.__SS_extdata;
|
|
|
|
if (!tabData.entries || tabData.entries.length == 0) {
|
|
// make sure to blank out this tab's content
|
|
// (just purging the tab's history won't be enough)
|
|
browser.contentDocument.location = "about:blank";
|
|
continue;
|
|
}
|
|
|
|
browser.stop(); // in case about:blank isn't done yet
|
|
|
|
// wall-paper fix for bug 439675: make sure that the URL to be loaded
|
|
// is always visible in the address bar
|
|
let activeIndex = (tabData.index || tabData.entries.length) - 1;
|
|
let activePageData = tabData.entries[activeIndex] || null;
|
|
let uri = activePageData ? activePageData.url || null : null;
|
|
browser.userTypedValue = uri;
|
|
|
|
// Also make sure currentURI is set so that switch-to-tab works before
|
|
// the tab is restored. We'll reset this to about:blank when we try to
|
|
// restore the tab to ensure that docshell doeesn't get confused.
|
|
if (uri)
|
|
browser.docShell.setCurrentURI(this._getURIFromString(uri));
|
|
|
|
// If the page has a title, set it.
|
|
if (activePageData) {
|
|
if (activePageData.title) {
|
|
tab.label = activePageData.title;
|
|
tab.crop = "end";
|
|
} else if (activePageData.url != "about:blank") {
|
|
tab.label = activePageData.url;
|
|
tab.crop = "center";
|
|
}
|
|
}
|
|
}
|
|
|
|
// helper hashes for ensuring unique frame IDs and unique document
|
|
// identifiers.
|
|
var idMap = { used: {} };
|
|
var docIdentMap = {};
|
|
this.restoreHistory(aWindow, aTabs, aTabData, idMap, docIdentMap,
|
|
aRestoreImmediately);
|
|
},
|
|
|
|
/**
|
|
* Restore history for a window
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aTabs
|
|
* Array of tab references
|
|
* @param aTabData
|
|
* Array of tab data
|
|
* @param aIdMap
|
|
* Hash for ensuring unique frame IDs
|
|
* @param aRestoreImmediately
|
|
* Flag to indicate whether the given set of tabs aTabs should be
|
|
* restored/loaded immediately even if restore_on_demand = true
|
|
*/
|
|
restoreHistory:
|
|
function(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap,
|
|
aRestoreImmediately) {
|
|
var _this = this;
|
|
// if the tab got removed before being completely restored, then skip it
|
|
while (aTabs.length > 0 && !(this._canRestoreTabHistory(aTabs[0]))) {
|
|
aTabs.shift();
|
|
aTabData.shift();
|
|
}
|
|
if (aTabs.length == 0) {
|
|
// At this point we're essentially ready for consumers to read/write data
|
|
// via the sessionstore API so we'll send the SSWindowStateReady event.
|
|
this._setWindowStateReady(aWindow);
|
|
return; // no more tabs to restore
|
|
}
|
|
|
|
var tab = aTabs.shift();
|
|
var tabData = aTabData.shift();
|
|
|
|
var browser = aWindow.gBrowser.getBrowserForTab(tab);
|
|
var history = browser.webNavigation.sessionHistory;
|
|
|
|
if (history.count > 0) {
|
|
history.PurgeHistory(history.count);
|
|
}
|
|
history.QueryInterface(Ci.nsISHistoryInternal);
|
|
|
|
browser.__SS_shistoryListener = new SessionStoreSHistoryListener(tab);
|
|
history.addSHistoryListener(browser.__SS_shistoryListener);
|
|
|
|
if (!tabData.entries) {
|
|
tabData.entries = [];
|
|
}
|
|
if (tabData.extData) {
|
|
tab.__SS_extdata = {};
|
|
for (let key in tabData.extData)
|
|
tab.__SS_extdata[key] = tabData.extData[key];
|
|
}
|
|
else
|
|
delete tab.__SS_extdata;
|
|
|
|
for (var i = 0; i < tabData.entries.length; i++) {
|
|
//XXXzpao Wallpaper patch for bug 514751
|
|
if (!tabData.entries[i].url)
|
|
continue;
|
|
history.addEntry(this._deserializeHistoryEntry(tabData.entries[i],
|
|
aIdMap, aDocIdentMap), true);
|
|
}
|
|
|
|
// make sure to reset the capabilities and attributes, in case this tab gets reused
|
|
let disallow = new Set(tabData.disallow && tabData.disallow.split(","));
|
|
for (let cap of gDocShellCapabilities(browser.docShell))
|
|
browser.docShell["allow" + cap] = !disallow.has(cap);
|
|
|
|
// Restore tab attributes.
|
|
if ("attributes" in tabData) {
|
|
TabAttributes.set(tab, tabData.attributes);
|
|
}
|
|
|
|
// Restore the tab icon.
|
|
if ("image" in tabData) {
|
|
// Using null as the loadingPrincipal because serializing
|
|
// the principal would be overkill. Within SetIcon we
|
|
// default to the systemPrincipal if aLoadingPrincipal is
|
|
// null which will allow the favicon to load.
|
|
aWindow.gBrowser.setIcon(tab, tabData.image, null);
|
|
}
|
|
|
|
if (tabData.storage && browser.docShell instanceof Ci.nsIDocShell)
|
|
SessionStorage.deserialize(browser.docShell, tabData.storage);
|
|
|
|
// notify the tabbrowser that the tab chrome has been restored
|
|
var event = aWindow.document.createEvent("Events");
|
|
event.initEvent("SSTabRestoring", true, false);
|
|
tab.dispatchEvent(event);
|
|
|
|
// Restore the history in the next tab
|
|
aWindow.setTimeout(function(){
|
|
_this.restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap,
|
|
aRestoreImmediately);
|
|
}, 0);
|
|
|
|
// This could cause us to ignore max_concurrent_tabs pref a bit, but
|
|
// it ensures each window will have its selected tab loaded.
|
|
if (aRestoreImmediately || aWindow.gBrowser.selectedBrowser == browser) {
|
|
this.restoreTab(tab);
|
|
}
|
|
else {
|
|
TabRestoreQueue.add(tab);
|
|
this.restoreNextTab();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Restores the specified tab. If the tab can't be restored (eg, no history or
|
|
* calling gotoIndex fails), then state changes will be rolled back.
|
|
* This method will check if gTabsProgressListener is attached to the tab's
|
|
* window, ensuring that we don't get caught without one.
|
|
* This method removes the session history listener right before starting to
|
|
* attempt a load. This will prevent cases of "stuck" listeners.
|
|
* If this method returns false, then it is up to the caller to decide what to
|
|
* do. In the common case (restoreNextTab), we will want to then attempt to
|
|
* restore the next tab. In the other case (selecting the tab, reloading the
|
|
* tab), the caller doesn't actually want to do anything if no page is loaded.
|
|
*
|
|
* @param aTab
|
|
* the tab to restore
|
|
*
|
|
* @returns true/false indicating whether or not a load actually happened
|
|
*/
|
|
restoreTab: function(aTab) {
|
|
let window = aTab.ownerDocument.defaultView;
|
|
let browser = aTab.linkedBrowser;
|
|
let tabData = browser.__SS_data;
|
|
|
|
// There are cases within where we haven't actually started a load. In that
|
|
// that case we'll reset state changes we made and return false to the caller
|
|
// can handle appropriately.
|
|
let didStartLoad = false;
|
|
|
|
// Make sure that the tabs progress listener is attached to this window
|
|
this._ensureTabsProgressListener(window);
|
|
|
|
// Make sure that this tab is removed from the priority queue.
|
|
TabRestoreQueue.remove(aTab);
|
|
|
|
// Increase our internal count.
|
|
this._tabsRestoringCount++;
|
|
|
|
// Set this tab's state to restoring
|
|
browser.__SS_restoreState = TAB_STATE_RESTORING;
|
|
browser.removeAttribute("pending");
|
|
aTab.removeAttribute("pending");
|
|
|
|
// Remove the history listener, since we no longer need it once we start restoring
|
|
this._removeSHistoryListener(aTab);
|
|
|
|
let activeIndex = (tabData.index || tabData.entries.length) - 1;
|
|
if (activeIndex >= tabData.entries.length)
|
|
activeIndex = tabData.entries.length - 1;
|
|
// Reset currentURI. This creates a new session history entry with a new
|
|
// doc identifier, so we need to explicitly save and restore the old doc
|
|
// identifier (corresponding to the SHEntry at activeIndex) below.
|
|
browser.webNavigation.setCurrentURI(this._getURIFromString("about:blank"));
|
|
// Attach data that will be restored on "load" event, after tab is restored.
|
|
if (activeIndex > -1) {
|
|
// restore those aspects of the currently active documents which are not
|
|
// preserved in the plain history entries (mainly scroll state and text data)
|
|
browser.__SS_restore_data = tabData.entries[activeIndex] || {};
|
|
browser.__SS_restore_pageStyle = tabData.pageStyle || "";
|
|
browser.__SS_restore_tab = aTab;
|
|
didStartLoad = true;
|
|
try {
|
|
// In order to work around certain issues in session history, we need to
|
|
// force session history to update its internal index and call reload
|
|
// instead of gotoIndex. See bug 597315.
|
|
browser.webNavigation.sessionHistory.getEntryAtIndex(activeIndex, true);
|
|
browser.webNavigation.sessionHistory.reloadCurrentEntry();
|
|
// If the user prefers it, bypass cache and always load from the network,
|
|
// but only if restoring on demand, to prevent request flooding (since
|
|
// reloading will override the max tabs to restore concurrently mechanism).
|
|
// See Issue #1772
|
|
if (TabRestoreQueue.prefs.restoreOnDemand) {
|
|
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
switch (this._cacheBehavior) {
|
|
case 2: // hard refresh
|
|
flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
|
|
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
|
|
browser.webNavigation.reload(flags);
|
|
break;
|
|
case 1: // soft refresh
|
|
browser.webNavigation.reload(flags);
|
|
break;
|
|
default: // 0 or other: use cache, so do nothing.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (ex) {
|
|
// ignore page load errors
|
|
aTab.removeAttribute("busy");
|
|
didStartLoad = false;
|
|
}
|
|
}
|
|
|
|
// Handle userTypedValue. Setting userTypedValue seems to update gURLbar
|
|
// as needed. Calling loadURI will cancel form filling in restoreDocument
|
|
if (tabData.userTypedValue) {
|
|
browser.userTypedValue = tabData.userTypedValue;
|
|
if (tabData.userTypedClear) {
|
|
// Make it so that we'll enter restoreDocument on page load. We will
|
|
// fire SSTabRestored from there. We don't have any form data to restore
|
|
// so we can just set the URL to null.
|
|
browser.__SS_restore_data = { url: null };
|
|
browser.__SS_restore_tab = aTab;
|
|
if (didStartLoad)
|
|
browser.stop();
|
|
didStartLoad = true;
|
|
browser.loadURIWithFlags(tabData.userTypedValue,
|
|
Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP);
|
|
}
|
|
}
|
|
|
|
// If we didn't start a load, then we won't reset this tab through the usual
|
|
// channel (via the progress listener), so reset the tab ourselves. We will
|
|
// also send SSTabRestored since this tab has technically been restored.
|
|
if (!didStartLoad) {
|
|
this._sendTabRestoredNotification(aTab);
|
|
this._resetTabRestoringState(aTab);
|
|
}
|
|
|
|
return didStartLoad;
|
|
},
|
|
|
|
/**
|
|
* This _attempts_ to restore the next available tab. If the restore fails,
|
|
* then we will attempt the next one.
|
|
* There are conditions where this won't do anything:
|
|
* if we're in the process of quitting
|
|
* if there are no tabs to restore
|
|
* if we have already reached the limit for number of tabs to restore
|
|
*/
|
|
restoreNextTab: function() {
|
|
// If we call in here while quitting, we don't actually want to do anything
|
|
if (this._loadState == STATE_QUITTING)
|
|
return;
|
|
|
|
// Don't exceed the maximum number of concurrent tab restores.
|
|
if (this._tabsRestoringCount >= this._maxConcurrentTabRestores)
|
|
return;
|
|
|
|
let tab = TabRestoreQueue.shift();
|
|
if (tab) {
|
|
let didStartLoad = this.restoreTab(tab);
|
|
// If we don't start a load in the restored tab (eg, no entries) then we
|
|
// want to attempt to restore the next tab.
|
|
if (!didStartLoad)
|
|
this.restoreNextTab();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* expands serialized history data into a session-history-entry instance
|
|
* @param aEntry
|
|
* Object containing serialized history data for a URL
|
|
* @param aIdMap
|
|
* Hash for ensuring unique frame IDs
|
|
* @returns nsISHEntry
|
|
*/
|
|
_deserializeHistoryEntry:
|
|
function(aEntry, aIdMap, aDocIdentMap) {
|
|
|
|
var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].
|
|
createInstance(Ci.nsISHEntry);
|
|
|
|
shEntry.setURI(this._getURIFromString(aEntry.url));
|
|
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 = this._getURIFromString(aEntry.referrer);
|
|
if (aEntry.isSrcdocEntry)
|
|
shEntry.srcdocData = aEntry.srcdocData;
|
|
|
|
if (aEntry.cacheKey) {
|
|
var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].
|
|
createInstance(Ci.nsISupportsPRUint32);
|
|
cacheKey.data = aEntry.cacheKey;
|
|
shEntry.cacheKey = cacheKey;
|
|
}
|
|
|
|
if (aEntry.ID) {
|
|
// get a new unique ID for this frame (since the one from the last
|
|
// start might already be in use)
|
|
var id = aIdMap[aEntry.ID] || 0;
|
|
if (!id) {
|
|
for (id = Date.now(); 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.scroll) {
|
|
var scrollPos = (aEntry.scroll || "0,0").split(",");
|
|
scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
|
|
shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
|
|
}
|
|
|
|
if (aEntry.postdata_b64) {
|
|
var postdata = atob(aEntry.postdata_b64);
|
|
var stream = Cc["@mozilla.org/io/string-input-stream;1"].
|
|
createInstance(Ci.nsIStringInputStream);
|
|
stream.setData(postdata, postdata.length);
|
|
shEntry.postData = stream;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// The field aEntry.owner_b64 got renamed to aEntry.triggeringPricipal_b64 in
|
|
// Bug 1286472. To remain backward compatible we still have to support that
|
|
// field for a few cycles before we can remove it within Bug 1289785.
|
|
if (aEntry.owner_b64) {
|
|
aEntry.triggeringPrincipal_b64 = aEntry.owner_b64;
|
|
delete aEntry.owner_b64;
|
|
}
|
|
|
|
if (aEntry.triggeringPrincipal_b64) {
|
|
var triggeringPrincipalInput = Cc["@mozilla.org/io/string-input-stream;1"].
|
|
createInstance(Ci.nsIStringInputStream);
|
|
var binaryData = atob(aEntry.triggeringPrincipal_b64);
|
|
triggeringPrincipalInput.setData(binaryData, binaryData.length);
|
|
var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
createInstance(Ci.nsIObjectInputStream);
|
|
binaryStream.setInputStream(triggeringPrincipalInput);
|
|
try { // Catch possible deserialization exceptions
|
|
shEntry.triggeringPrincipal = binaryStream.readObject(true);
|
|
} catch (ex) { debug(ex); }
|
|
}
|
|
|
|
if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
|
|
for (var i = 0; i < aEntry.children.length; i++) {
|
|
//XXXzpao Wallpaper patch for bug 514751
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Restore properties to a loaded document
|
|
*/
|
|
restoreDocument: function(aWindow, aBrowser, aEvent) {
|
|
// wait for the top frame to be loaded completely
|
|
if (!aEvent || !aEvent.originalTarget || !aEvent.originalTarget.defaultView ||
|
|
aEvent.originalTarget.defaultView != aEvent.originalTarget.defaultView.top) {
|
|
return;
|
|
}
|
|
|
|
// always call this before injecting content into a document!
|
|
function hasExpectedURL(aDocument, aURL)
|
|
!aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, "");
|
|
|
|
let selectedPageStyle = aBrowser.__SS_restore_pageStyle;
|
|
function restoreTextDataAndScrolling(aContent, aData, aPrefix) {
|
|
if (aData.formdata && hasExpectedURL(aContent.document, aData.url)) {
|
|
let formdata = aData.formdata;
|
|
|
|
// handle backwards compatibility
|
|
// this is a migration from pre-firefox 15. cf. bug 742051
|
|
if (!("xpath" in formdata || "id" in formdata)) {
|
|
formdata = { xpath: {}, id: {} };
|
|
|
|
for each (let [key, value] in Iterator(aData.formdata)) {
|
|
if (key.charAt(0) == "#") {
|
|
formdata.id[key.slice(1)] = value;
|
|
} else {
|
|
formdata.xpath[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// for about:sessionrestore we saved the field as JSON to avoid
|
|
// nested instances causing humongous sessionstore.js files.
|
|
// cf. bug 467409
|
|
if (aData.url == "about:sessionrestore" &&
|
|
"sessionData" in formdata.id &&
|
|
typeof formdata.id["sessionData"] == "object") {
|
|
formdata.id["sessionData"] =
|
|
JSON.stringify(formdata.id["sessionData"]);
|
|
}
|
|
|
|
// update the formdata
|
|
aData.formdata = formdata;
|
|
// merge the formdata
|
|
DocumentUtils.mergeFormData(aContent.document, formdata);
|
|
}
|
|
|
|
if (aData.innerHTML) {
|
|
aWindow.setTimeout(function() {
|
|
if (aContent.document.designMode == "on" &&
|
|
hasExpectedURL(aContent.document, aData.url) &&
|
|
aContent.document.body) {
|
|
aContent.document.body.innerHTML = aData.innerHTML;
|
|
}
|
|
}, 0);
|
|
}
|
|
var match;
|
|
if (aData.scroll && (match = /(\d+),(\d+)/.exec(aData.scroll)) != null) {
|
|
aContent.scrollTo(match[1], match[2]);
|
|
}
|
|
Array.forEach(aContent.document.styleSheets, function(aSS) {
|
|
aSS.disabled = aSS.title && aSS.title != selectedPageStyle;
|
|
});
|
|
for (var i = 0; i < aContent.frames.length; i++) {
|
|
if (aData.children && aData.children[i] &&
|
|
hasExpectedURL(aContent.document, aData.url)) {
|
|
restoreTextDataAndScrolling(aContent.frames[i], aData.children[i], aPrefix + i + "|");
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't restore text data and scrolling state if the user has navigated
|
|
// away before the loading completed (except for in-page navigation)
|
|
if (hasExpectedURL(aEvent.originalTarget, aBrowser.__SS_restore_data.url)) {
|
|
var content = aEvent.originalTarget.defaultView;
|
|
restoreTextDataAndScrolling(content, aBrowser.__SS_restore_data, "");
|
|
aBrowser.markupDocumentViewer.authorStyleDisabled = selectedPageStyle == "_nostyle";
|
|
}
|
|
|
|
// notify the tabbrowser that this document has been completely restored
|
|
this._sendTabRestoredNotification(aBrowser.__SS_restore_tab);
|
|
|
|
delete aBrowser.__SS_restore_data;
|
|
delete aBrowser.__SS_restore_pageStyle;
|
|
delete aBrowser.__SS_restore_tab;
|
|
},
|
|
|
|
/**
|
|
* Restore visibility and dimension features to a window
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aWinData
|
|
* Object containing session data for the window
|
|
*/
|
|
restoreWindowFeatures: function(aWindow, aWinData) {
|
|
var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[];
|
|
WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) {
|
|
aWindow[aItem].visible = hidden.indexOf(aItem) == -1;
|
|
});
|
|
|
|
if (aWinData.isPopup) {
|
|
this._windows[aWindow.__SSi].isPopup = true;
|
|
if (aWindow.gURLBar) {
|
|
aWindow.gURLBar.readOnly = true;
|
|
aWindow.gURLBar.setAttribute("enablehistory", "false");
|
|
}
|
|
}
|
|
else {
|
|
delete this._windows[aWindow.__SSi].isPopup;
|
|
if (aWindow.gURLBar) {
|
|
aWindow.gURLBar.readOnly = false;
|
|
aWindow.gURLBar.setAttribute("enablehistory", "true");
|
|
}
|
|
}
|
|
|
|
var _this = this;
|
|
aWindow.setTimeout(function() {
|
|
_this.restoreDimensions.apply(_this, [aWindow,
|
|
+aWinData.width || 0,
|
|
+aWinData.height || 0,
|
|
"screenX" in aWinData ? +aWinData.screenX : NaN,
|
|
"screenY" in aWinData ? +aWinData.screenY : NaN,
|
|
aWinData.sizemode || "", aWinData.sidebar || ""]);
|
|
}, 0);
|
|
},
|
|
|
|
/**
|
|
* Restore a window's dimensions
|
|
* @param aWidth
|
|
* Window width
|
|
* @param aHeight
|
|
* Window height
|
|
* @param aLeft
|
|
* Window left
|
|
* @param aTop
|
|
* Window top
|
|
* @param aSizeMode
|
|
* Window size mode (eg: maximized)
|
|
* @param aSidebar
|
|
* Sidebar command
|
|
*/
|
|
restoreDimensions: function(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) {
|
|
var win = aWindow;
|
|
var _this = this;
|
|
function win_(aName) { return _this._getWindowDimension(win, aName); }
|
|
|
|
// Find available space on the screen where this window is being placed
|
|
let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight);
|
|
if (screen && !this._prefBranch.getBoolPref("sessionstore.exactPos")) {
|
|
let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {};
|
|
screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight);
|
|
|
|
// Screen X/Y are based on the origin of the screen's desktop-pixel coordinate space
|
|
let screenLeftCss = screenLeft.value;
|
|
let screenTopCss = screenTop.value;
|
|
|
|
// Convert the screen's device pixel dimensions to CSS px dimensions
|
|
screen.GetAvailRect(screenLeft, screenTop, screenWidth, screenHeight);
|
|
let cssToDevScale = screen.defaultCSSScaleFactor;
|
|
let screenRightCss = screenLeftCss + screenWidth.value / cssToDevScale;
|
|
let screenBottomCss = screenTopCss + screenHeight.value / cssToDevScale;
|
|
|
|
// Pull the window within the screen's bounds.
|
|
// First, ensure the left edge is on-screen
|
|
if (aLeft < screenLeftCss) {
|
|
aLeft = screenLeftCss;
|
|
}
|
|
// Then check the resulting right edge, and reduce it if necessary.
|
|
let right = aLeft + aWidth;
|
|
if (right > screenRightCss) {
|
|
right = screenRightCss;
|
|
// See if we can move the left edge leftwards to maintain width.
|
|
if (aLeft > screenLeftCss) {
|
|
aLeft = Math.max(right - aWidth, screenLeftCss);
|
|
}
|
|
}
|
|
// Finally, update aWidth to account for the adjusted left and right edges.
|
|
aWidth = right - aLeft;
|
|
|
|
// Do the same in the vertical dimension.
|
|
// First, ensure the top edge is on-screen
|
|
if (aTop < screenTopCss) {
|
|
aTop = screenTopCss;
|
|
}
|
|
// Then check the resulting right edge, and reduce it if necessary.
|
|
let bottom = aTop + aHeight;
|
|
if (bottom > screenBottomCss) {
|
|
bottom = screenBottomCss;
|
|
// See if we can move the top edge upwards to maintain height.
|
|
if (aTop > screenTopCss) {
|
|
aTop = Math.max(bottom - aHeight, screenTopCss);
|
|
}
|
|
}
|
|
// Finally, update aHeight to account for the adjusted top and bottom edges.
|
|
aHeight = bottom - aTop;
|
|
}
|
|
|
|
// Only modify those aspects which aren't correct yet
|
|
if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) {
|
|
aWindow.moveTo(aLeft, aTop);
|
|
}
|
|
if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) {
|
|
// Don't resize the window if it's currently maximized and we would
|
|
// maximize it again shortly after.
|
|
if (aSizeMode != "maximized" || win_("sizemode") != "maximized") {
|
|
aWindow.resizeTo(aWidth, aHeight);
|
|
}
|
|
}
|
|
|
|
// Restore window state
|
|
if (aSizeMode && win_("sizemode") != aSizeMode)
|
|
{
|
|
switch (aSizeMode)
|
|
{
|
|
case "maximized":
|
|
aWindow.maximize();
|
|
break;
|
|
case "minimized":
|
|
aWindow.minimize();
|
|
break;
|
|
case "normal":
|
|
aWindow.restore();
|
|
break;
|
|
}
|
|
}
|
|
var sidebar = aWindow.document.getElementById("sidebar-box");
|
|
if (sidebar.getAttribute("sidebarcommand") != aSidebar) {
|
|
aWindow.toggleSidebar(aSidebar);
|
|
}
|
|
// since resizing/moving a window brings it to the foreground,
|
|
// we might want to re-focus the last focused window
|
|
if (this.windowToFocus) {
|
|
this.windowToFocus.focus();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Restores cookies
|
|
* @param aCookies
|
|
* Array of cookie objects
|
|
*/
|
|
restoreCookies: function(aCookies) {
|
|
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision
|
|
var MAX_EXPIRY = Math.pow(2, 62);
|
|
for (let i = 0; i < aCookies.length; i++) {
|
|
var cookie = aCookies[i];
|
|
try {
|
|
Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "",
|
|
cookie.value, !!cookie.secure, !!cookie.httponly, true,
|
|
"expiry" in cookie ? cookie.expiry : MAX_EXPIRY, {});
|
|
}
|
|
catch (ex) { Cu.reportError(ex); } // don't let a single cookie stop recovering
|
|
}
|
|
},
|
|
|
|
/* ........ Disk Access .............. */
|
|
|
|
/**
|
|
* save state delayed by N ms
|
|
* marks window as dirty (i.e. data update can't be skipped)
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aDelay
|
|
* Milliseconds to delay
|
|
*/
|
|
saveStateDelayed: function(aWindow, aDelay) {
|
|
if (aWindow) {
|
|
this._dirtyWindows[aWindow.__SSi] = true;
|
|
}
|
|
|
|
if (!this._saveTimer) {
|
|
// interval until the next disk operation is allowed
|
|
var minimalDelay = this._lastSaveTime + this._interval - Date.now();
|
|
|
|
// if we have to wait, set a timer, otherwise saveState directly
|
|
aDelay = Math.max(minimalDelay, aDelay || 2000);
|
|
if (aDelay > 0) {
|
|
this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
}
|
|
else {
|
|
this.saveState();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* save state to disk
|
|
* @param aUpdateAll
|
|
* Bool update all windows
|
|
*/
|
|
saveState: function(aUpdateAll) {
|
|
// If crash recovery is disabled, we only want to resume with pinned tabs
|
|
// if we crash.
|
|
let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash;
|
|
|
|
var oState = this._getCurrentState(aUpdateAll, pinnedOnly);
|
|
if (!oState) {
|
|
return;
|
|
}
|
|
|
|
// Forget about private windows.
|
|
for (let i = oState.windows.length - 1; i >= 0; i--) {
|
|
if (oState.windows[i].isPrivate) {
|
|
oState.windows.splice(i, 1);
|
|
if (oState.selectedWindow >= i) {
|
|
oState.selectedWindow--;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = oState._closedWindows.length - 1; i >= 0; i--) {
|
|
if (oState._closedWindows[i].isPrivate) {
|
|
oState._closedWindows.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
#ifndef XP_MACOSX
|
|
// We want to restore closed windows that are marked with _shouldRestore.
|
|
// We're doing this here because we want to control this only when saving
|
|
// the file.
|
|
while (oState._closedWindows.length) {
|
|
let i = oState._closedWindows.length - 1;
|
|
if (oState._closedWindows[i]._shouldRestore) {
|
|
delete oState._closedWindows[i]._shouldRestore;
|
|
oState.windows.unshift(oState._closedWindows.pop());
|
|
}
|
|
else {
|
|
// We only need to go until we hit !needsRestore since we're going in reverse
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (pinnedOnly) {
|
|
// Save original resume_session_once preference for when quiting browser,
|
|
// otherwise session will be restored next time browser starts and we
|
|
// only want it to be restored in the case of a crash.
|
|
if (this._resume_session_once_on_shutdown == null) {
|
|
this._resume_session_once_on_shutdown =
|
|
this._prefBranch.getBoolPref("sessionstore.resume_session_once");
|
|
this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
|
|
// flush the preference file so preference will be saved in case of a crash
|
|
Services.prefs.savePrefFile(null);
|
|
}
|
|
}
|
|
|
|
// Persist the last session if we deferred restoring it
|
|
if (this._lastSessionState)
|
|
oState.lastSessionState = this._lastSessionState;
|
|
|
|
// Make sure that we keep the previous session if we started with a single
|
|
// private window and no non-private windows have been opened, yet.
|
|
if (this._deferredInitialState) {
|
|
oState.windows = this._deferredInitialState.windows || [];
|
|
}
|
|
|
|
this._saveStateObject(oState);
|
|
},
|
|
|
|
/**
|
|
* write a state object to disk
|
|
*/
|
|
_saveStateObject: function(aStateObj) {
|
|
let data = this._toJSONString(aStateObj);
|
|
|
|
let stateString = this._createSupportsString(data);
|
|
Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
|
|
data = stateString.data;
|
|
|
|
// Don't touch the file if an observer has deleted all state data.
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
let promise;
|
|
// If "sessionstore.resume_from_crash" is true, attempt to backup the
|
|
// session file first, before writing to it.
|
|
if (this._resume_from_crash) {
|
|
// Note that we do not have race conditions here as _SessionFile
|
|
// guarantees that any I/O operation is completed before proceeding to
|
|
// the next I/O operation.
|
|
// Note backup happens only once, on initial save.
|
|
promise = this._backupSessionFileOnce;
|
|
} else {
|
|
promise = Promise.resolve();
|
|
}
|
|
|
|
// Attempt to write to the session file (potentially, depending on
|
|
// "sessionstore.resume_from_crash" preference, after successful backup).
|
|
promise = promise.then(function onSuccess() {
|
|
// Write (atomically) to a session file, using a tmp file.
|
|
return _SessionFile.write(data);
|
|
});
|
|
|
|
// Once the session file is successfully updated, save the time stamp of the
|
|
// last save and notify the observers.
|
|
promise = promise.then(() => {
|
|
this._lastSaveTime = Date.now();
|
|
Services.obs.notifyObservers(null, "sessionstore-state-write-complete",
|
|
"");
|
|
});
|
|
},
|
|
|
|
/* ........ Auxiliary Functions .............. */
|
|
|
|
// Wrap a string as a nsISupports
|
|
_createSupportsString: function(aData) {
|
|
let string = Cc["@mozilla.org/supports-string;1"]
|
|
.createInstance(Ci.nsISupportsString);
|
|
string.data = aData;
|
|
return string;
|
|
},
|
|
|
|
/**
|
|
* call a callback for all currently opened browser windows
|
|
* (might miss the most recent one)
|
|
* @param aFunc
|
|
* Callback each window is passed to
|
|
*/
|
|
_forEachBrowserWindow: function(aFunc) {
|
|
var windowsEnum = Services.wm.getEnumerator("navigator:browser");
|
|
|
|
while (windowsEnum.hasMoreElements()) {
|
|
var window = windowsEnum.getNext();
|
|
if (window.__SSi && !window.closed) {
|
|
aFunc.call(this, window);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns most recent window
|
|
* @returns Window reference
|
|
*/
|
|
_getMostRecentBrowserWindow: function() {
|
|
var win = Services.wm.getMostRecentWindow("navigator:browser");
|
|
if (!win)
|
|
return null;
|
|
if (!win.closed)
|
|
return win;
|
|
|
|
#ifdef BROKEN_WM_Z_ORDER
|
|
win = null;
|
|
var windowsEnum = Services.wm.getEnumerator("navigator:browser");
|
|
// this is oldest to newest, so this gets a bit ugly
|
|
while (windowsEnum.hasMoreElements()) {
|
|
let nextWin = windowsEnum.getNext();
|
|
if (!nextWin.closed)
|
|
win = nextWin;
|
|
}
|
|
return win;
|
|
#else
|
|
var windowsEnum =
|
|
Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true);
|
|
while (windowsEnum.hasMoreElements()) {
|
|
win = windowsEnum.getNext();
|
|
if (!win.closed)
|
|
return win;
|
|
}
|
|
return null;
|
|
#endif
|
|
},
|
|
|
|
/**
|
|
* Calls onClose for windows that are determined to be closed but aren't
|
|
* destroyed yet, which would otherwise cause getBrowserState and
|
|
* setBrowserState to treat them as open windows.
|
|
*/
|
|
_handleClosedWindows: function() {
|
|
var windowsEnum = Services.wm.getEnumerator("navigator:browser");
|
|
|
|
while (windowsEnum.hasMoreElements()) {
|
|
var window = windowsEnum.getNext();
|
|
if (window.closed) {
|
|
this.onClose(window);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* open a new browser window for a given session state
|
|
* called when restoring a multi-window session
|
|
* @param aState
|
|
* Object containing session data
|
|
*/
|
|
_openWindowWithState: function(aState) {
|
|
var argString = Cc["@mozilla.org/supports-string;1"].
|
|
createInstance(Ci.nsISupportsString);
|
|
argString.data = "";
|
|
|
|
// Build feature string
|
|
let features = "chrome,dialog=no,macsuppressanimation,all";
|
|
let winState = aState.windows[0];
|
|
WINDOW_ATTRIBUTES.forEach(function(aFeature) {
|
|
// Use !isNaN as an easy way to ignore sizemode and check for numbers
|
|
if (aFeature in winState && !isNaN(winState[aFeature]))
|
|
features += "," + aFeature + "=" + winState[aFeature];
|
|
});
|
|
|
|
if (winState.isPrivate) {
|
|
features += ",private";
|
|
}
|
|
|
|
var window =
|
|
Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"),
|
|
"_blank", features, argString);
|
|
|
|
do {
|
|
var ID = "window" + Math.random();
|
|
} while (ID in this._statesToRestore);
|
|
this._statesToRestore[(window.__SS_restoreID = ID)] = aState;
|
|
|
|
return window;
|
|
},
|
|
|
|
/**
|
|
* Whether or not to resume session, if not recovering from a crash.
|
|
* @returns bool
|
|
*/
|
|
_doResumeSession: function() {
|
|
return this._prefBranch.getIntPref("startup.page") == 3 ||
|
|
this._prefBranch.getBoolPref("sessionstore.resume_session_once");
|
|
},
|
|
|
|
/**
|
|
* whether the user wants to load any other page at startup
|
|
* (except the homepage) - needed for determining whether to overwrite the current tabs
|
|
* C.f.: nsBrowserContentHandler's defaultArgs implementation.
|
|
* @returns bool
|
|
*/
|
|
_isCmdLineEmpty: function(aWindow, aState) {
|
|
var pinnedOnly = aState.windows &&
|
|
aState.windows.every(function(win)
|
|
win.tabs.every(function(tab) tab.pinned));
|
|
|
|
let hasFirstArgument = aWindow.arguments && aWindow.arguments[0];
|
|
if (!pinnedOnly) {
|
|
let defaultArgs = Cc["@mozilla.org/browser/clh;1"].
|
|
getService(Ci.nsIBrowserHandler).defaultArgs;
|
|
if (aWindow.arguments &&
|
|
aWindow.arguments[0] &&
|
|
aWindow.arguments[0] == defaultArgs)
|
|
hasFirstArgument = false;
|
|
}
|
|
|
|
return !hasFirstArgument;
|
|
},
|
|
|
|
/**
|
|
* don't save sensitive data if the user doesn't want to
|
|
* (distinguishes between encrypted and non-encrypted sites)
|
|
* @param aIsHTTPS
|
|
* Bool is encrypted
|
|
* @param aUseDefaultPref
|
|
* don't do normal check for deferred
|
|
* @returns bool
|
|
*/
|
|
checkPrivacyLevel: function(aIsHTTPS, aUseDefaultPref) {
|
|
let pref = "sessionstore.privacy_level";
|
|
// If we're in the process of quitting and we're not autoresuming the session
|
|
// then we should treat it as a deferred session. We have a different privacy
|
|
// pref for that case.
|
|
if (!aUseDefaultPref && this._loadState == STATE_QUITTING && !this._doResumeSession())
|
|
pref = "sessionstore.privacy_level_deferred";
|
|
return this._prefBranch.getIntPref(pref) < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
|
|
},
|
|
|
|
/**
|
|
* on popup windows, the XULWindow's attributes seem not to be set correctly
|
|
* we use thus JSDOMWindow attributes for sizemode and normal window attributes
|
|
* (and hope for reasonable values when maximized/minimized - since then
|
|
* outerWidth/outerHeight aren't the dimensions of the restored window)
|
|
* @param aWindow
|
|
* Window reference
|
|
* @param aAttribute
|
|
* String sizemode | width | height | other window attribute
|
|
* @returns string
|
|
*/
|
|
_getWindowDimension: function(aWindow, aAttribute) {
|
|
if (aAttribute == "sizemode") {
|
|
switch (aWindow.windowState) {
|
|
case aWindow.STATE_FULLSCREEN:
|
|
case aWindow.STATE_MAXIMIZED:
|
|
return "maximized";
|
|
case aWindow.STATE_MINIMIZED:
|
|
return "minimized";
|
|
default:
|
|
return "normal";
|
|
}
|
|
}
|
|
|
|
var dimension;
|
|
switch (aAttribute) {
|
|
case "width":
|
|
dimension = aWindow.outerWidth;
|
|
break;
|
|
case "height":
|
|
dimension = aWindow.outerHeight;
|
|
break;
|
|
default:
|
|
dimension = aAttribute in aWindow ? aWindow[aAttribute] : "";
|
|
break;
|
|
}
|
|
|
|
if (aWindow.windowState == aWindow.STATE_NORMAL) {
|
|
return dimension;
|
|
}
|
|
return aWindow.document.documentElement.getAttribute(aAttribute) || dimension;
|
|
},
|
|
|
|
/**
|
|
* Get nsIURI from string
|
|
* @param string
|
|
* @returns nsIURI
|
|
*/
|
|
_getURIFromString: function(aString) {
|
|
return Services.io.newURI(aString, null, null);
|
|
},
|
|
|
|
/**
|
|
* @param aState is a session state
|
|
* @param aRecentCrashes is the number of consecutive crashes
|
|
* @returns whether a restore page will be needed for the session state
|
|
*/
|
|
_needsRestorePage: function(aState, aRecentCrashes) {
|
|
const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000;
|
|
|
|
// don't display the page when there's nothing to restore
|
|
let winData = aState.windows || null;
|
|
if (!winData || winData.length == 0)
|
|
return false;
|
|
|
|
// don't wrap a single about:sessionrestore page
|
|
if (winData.length == 1 && winData[0].tabs &&
|
|
winData[0].tabs.length == 1 && winData[0].tabs[0].entries &&
|
|
winData[0].tabs[0].entries.length == 1 &&
|
|
winData[0].tabs[0].entries[0].url == "about:sessionrestore")
|
|
return false;
|
|
|
|
// don't automatically restore in Safe Mode
|
|
if (Services.appinfo.inSafeMode)
|
|
return true;
|
|
|
|
let max_resumed_crashes =
|
|
this._prefBranch.getIntPref("sessionstore.max_resumed_crashes");
|
|
let sessionAge = aState.session && aState.session.lastUpdate &&
|
|
(Date.now() - aState.session.lastUpdate);
|
|
|
|
return max_resumed_crashes != -1 &&
|
|
(aRecentCrashes > max_resumed_crashes ||
|
|
sessionAge && sessionAge >= SIX_HOURS_IN_MS);
|
|
},
|
|
|
|
/**
|
|
* Determine if the tab state we're passed is something we should save. This
|
|
* is used when closing a tab or closing a window with a single tab
|
|
*
|
|
* @param aTabState
|
|
* The current tab state
|
|
* @returns boolean
|
|
*/
|
|
_shouldSaveTabState: function(aTabState) {
|
|
// If the tab has only a transient about: history entry, no other
|
|
// session history, and no userTypedValue, then we don't actually want to
|
|
// store this tab's data.
|
|
return aTabState.entries.length &&
|
|
!(aTabState.entries.length == 1 &&
|
|
(aTabState.entries[0].url == "about:blank" ||
|
|
aTabState.entries[0].url == "about:newtab") &&
|
|
!aTabState.userTypedValue);
|
|
},
|
|
|
|
/**
|
|
* Determine if we can restore history into this tab.
|
|
* This will be false when a tab has been removed (usually between
|
|
* restoreHistoryPrecursor && restoreHistory) or if the tab is still marked
|
|
* as loading.
|
|
*
|
|
* @param aTab
|
|
* @returns boolean
|
|
*/
|
|
_canRestoreTabHistory: function(aTab) {
|
|
return aTab.parentNode && aTab.linkedBrowser &&
|
|
aTab.linkedBrowser.__SS_tabStillLoading;
|
|
},
|
|
|
|
/**
|
|
* This is going to take a state as provided at startup (via
|
|
* nsISessionStartup.state) and split it into 2 parts. The first part
|
|
* (defaultState) will be a state that should still be restored at startup,
|
|
* while the second part (state) is a state that should be saved for later.
|
|
* defaultState will be comprised of windows with only pinned tabs, extracted
|
|
* from state. It will contain the cookies that go along with the history
|
|
* entries in those tabs. It will also contain window position information.
|
|
*
|
|
* defaultState will be restored at startup. state will be placed into
|
|
* this._lastSessionState and will be kept in case the user explicitly wants
|
|
* to restore the previous session (publicly exposed as restoreLastSession).
|
|
*
|
|
* @param state
|
|
* The state, presumably from nsISessionStartup.state
|
|
* @returns [defaultState, state]
|
|
*/
|
|
_prepDataForDeferredRestore: function(state) {
|
|
// Make sure that we don't modify the global state as provided by
|
|
// nsSessionStartup.state. Converting the object to a JSON string and
|
|
// parsing it again is the easiest way to do that, although not the most
|
|
// efficient one. Deferred sessions that don't have automatic session
|
|
// restore enabled tend to be a lot smaller though so that this shouldn't
|
|
// be a big perf hit.
|
|
state = JSON.parse(JSON.stringify(state));
|
|
|
|
let defaultState = { windows: [], selectedWindow: 1 };
|
|
|
|
state.selectedWindow = state.selectedWindow || 1;
|
|
|
|
// Look at each window, remove pinned tabs, adjust selectedindex,
|
|
// remove window if necessary.
|
|
for (let wIndex = 0; wIndex < state.windows.length;) {
|
|
let window = state.windows[wIndex];
|
|
window.selected = window.selected || 1;
|
|
// We're going to put the state of the window into this object
|
|
let pinnedWindowState = { tabs: [], cookies: []};
|
|
for (let tIndex = 0; tIndex < window.tabs.length;) {
|
|
if (window.tabs[tIndex].pinned) {
|
|
// Adjust window.selected
|
|
if (tIndex + 1 < window.selected)
|
|
window.selected -= 1;
|
|
else if (tIndex + 1 == window.selected)
|
|
pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
|
|
// + 2 because the tab isn't actually in the array yet
|
|
|
|
// Now add the pinned tab to our window
|
|
pinnedWindowState.tabs =
|
|
pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1));
|
|
// We don't want to increment tIndex here.
|
|
continue;
|
|
}
|
|
tIndex++;
|
|
}
|
|
|
|
// At this point the window in the state object has been modified (or not)
|
|
// We want to build the rest of this new window object if we have pinnedTabs.
|
|
if (pinnedWindowState.tabs.length) {
|
|
// First get the other attributes off the window
|
|
WINDOW_ATTRIBUTES.forEach(function(attr) {
|
|
if (attr in window) {
|
|
pinnedWindowState[attr] = window[attr];
|
|
delete window[attr];
|
|
}
|
|
});
|
|
// We're just copying position data into the pinned window.
|
|
// Not copying over:
|
|
// - _closedTabs
|
|
// - extData
|
|
// - isPopup
|
|
// - hidden
|
|
|
|
// Assign a unique ID to correlate the window to be opened with the
|
|
// remaining data
|
|
window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID
|
|
= "" + Date.now() + Math.random();
|
|
|
|
// Extract the cookies that belong with each pinned tab
|
|
this._splitCookiesFromWindow(window, pinnedWindowState);
|
|
|
|
// Actually add this window to our defaultState
|
|
defaultState.windows.push(pinnedWindowState);
|
|
// Remove the window from the state if it doesn't have any tabs
|
|
if (!window.tabs.length) {
|
|
if (wIndex + 1 <= state.selectedWindow)
|
|
state.selectedWindow -= 1;
|
|
else if (wIndex + 1 == state.selectedWindow)
|
|
defaultState.selectedIndex = defaultState.windows.length + 1;
|
|
|
|
state.windows.splice(wIndex, 1);
|
|
// We don't want to increment wIndex here.
|
|
continue;
|
|
}
|
|
|
|
|
|
}
|
|
wIndex++;
|
|
}
|
|
|
|
return [defaultState, state];
|
|
},
|
|
|
|
/**
|
|
* Splits out the cookies from aWinState into aTargetWinState based on the
|
|
* tabs that are in aTargetWinState.
|
|
* This alters the state of aWinState and aTargetWinState.
|
|
*/
|
|
_splitCookiesFromWindow:
|
|
function(aWinState, aTargetWinState) {
|
|
if (!aWinState.cookies || !aWinState.cookies.length)
|
|
return;
|
|
|
|
// Get the hosts for history entries in aTargetWinState
|
|
let cookieHosts = {};
|
|
aTargetWinState.tabs.forEach(function(tab) {
|
|
tab.entries.forEach(function(entry) {
|
|
this._extractHostsForCookiesFromEntry(entry, cookieHosts, false);
|
|
}, this);
|
|
}, this);
|
|
|
|
// By creating a regex we reduce overhead and there is only one loop pass
|
|
// through either array (cookieHosts and aWinState.cookies).
|
|
let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g");
|
|
// If we don't actually have any hosts, then we don't want to do anything.
|
|
if (!hosts.length)
|
|
return;
|
|
let cookieRegex = new RegExp(".*(" + hosts + ")");
|
|
for (let cIndex = 0; cIndex < aWinState.cookies.length;) {
|
|
if (cookieRegex.test(aWinState.cookies[cIndex].host)) {
|
|
aTargetWinState.cookies =
|
|
aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1));
|
|
continue;
|
|
}
|
|
cIndex++;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Converts a JavaScript object into a JSON string
|
|
* (see http://www.json.org/ for more information).
|
|
*
|
|
* The inverse operation consists of JSON.parse(JSON_string).
|
|
*
|
|
* @param aJSObject is the object to be converted
|
|
* @returns the object's JSON representation
|
|
*/
|
|
_toJSONString: function(aJSObject) {
|
|
return JSON.stringify(aJSObject);
|
|
},
|
|
|
|
_sendRestoreCompletedNotifications: function() {
|
|
// not all windows restored, yet
|
|
if (this._restoreCount > 1) {
|
|
this._restoreCount--;
|
|
return;
|
|
}
|
|
|
|
// observers were already notified
|
|
if (this._restoreCount == -1)
|
|
return;
|
|
|
|
// This was the last window restored at startup, notify observers.
|
|
Services.obs.notifyObservers(null,
|
|
this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED,
|
|
"");
|
|
|
|
this._browserSetState = false;
|
|
this._restoreCount = -1;
|
|
},
|
|
|
|
/**
|
|
* Set the given window's busy state
|
|
* @param aWindow the window
|
|
* @param aValue the window's busy state
|
|
*/
|
|
_setWindowStateBusyValue:
|
|
function(aWindow, aValue) {
|
|
|
|
this._windows[aWindow.__SSi].busy = aValue;
|
|
|
|
// Keep the to-be-restored state in sync because that is returned by
|
|
// getWindowState() as long as the window isn't loaded, yet.
|
|
if (!this._isWindowLoaded(aWindow)) {
|
|
let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0];
|
|
stateToRestore.busy = aValue;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set the given window's state to 'not busy'.
|
|
* @param aWindow the window
|
|
*/
|
|
_setWindowStateReady: function(aWindow) {
|
|
this._setWindowStateBusyValue(aWindow, false);
|
|
this._sendWindowStateEvent(aWindow, "Ready");
|
|
},
|
|
|
|
/**
|
|
* Set the given window's state to 'busy'.
|
|
* @param aWindow the window
|
|
*/
|
|
_setWindowStateBusy: function(aWindow) {
|
|
this._setWindowStateBusyValue(aWindow, true);
|
|
this._sendWindowStateEvent(aWindow, "Busy");
|
|
},
|
|
|
|
/**
|
|
* Dispatch an SSWindowState_____ event for the given window.
|
|
* @param aWindow the window
|
|
* @param aType the type of event, SSWindowState will be prepended to this string
|
|
*/
|
|
_sendWindowStateEvent: function(aWindow, aType) {
|
|
let event = aWindow.document.createEvent("Events");
|
|
event.initEvent("SSWindowState" + aType, true, false);
|
|
aWindow.dispatchEvent(event);
|
|
},
|
|
|
|
/**
|
|
* Dispatch the SSTabRestored event for the given tab.
|
|
* @param aTab the which has been restored
|
|
*/
|
|
_sendTabRestoredNotification: function(aTab) {
|
|
let event = aTab.ownerDocument.createEvent("Events");
|
|
event.initEvent("SSTabRestored", true, false);
|
|
aTab.dispatchEvent(event);
|
|
},
|
|
|
|
/**
|
|
* @param aWindow
|
|
* Window reference
|
|
* @returns whether this window's data is still cached in _statesToRestore
|
|
* because it's not fully loaded yet
|
|
*/
|
|
_isWindowLoaded: function(aWindow) {
|
|
return !aWindow.__SS_restoreID;
|
|
},
|
|
|
|
/**
|
|
* Replace "Loading..." with the tab label (with minimal side-effects)
|
|
* @param aString is the string the title is stored in
|
|
* @param aTabbrowser is a tabbrowser object, containing aTab
|
|
* @param aTab is the tab whose title we're updating & using
|
|
*
|
|
* @returns aString that has been updated with the new title
|
|
*/
|
|
_replaceLoadingTitle : function(aString, aTabbrowser, aTab) {
|
|
if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) {
|
|
aTabbrowser.setTabTitle(aTab);
|
|
[aString, aTab.label] = [aTab.label, aString];
|
|
}
|
|
return aString;
|
|
},
|
|
|
|
/**
|
|
* Resize this._closedWindows to the value of the pref, except in the case
|
|
* where we don't have any non-popup windows on Windows and Linux. Then we must
|
|
* resize such that we have at least one non-popup window.
|
|
*/
|
|
_capClosedWindows : function() {
|
|
if (this._closedWindows.length <= this._max_windows_undo)
|
|
return;
|
|
let spliceTo = this._max_windows_undo;
|
|
#ifndef XP_MACOSX
|
|
let normalWindowIndex = 0;
|
|
// try to find a non-popup window in this._closedWindows
|
|
while (normalWindowIndex < this._closedWindows.length &&
|
|
!!this._closedWindows[normalWindowIndex].isPopup)
|
|
normalWindowIndex++;
|
|
if (normalWindowIndex >= this._max_windows_undo)
|
|
spliceTo = normalWindowIndex + 1;
|
|
#endif
|
|
this._closedWindows.splice(spliceTo, this._closedWindows.length);
|
|
},
|
|
|
|
_clearRestoringWindows: function() {
|
|
for (let i = 0; i < this._closedWindows.length; i++) {
|
|
delete this._closedWindows[i]._shouldRestore;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reset state to prepare for a new session state to be restored.
|
|
*/
|
|
_resetRestoringState: function() {
|
|
TabRestoreQueue.reset();
|
|
this._tabsRestoringCount = 0;
|
|
},
|
|
|
|
/**
|
|
* Reset the restoring state for a particular tab. This will be called when
|
|
* removing a tab or when a tab needs to be reset (it's being overwritten).
|
|
*
|
|
* @param aTab
|
|
* The tab that will be "reset"
|
|
*/
|
|
_resetTabRestoringState: function(aTab) {
|
|
let window = aTab.ownerDocument.defaultView;
|
|
let browser = aTab.linkedBrowser;
|
|
|
|
// Keep the tab's previous state for later in this method
|
|
let previousState = browser.__SS_restoreState;
|
|
|
|
// The browser is no longer in any sort of restoring state.
|
|
delete browser.__SS_restoreState;
|
|
|
|
aTab.removeAttribute("pending");
|
|
browser.removeAttribute("pending");
|
|
|
|
// We want to decrement window.__SS_tabsToRestore here so that we always
|
|
// decrement it AFTER a tab is done restoring or when a tab gets "reset".
|
|
window.__SS_tabsToRestore--;
|
|
|
|
// Remove the progress listener if we should.
|
|
this._removeTabsProgressListener(window);
|
|
|
|
if (previousState == TAB_STATE_RESTORING) {
|
|
if (this._tabsRestoringCount)
|
|
this._tabsRestoringCount--;
|
|
}
|
|
else if (previousState == TAB_STATE_NEEDS_RESTORE) {
|
|
// Make sure the session history listener is removed. This is normally
|
|
// done in restoreTab, but this tab is being removed before that gets called.
|
|
this._removeSHistoryListener(aTab);
|
|
|
|
// Make sure that the tab is removed from the list of tabs to restore.
|
|
// Again, this is normally done in restoreTab, but that isn't being called
|
|
// for this tab.
|
|
TabRestoreQueue.remove(aTab);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add the tabs progress listener to the window if it isn't already
|
|
*
|
|
* @param aWindow
|
|
* The window to add our progress listener to
|
|
*/
|
|
_ensureTabsProgressListener: function(aWindow) {
|
|
let tabbrowser = aWindow.gBrowser;
|
|
if (tabbrowser.mTabsProgressListeners.indexOf(gRestoreTabsProgressListener) == -1)
|
|
tabbrowser.addTabsProgressListener(gRestoreTabsProgressListener);
|
|
},
|
|
|
|
/**
|
|
* Attempt to remove the tabs progress listener from the window.
|
|
*
|
|
* @param aWindow
|
|
* The window from which to remove our progress listener from
|
|
*/
|
|
_removeTabsProgressListener: function(aWindow) {
|
|
// If there are no tabs left to restore (or restoring) in this window, then
|
|
// we can safely remove the progress listener from this window.
|
|
if (!aWindow.__SS_tabsToRestore)
|
|
aWindow.gBrowser.removeTabsProgressListener(gRestoreTabsProgressListener);
|
|
},
|
|
|
|
/**
|
|
* Remove the session history listener from the tab's browser if there is one.
|
|
*
|
|
* @param aTab
|
|
* The tab who's browser to remove the listener
|
|
*/
|
|
_removeSHistoryListener: function(aTab) {
|
|
let browser = aTab.linkedBrowser;
|
|
if (browser.__SS_shistoryListener) {
|
|
browser.webNavigation.sessionHistory.
|
|
removeSHistoryListener(browser.__SS_shistoryListener);
|
|
delete browser.__SS_shistoryListener;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Priority queue that keeps track of a list of tabs to restore and returns
|
|
* the tab we should restore next, based on priority rules. We decide between
|
|
* pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only
|
|
* restored with restore_hidden_tabs=true.
|
|
*/
|
|
var TabRestoreQueue = {
|
|
// The separate buckets used to store tabs.
|
|
tabs: {priority: [], visible: [], hidden: []},
|
|
|
|
// Preferences used by the TabRestoreQueue to determine which tabs
|
|
// are restored automatically and which tabs will be on-demand.
|
|
prefs: {
|
|
// Lazy getter that returns whether tabs are restored on demand.
|
|
get restoreOnDemand() {
|
|
let updateValue = () => {
|
|
let value = Services.prefs.getBoolPref(PREF);
|
|
let definition = {value: value, configurable: true};
|
|
Object.defineProperty(this, "restoreOnDemand", definition);
|
|
return value;
|
|
}
|
|
|
|
const PREF = "browser.sessionstore.restore_on_demand";
|
|
Services.prefs.addObserver(PREF, updateValue, false);
|
|
return updateValue();
|
|
},
|
|
|
|
// Lazy getter that returns whether pinned tabs are restored on demand.
|
|
get restorePinnedTabsOnDemand() {
|
|
let updateValue = () => {
|
|
let value = Services.prefs.getBoolPref(PREF);
|
|
let definition = {value: value, configurable: true};
|
|
Object.defineProperty(this, "restorePinnedTabsOnDemand", definition);
|
|
return value;
|
|
}
|
|
|
|
const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand";
|
|
Services.prefs.addObserver(PREF, updateValue, false);
|
|
return updateValue();
|
|
},
|
|
|
|
// Lazy getter that returns whether we should restore hidden tabs.
|
|
get restoreHiddenTabs() {
|
|
let updateValue = () => {
|
|
let value = Services.prefs.getBoolPref(PREF);
|
|
let definition = {value: value, configurable: true};
|
|
Object.defineProperty(this, "restoreHiddenTabs", definition);
|
|
return value;
|
|
}
|
|
|
|
const PREF = "browser.sessionstore.restore_hidden_tabs";
|
|
Services.prefs.addObserver(PREF, updateValue, false);
|
|
return updateValue();
|
|
}
|
|
},
|
|
|
|
// Resets the queue and removes all tabs.
|
|
reset: function() {
|
|
this.tabs = {priority: [], visible: [], hidden: []};
|
|
},
|
|
|
|
// Adds a tab to the queue and determines its priority bucket.
|
|
add: function(tab) {
|
|
let {priority, hidden, visible} = this.tabs;
|
|
|
|
if (tab.pinned) {
|
|
priority.push(tab);
|
|
} else if (tab.hidden) {
|
|
hidden.push(tab);
|
|
} else {
|
|
visible.push(tab);
|
|
}
|
|
},
|
|
|
|
// Removes a given tab from the queue, if it's in there.
|
|
remove: function(tab) {
|
|
let {priority, hidden, visible} = this.tabs;
|
|
|
|
// We'll always check priority first since we don't
|
|
// have an indicator if a tab will be there or not.
|
|
let set = priority;
|
|
let index = set.indexOf(tab);
|
|
|
|
if (index == -1) {
|
|
set = tab.hidden ? hidden : visible;
|
|
index = set.indexOf(tab);
|
|
}
|
|
|
|
if (index > -1) {
|
|
set.splice(index, 1);
|
|
}
|
|
},
|
|
|
|
// Returns and removes the tab with the highest priority.
|
|
shift: function() {
|
|
let set;
|
|
let {priority, hidden, visible} = this.tabs;
|
|
|
|
let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs;
|
|
let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
|
|
if (restorePinned && priority.length) {
|
|
set = priority;
|
|
} else if (!restoreOnDemand) {
|
|
if (visible.length) {
|
|
set = visible;
|
|
} else if (this.prefs.restoreHiddenTabs && hidden.length) {
|
|
set = hidden;
|
|
}
|
|
}
|
|
|
|
return set && set.shift();
|
|
},
|
|
|
|
// Moves a given tab from the 'hidden' to the 'visible' bucket.
|
|
hiddenToVisible: function(tab) {
|
|
let {hidden, visible} = this.tabs;
|
|
let index = hidden.indexOf(tab);
|
|
|
|
if (index > -1) {
|
|
hidden.splice(index, 1);
|
|
visible.push(tab);
|
|
} else {
|
|
throw new Error("restore queue: hidden tab not found");
|
|
}
|
|
},
|
|
|
|
// Moves a given tab from the 'visible' to the 'hidden' bucket.
|
|
visibleToHidden: function(tab) {
|
|
let {visible, hidden} = this.tabs;
|
|
let index = visible.indexOf(tab);
|
|
|
|
if (index > -1) {
|
|
visible.splice(index, 1);
|
|
hidden.push(tab);
|
|
} else {
|
|
throw new Error("restore queue: visible tab not found");
|
|
}
|
|
}
|
|
};
|
|
|
|
// A map storing a closed window's state data until it goes aways (is GC'ed).
|
|
// This ensures that API clients can still read (but not write) states of
|
|
// windows they still hold a reference to but we don't.
|
|
var DyingWindowCache = {
|
|
_data: new WeakMap(),
|
|
|
|
has: function(window) {
|
|
return this._data.has(window);
|
|
},
|
|
|
|
get: function(window) {
|
|
return this._data.get(window);
|
|
},
|
|
|
|
set: function(window, data) {
|
|
this._data.set(window, data);
|
|
},
|
|
|
|
remove: function(window) {
|
|
this._data.delete(window);
|
|
}
|
|
};
|
|
|
|
// A set of tab attributes to persist. We will read a given list of tab
|
|
// attributes when collecting tab data and will re-set those attributes when
|
|
// the given tab data is restored to a new tab.
|
|
var TabAttributes = {
|
|
_attrs: new Set(),
|
|
|
|
// We never want to directly read or write those attributes.
|
|
// 'image' should not be accessed directly but handled by using the
|
|
// gBrowser.getIcon()/setIcon() methods.
|
|
// 'pending' is used internal by sessionstore and managed accordingly.
|
|
// 'skipbackgroundnotify' is used internal by tabbrowser.xml.
|
|
_skipAttrs: new Set(["image", "pending", "skipbackgroundnotify"]),
|
|
|
|
persist: function(name) {
|
|
if (this._attrs.has(name) || this._skipAttrs.has(name)) {
|
|
return false;
|
|
}
|
|
|
|
this._attrs.add(name);
|
|
return true;
|
|
},
|
|
|
|
get: function(tab) {
|
|
let data = {};
|
|
|
|
for (let name of this._attrs) {
|
|
if (tab.hasAttribute(name)) {
|
|
data[name] = tab.getAttribute(name);
|
|
}
|
|
}
|
|
|
|
return data;
|
|
},
|
|
|
|
set: function(tab, data = {}) {
|
|
// Clear attributes.
|
|
for (let name of this._attrs) {
|
|
tab.removeAttribute(name);
|
|
}
|
|
|
|
// Set attributes.
|
|
for (let name in data) {
|
|
tab.setAttribute(name, data[name]);
|
|
}
|
|
}
|
|
};
|
|
|
|
// This is used to help meter the number of restoring tabs. This is the control
|
|
// point for telling the next tab to restore. It gets attached to each gBrowser
|
|
// via gBrowser.addTabsProgressListener
|
|
var gRestoreTabsProgressListener = {
|
|
onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
|
|
// Ignore state changes on browsers that we've already restored and state
|
|
// changes that aren't applicable.
|
|
if (aBrowser.__SS_restoreState &&
|
|
aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
|
|
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
|
|
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
|
|
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
|
|
// We need to reset the tab before starting the next restore.
|
|
let win = aBrowser.ownerDocument.defaultView;
|
|
let tab = win.gBrowser.getTabForBrowser(aBrowser);
|
|
SessionStoreInternal._resetTabRestoringState(tab);
|
|
SessionStoreInternal.restoreNextTab();
|
|
}
|
|
}
|
|
};
|
|
|
|
// A SessionStoreSHistoryListener will be attached to each browser before it is
|
|
// restored. We need to catch reloads that occur before the tab is restored
|
|
// because otherwise, docShell will reload an old URI (usually about:blank).
|
|
function SessionStoreSHistoryListener(aTab) {
|
|
this.tab = aTab;
|
|
}
|
|
SessionStoreSHistoryListener.prototype = {
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsISHistoryListener,
|
|
Ci.nsISupportsWeakReference
|
|
]),
|
|
browser: null,
|
|
OnHistoryNewEntry: function(aNewURI) { },
|
|
OnHistoryGoBack: function(aBackURI) { return true; },
|
|
OnHistoryGoForward: function(aForwardURI) { return true; },
|
|
OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; },
|
|
OnHistoryPurge: function(aNumEntries) { return true; },
|
|
OnHistoryReload: function(aReloadURI, aReloadFlags) {
|
|
// On reload, we want to make sure that session history loads the right
|
|
// URI. In order to do that, we will juet call restoreTab. That will remove
|
|
// the history listener and load the right URI.
|
|
SessionStoreInternal.restoreTab(this.tab);
|
|
// Returning false will stop the load that docshell is attempting.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// See toolkit/forgetaboutsite/ForgetAboutSite.jsm
|
|
String.prototype.hasRootDomain = function hasRootDomain(aDomain) {
|
|
let index = this.indexOf(aDomain);
|
|
if (index == -1)
|
|
return false;
|
|
|
|
if (this == aDomain)
|
|
return true;
|
|
|
|
let prevChar = this[index - 1];
|
|
return (index == (this.length - aDomain.length)) &&
|
|
(prevChar == "." || prevChar == "/");
|
|
}
|