summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sessionstore')
-rw-r--r--browser/components/sessionstore/ContentRestore.jsm431
-rw-r--r--browser/components/sessionstore/DocShellCapabilities.jsm50
-rw-r--r--browser/components/sessionstore/FrameTree.jsm254
-rw-r--r--browser/components/sessionstore/GlobalState.jsm84
-rw-r--r--browser/components/sessionstore/PageStyle.jsm100
-rw-r--r--browser/components/sessionstore/PrivacyFilter.jsm135
-rw-r--r--browser/components/sessionstore/PrivacyLevel.jsm64
-rw-r--r--browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm214
-rw-r--r--browser/components/sessionstore/RunState.jsm96
-rw-r--r--browser/components/sessionstore/SessionCookies.jsm476
-rw-r--r--browser/components/sessionstore/SessionFile.jsm399
-rw-r--r--browser/components/sessionstore/SessionHistory.jsm428
-rw-r--r--browser/components/sessionstore/SessionMigration.jsm100
-rw-r--r--browser/components/sessionstore/SessionSaver.jsm264
-rw-r--r--browser/components/sessionstore/SessionStorage.jsm173
-rw-r--r--browser/components/sessionstore/SessionStore.jsm4719
-rw-r--r--browser/components/sessionstore/SessionWorker.js381
-rw-r--r--browser/components/sessionstore/SessionWorker.jsm25
-rw-r--r--browser/components/sessionstore/StartupPerformance.jsm234
-rw-r--r--browser/components/sessionstore/TabAttributes.jsm74
-rw-r--r--browser/components/sessionstore/TabState.jsm196
-rw-r--r--browser/components/sessionstore/TabStateCache.jsm163
-rw-r--r--browser/components/sessionstore/TabStateFlusher.jsm184
-rw-r--r--browser/components/sessionstore/content/aboutSessionRestore.js362
-rw-r--r--browser/components/sessionstore/content/aboutSessionRestore.xhtml86
-rw-r--r--browser/components/sessionstore/content/content-sessionStore.js897
-rw-r--r--browser/components/sessionstore/jar.mn8
-rw-r--r--browser/components/sessionstore/moz.build52
-rw-r--r--browser/components/sessionstore/nsISessionStartup.idl66
-rw-r--r--browser/components/sessionstore/nsISessionStore.idl220
-rw-r--r--browser/components/sessionstore/nsSessionStartup.js353
-rw-r--r--browser/components/sessionstore/nsSessionStore.js39
-rw-r--r--browser/components/sessionstore/nsSessionStore.manifest15
-rw-r--r--browser/components/sessionstore/test/.eslintrc.js7
-rw-r--r--browser/components/sessionstore/test/browser.ini242
-rw-r--r--browser/components/sessionstore/test/browser_1234021.js18
-rw-r--r--browser/components/sessionstore/test/browser_1234021_page.html6
-rw-r--r--browser/components/sessionstore/test/browser_248970_b_perwindowpb.js166
-rw-r--r--browser/components/sessionstore/test/browser_248970_b_sample.html37
-rw-r--r--browser/components/sessionstore/test/browser_339445.js32
-rw-r--r--browser/components/sessionstore/test/browser_339445_sample.html18
-rw-r--r--browser/components/sessionstore/test/browser_345898.js44
-rw-r--r--browser/components/sessionstore/test/browser_350525.js102
-rw-r--r--browser/components/sessionstore/test/browser_354894_perwindowpb.js474
-rw-r--r--browser/components/sessionstore/test/browser_367052.js41
-rw-r--r--browser/components/sessionstore/test/browser_393716.js71
-rw-r--r--browser/components/sessionstore/test/browser_394759_basic.js92
-rw-r--r--browser/components/sessionstore/test/browser_394759_behavior.js76
-rw-r--r--browser/components/sessionstore/test/browser_394759_perwindowpb.js55
-rw-r--r--browser/components/sessionstore/test/browser_394759_purge.js130
-rw-r--r--browser/components/sessionstore/test/browser_423132.js59
-rw-r--r--browser/components/sessionstore/test/browser_423132_sample.html14
-rw-r--r--browser/components/sessionstore/test/browser_447951.js65
-rw-r--r--browser/components/sessionstore/test/browser_447951_sample.html5
-rw-r--r--browser/components/sessionstore/test/browser_454908.js47
-rw-r--r--browser/components/sessionstore/test/browser_454908_sample.html8
-rw-r--r--browser/components/sessionstore/test/browser_456342.js49
-rw-r--r--browser/components/sessionstore/test/browser_456342_sample.xhtml36
-rw-r--r--browser/components/sessionstore/test/browser_459906.js62
-rw-r--r--browser/components/sessionstore/test/browser_459906_empty.html3
-rw-r--r--browser/components/sessionstore/test/browser_459906_sample.html41
-rw-r--r--browser/components/sessionstore/test/browser_461634.js85
-rw-r--r--browser/components/sessionstore/test/browser_461743.js39
-rw-r--r--browser/components/sessionstore/test/browser_461743_sample.html56
-rw-r--r--browser/components/sessionstore/test/browser_463205.js40
-rw-r--r--browser/components/sessionstore/test/browser_463205_sample.html7
-rw-r--r--browser/components/sessionstore/test/browser_463206.js53
-rw-r--r--browser/components/sessionstore/test/browser_463206_sample.html11
-rw-r--r--browser/components/sessionstore/test/browser_464199.js85
-rw-r--r--browser/components/sessionstore/test/browser_464620_a.html54
-rw-r--r--browser/components/sessionstore/test/browser_464620_a.js48
-rw-r--r--browser/components/sessionstore/test/browser_464620_b.html58
-rw-r--r--browser/components/sessionstore/test/browser_464620_b.js48
-rw-r--r--browser/components/sessionstore/test/browser_464620_xd.html5
-rw-r--r--browser/components/sessionstore/test/browser_465215.js28
-rw-r--r--browser/components/sessionstore/test/browser_465223.js45
-rw-r--r--browser/components/sessionstore/test/browser_466937.js42
-rw-r--r--browser/components/sessionstore/test/browser_466937_sample.html22
-rw-r--r--browser/components/sessionstore/test/browser_467409-backslashplosion.js74
-rw-r--r--browser/components/sessionstore/test/browser_477657.js60
-rw-r--r--browser/components/sessionstore/test/browser_480893.js47
-rw-r--r--browser/components/sessionstore/test/browser_485482.js37
-rw-r--r--browser/components/sessionstore/test/browser_485482_sample.html12
-rw-r--r--browser/components/sessionstore/test/browser_485563.js26
-rw-r--r--browser/components/sessionstore/test/browser_490040.js65
-rw-r--r--browser/components/sessionstore/test/browser_491168.js42
-rw-r--r--browser/components/sessionstore/test/browser_491577.js120
-rw-r--r--browser/components/sessionstore/test/browser_495495.js46
-rw-r--r--browser/components/sessionstore/test/browser_500328.js120
-rw-r--r--browser/components/sessionstore/test/browser_506482.js73
-rw-r--r--browser/components/sessionstore/test/browser_514751.js38
-rw-r--r--browser/components/sessionstore/test/browser_522375.js21
-rw-r--r--browser/components/sessionstore/test/browser_522545.js269
-rw-r--r--browser/components/sessionstore/test/browser_524745.js42
-rw-r--r--browser/components/sessionstore/test/browser_526613.js72
-rw-r--r--browser/components/sessionstore/test/browser_528776.js21
-rw-r--r--browser/components/sessionstore/test/browser_579868.js30
-rw-r--r--browser/components/sessionstore/test/browser_579879.js20
-rw-r--r--browser/components/sessionstore/test/browser_580512.js81
-rw-r--r--browser/components/sessionstore/test/browser_581937.js19
-rw-r--r--browser/components/sessionstore/test/browser_586068-apptabs.js58
-rw-r--r--browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js53
-rw-r--r--browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js113
-rw-r--r--browser/components/sessionstore/test/browser_586068-cascade.js54
-rw-r--r--browser/components/sessionstore/test/browser_586068-multi_window.js70
-rw-r--r--browser/components/sessionstore/test/browser_586068-reload.js54
-rw-r--r--browser/components/sessionstore/test/browser_586068-select.js69
-rw-r--r--browser/components/sessionstore/test/browser_586068-window_state.js59
-rw-r--r--browser/components/sessionstore/test/browser_586068-window_state_override.js59
-rw-r--r--browser/components/sessionstore/test/browser_586147.js52
-rw-r--r--browser/components/sessionstore/test/browser_588426.js41
-rw-r--r--browser/components/sessionstore/test/browser_589246.js242
-rw-r--r--browser/components/sessionstore/test/browser_590268.js137
-rw-r--r--browser/components/sessionstore/test/browser_590563.js74
-rw-r--r--browser/components/sessionstore/test/browser_595601-restore_hidden.js112
-rw-r--r--browser/components/sessionstore/test/browser_597071.js36
-rw-r--r--browser/components/sessionstore/test/browser_599909.js120
-rw-r--r--browser/components/sessionstore/test/browser_600545.js89
-rw-r--r--browser/components/sessionstore/test/browser_601955.js54
-rw-r--r--browser/components/sessionstore/test/browser_607016.js98
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events.js361
-rw-r--r--browser/components/sessionstore/test/browser_618151.js65
-rw-r--r--browser/components/sessionstore/test/browser_623779.js13
-rw-r--r--browser/components/sessionstore/test/browser_624727.js35
-rw-r--r--browser/components/sessionstore/test/browser_625016.js82
-rw-r--r--browser/components/sessionstore/test/browser_628270.js52
-rw-r--r--browser/components/sessionstore/test/browser_635418.js55
-rw-r--r--browser/components/sessionstore/test/browser_636279.js101
-rw-r--r--browser/components/sessionstore/test/browser_637020.js66
-rw-r--r--browser/components/sessionstore/test/browser_637020_slow.sjs21
-rw-r--r--browser/components/sessionstore/test/browser_644409-scratchpads.js68
-rw-r--r--browser/components/sessionstore/test/browser_645428.js22
-rw-r--r--browser/components/sessionstore/test/browser_659591.js33
-rw-r--r--browser/components/sessionstore/test/browser_662743.js110
-rw-r--r--browser/components/sessionstore/test/browser_662743_sample.html15
-rw-r--r--browser/components/sessionstore/test/browser_662812.js36
-rw-r--r--browser/components/sessionstore/test/browser_665702-state_session.js24
-rw-r--r--browser/components/sessionstore/test/browser_682507.js16
-rw-r--r--browser/components/sessionstore/test/browser_687710.js44
-rw-r--r--browser/components/sessionstore/test/browser_687710_2.js64
-rw-r--r--browser/components/sessionstore/test/browser_694378.js33
-rw-r--r--browser/components/sessionstore/test/browser_701377.js41
-rw-r--r--browser/components/sessionstore/test/browser_705597.js58
-rw-r--r--browser/components/sessionstore/test/browser_707862.js61
-rw-r--r--browser/components/sessionstore/test/browser_739531.js47
-rw-r--r--browser/components/sessionstore/test/browser_739531_sample.html25
-rw-r--r--browser/components/sessionstore/test/browser_739805.js41
-rw-r--r--browser/components/sessionstore/test/browser_819510_perwindowpb.js120
-rw-r--r--browser/components/sessionstore/test/browser_911547.js63
-rw-r--r--browser/components/sessionstore/test/browser_911547_sample.html19
-rw-r--r--browser/components/sessionstore/test/browser_911547_sample.html^headers^1
-rw-r--r--browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js21
-rw-r--r--browser/components/sessionstore/test/browser_aboutSessionRestore.js55
-rw-r--r--browser/components/sessionstore/test/browser_async_duplicate_tab.js78
-rw-r--r--browser/components/sessionstore/test/browser_async_flushes.js113
-rw-r--r--browser/components/sessionstore/test/browser_async_remove_tab.js242
-rw-r--r--browser/components/sessionstore/test/browser_async_window_flushing.js178
-rw-r--r--browser/components/sessionstore/test/browser_attributes.js73
-rw-r--r--browser/components/sessionstore/test/browser_background_tab_crash.js221
-rw-r--r--browser/components/sessionstore/test/browser_backup_recovery.js206
-rw-r--r--browser/components/sessionstore/test/browser_broadcast.js131
-rw-r--r--browser/components/sessionstore/test/browser_capabilities.js76
-rw-r--r--browser/components/sessionstore/test/browser_cleaner.js157
-rw-r--r--browser/components/sessionstore/test/browser_cookies.js173
-rw-r--r--browser/components/sessionstore/test/browser_cookies.sjs21
-rw-r--r--browser/components/sessionstore/test/browser_crashedTabs.js462
-rw-r--r--browser/components/sessionstore/test/browser_dying_cache.js66
-rw-r--r--browser/components/sessionstore/test/browser_dynamic_frames.js87
-rw-r--r--browser/components/sessionstore/test/browser_forget_async_closings.js144
-rw-r--r--browser/components/sessionstore/test/browser_form_restore_events.js63
-rw-r--r--browser/components/sessionstore/test/browser_form_restore_events_sample.html99
-rw-r--r--browser/components/sessionstore/test/browser_formdata.js194
-rw-r--r--browser/components/sessionstore/test/browser_formdata_cc.js79
-rw-r--r--browser/components/sessionstore/test/browser_formdata_format.js113
-rw-r--r--browser/components/sessionstore/test/browser_formdata_format_sample.html7
-rw-r--r--browser/components/sessionstore/test/browser_formdata_sample.html20
-rw-r--r--browser/components/sessionstore/test/browser_formdata_xpath.js151
-rw-r--r--browser/components/sessionstore/test/browser_formdata_xpath_sample.html37
-rw-r--r--browser/components/sessionstore/test/browser_frame_history.js170
-rwxr-xr-xbrowser/components/sessionstore/test/browser_frame_history_a.html5
-rwxr-xr-xbrowser/components/sessionstore/test/browser_frame_history_b.html10
-rwxr-xr-xbrowser/components/sessionstore/test/browser_frame_history_c.html5
-rwxr-xr-xbrowser/components/sessionstore/test/browser_frame_history_c1.html5
-rwxr-xr-xbrowser/components/sessionstore/test/browser_frame_history_c2.html5
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_index.html10
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_index2.html4
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_index_blank.html5
-rw-r--r--browser/components/sessionstore/test/browser_frametree.js131
-rw-r--r--browser/components/sessionstore/test/browser_frametree_sample.html8
-rw-r--r--browser/components/sessionstore/test/browser_frametree_sample_frameset.html11
-rw-r--r--browser/components/sessionstore/test/browser_global_store.js45
-rw-r--r--browser/components/sessionstore/test/browser_history_persist.js93
-rw-r--r--browser/components/sessionstore/test/browser_label_and_icon.js53
-rw-r--r--browser/components/sessionstore/test/browser_merge_closed_tabs.js71
-rw-r--r--browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js36
-rw-r--r--browser/components/sessionstore/test/browser_newtab_userTypedValue.js72
-rw-r--r--browser/components/sessionstore/test/browser_pageStyle.js89
-rw-r--r--browser/components/sessionstore/test/browser_pageStyle_sample.html16
-rw-r--r--browser/components/sessionstore/test/browser_pageStyle_sample_nested.html9
-rw-r--r--browser/components/sessionstore/test/browser_page_title.js45
-rw-r--r--browser/components/sessionstore/test/browser_parentProcessRestoreHash.js95
-rw-r--r--browser/components/sessionstore/test/browser_pending_tabs.js35
-rw-r--r--browser/components/sessionstore/test/browser_privatetabs.js133
-rw-r--r--browser/components/sessionstore/test/browser_purge_shistory.js59
-rw-r--r--browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js342
-rw-r--r--browser/components/sessionstore/test/browser_replace_load.js52
-rw-r--r--browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js171
-rw-r--r--browser/components/sessionstore/test/browser_restore_redirect.js69
-rw-r--r--browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js56
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions.js153
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js67
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html26
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions_sample.html8
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html11
-rw-r--r--browser/components/sessionstore/test/browser_send_async_message_oom.js75
-rw-r--r--browser/components/sessionstore/test/browser_sessionHistory.js240
-rw-r--r--browser/components/sessionstore/test/browser_sessionHistory_slow.sjs21
-rw-r--r--browser/components/sessionstore/test/browser_sessionStorage.html27
-rw-r--r--browser/components/sessionstore/test/browser_sessionStorage.js188
-rw-r--r--browser/components/sessionstore/test/browser_sessionStorage_size.js51
-rw-r--r--browser/components/sessionstore/test/browser_sessionStoreContainer.js141
-rw-r--r--browser/components/sessionstore/test/browser_swapDocShells.js35
-rw-r--r--browser/components/sessionstore/test/browser_switch_remoteness.js49
-rw-r--r--browser/components/sessionstore/test/browser_undoCloseById.js118
-rw-r--r--browser/components/sessionstore/test/browser_unrestored_crashedTabs.js69
-rw-r--r--browser/components/sessionstore/test/browser_upgrade_backup.js134
-rw-r--r--browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js26
-rw-r--r--browser/components/sessionstore/test/browser_windowStateContainer.js122
-rw-r--r--browser/components/sessionstore/test/content-forms.js133
-rw-r--r--browser/components/sessionstore/test/content.js222
-rw-r--r--browser/components/sessionstore/test/head.js564
-rw-r--r--browser/components/sessionstore/test/restore_redirect_http.html0
-rw-r--r--browser/components/sessionstore/test/restore_redirect_http.html^headers^2
-rw-r--r--browser/components/sessionstore/test/restore_redirect_js.html10
-rw-r--r--browser/components/sessionstore/test/restore_redirect_target.html8
-rw-r--r--browser/components/sessionstore/test/unit/.eslintrc.js7
-rw-r--r--browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json1
-rw-r--r--browser/components/sessionstore/test/unit/data/sessionstore_invalid.js3
-rw-r--r--browser/components/sessionstore/test/unit/data/sessionstore_valid.js3
-rw-r--r--browser/components/sessionstore/test/unit/head.js32
-rw-r--r--browser/components/sessionstore/test/unit/test_backup_once.js130
-rw-r--r--browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js114
-rw-r--r--browser/components/sessionstore/test/unit/test_shutdown_cleanup.js127
-rw-r--r--browser/components/sessionstore/test/unit/test_startup_invalid_session.js21
-rw-r--r--browser/components/sessionstore/test/unit/test_startup_nosession_async.js22
-rw-r--r--browser/components/sessionstore/test/unit/test_startup_session_async.js27
-rw-r--r--browser/components/sessionstore/test/unit/xpcshell.ini16
247 files changed, 27019 insertions, 0 deletions
diff --git a/browser/components/sessionstore/ContentRestore.jsm b/browser/components/sessionstore/ContentRestore.jsm
new file mode 100644
index 000000000..976016770
--- /dev/null
+++ b/browser/components/sessionstore/ContentRestore.jsm
@@ -0,0 +1,431 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ContentRestore"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
+ "resource:///modules/sessionstore/DocShellCapabilities.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormData",
+ "resource://gre/modules/FormData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
+ "resource:///modules/sessionstore/PageStyle.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
+ "resource://gre/modules/ScrollPosition.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
+ "resource:///modules/sessionstore/SessionHistory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
+ "resource:///modules/sessionstore/SessionStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+ "resource://gre/modules/sessionstore/Utils.jsm");
+
+/**
+ * This module implements the content side of session restoration. The chrome
+ * side is handled by SessionStore.jsm. The functions in this module are called
+ * by content-sessionStore.js based on messages received from SessionStore.jsm
+ * (or, in one case, based on a "load" event). Each tab has its own
+ * ContentRestore instance, constructed by content-sessionStore.js.
+ *
+ * In a typical restore, content-sessionStore.js will call the following based
+ * on messages and events it receives:
+ *
+ * restoreHistory(tabData, loadArguments, callbacks)
+ * Restores the tab's history and session cookies.
+ * restoreTabContent(loadArguments, finishCallback)
+ * Starts loading the data for the current page to restore.
+ * restoreDocument()
+ * Restore form and scroll data.
+ *
+ * When the page has been loaded from the network, we call finishCallback. It
+ * should send a message to SessionStore.jsm, which may cause other tabs to be
+ * restored.
+ *
+ * When the page has finished loading, a "load" event will trigger in
+ * content-sessionStore.js, which will call restoreDocument. At that point,
+ * form data is restored and the restore is complete.
+ *
+ * At any time, SessionStore.jsm can cancel the ongoing restore by sending a
+ * reset message, which causes resetRestore to be called. At that point it's
+ * legal to begin another restore.
+ */
+function ContentRestore(chromeGlobal) {
+ let internal = new ContentRestoreInternal(chromeGlobal);
+ let external = {};
+
+ let EXPORTED_METHODS = ["restoreHistory",
+ "restoreTabContent",
+ "restoreDocument",
+ "resetRestore"
+ ];
+
+ for (let method of EXPORTED_METHODS) {
+ external[method] = internal[method].bind(internal);
+ }
+
+ return Object.freeze(external);
+}
+
+function ContentRestoreInternal(chromeGlobal) {
+ this.chromeGlobal = chromeGlobal;
+
+ // The following fields are only valid during certain phases of the restore
+ // process.
+
+ // The tabData for the restore. Set in restoreHistory and removed in
+ // restoreTabContent.
+ this._tabData = null;
+
+ // Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a
+ // single entry from the tabData.entries array. Set in
+ // restoreTabContent and removed in restoreDocument.
+ this._restoringDocument = null;
+
+ // This listener is used to detect reloads on restoring tabs. Set in
+ // restoreHistory and removed in restoreTabContent.
+ this._historyListener = null;
+
+ // This listener detects when a pending tab starts loading (when not
+ // initiated by sessionstore) and when a restoring tab has finished loading
+ // data from the network. Set in restoreHistory() and restoreTabContent(),
+ // removed in resetRestore().
+ this._progressListener = null;
+}
+
+/**
+ * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are
+ * public.
+ */
+ContentRestoreInternal.prototype = {
+
+ get docShell() {
+ return this.chromeGlobal.docShell;
+ },
+
+ /**
+ * Starts the process of restoring a tab. The tabData to be restored is passed
+ * in here and used throughout the restoration. The epoch (which must be
+ * non-zero) is passed through to all the callbacks. If a load in the tab
+ * is started while it is pending, the appropriate callbacks are called.
+ */
+ restoreHistory(tabData, loadArguments, callbacks) {
+ this._tabData = tabData;
+
+ // In case about:blank isn't done yet.
+ let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
+
+ // 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. Don't bother doing this if
+ // we're restoring immediately due to a process switch. It just causes the
+ // URL bar to be temporarily blank.
+ let activeIndex = tabData.index - 1;
+ let activePageData = tabData.entries[activeIndex] || {};
+ let uri = activePageData.url || null;
+ if (uri && !loadArguments) {
+ webNavigation.setCurrentURI(Utils.makeURI(uri));
+ }
+
+ SessionHistory.restore(this.docShell, tabData);
+
+ // Add a listener to watch for reloads.
+ let listener = new HistoryListener(this.docShell, () => {
+ // On reload, restore tab contents.
+ this.restoreTabContent(null, false, callbacks.onLoadFinished);
+ });
+
+ webNavigation.sessionHistory.addSHistoryListener(listener);
+ this._historyListener = listener;
+
+ // Make sure to reset the capabilities and attributes in case this tab gets
+ // reused.
+ let disallow = new Set(tabData.disallow && tabData.disallow.split(","));
+ DocShellCapabilities.restore(this.docShell, disallow);
+
+ if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) {
+ SessionStorage.restore(this.docShell, tabData.storage);
+ delete tabData.storage;
+ }
+
+ // Add a progress listener to correctly handle browser.loadURI()
+ // calls from foreign code.
+ this._progressListener = new ProgressListener(this.docShell, {
+ onStartRequest: () => {
+ // Some code called browser.loadURI() on a pending tab. It's safe to
+ // assume we don't care about restoring scroll or form data.
+ this._tabData = null;
+
+ // Listen for the tab to finish loading.
+ this.restoreTabContentStarted(callbacks.onLoadFinished);
+
+ // Notify the parent.
+ callbacks.onLoadStarted();
+ }
+ });
+ },
+
+ /**
+ * Start loading the current page. When the data has finished loading from the
+ * network, finishCallback is called. Returns true if the load was successful.
+ */
+ restoreTabContent: function (loadArguments, isRemotenessUpdate, finishCallback) {
+ let tabData = this._tabData;
+ this._tabData = null;
+
+ let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
+ let history = webNavigation.sessionHistory;
+
+ // Listen for the tab to finish loading.
+ this.restoreTabContentStarted(finishCallback);
+
+ // Reset the current URI to about:blank. We changed it above for
+ // switch-to-tab, but now it must go back to the correct value before the
+ // load happens. Don't bother doing this if we're restoring immediately
+ // due to a process switch.
+ if (!isRemotenessUpdate) {
+ webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
+ }
+
+ try {
+ if (loadArguments) {
+ // A load has been redirected to a new process so get history into the
+ // same state it was before the load started then trigger the load.
+ let referrer = loadArguments.referrer ?
+ Utils.makeURI(loadArguments.referrer) : null;
+ let referrerPolicy = ('referrerPolicy' in loadArguments
+ ? loadArguments.referrerPolicy
+ : Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT);
+ let postData = loadArguments.postData ?
+ Utils.makeInputStream(loadArguments.postData) : null;
+
+ if (loadArguments.userContextId) {
+ webNavigation.setOriginAttributesBeforeLoading({ userContextId: loadArguments.userContextId });
+ }
+
+ webNavigation.loadURIWithOptions(loadArguments.uri, loadArguments.flags,
+ referrer, referrerPolicy, postData,
+ null, null);
+ } else if (tabData.userTypedValue && tabData.userTypedClear) {
+ // If the user typed a URL into the URL bar and hit enter right before
+ // we crashed, we want to start loading that page again. A non-zero
+ // userTypedClear value means that the load had started.
+ // Load userTypedValue and fix up the URL if it's partial/broken.
+ webNavigation.loadURI(tabData.userTypedValue,
+ Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
+ null, null, null);
+ } else if (tabData.entries.length) {
+ // Stash away the data we need for restoreDocument.
+ let activeIndex = tabData.index - 1;
+ this._restoringDocument = {entry: tabData.entries[activeIndex] || {},
+ formdata: tabData.formdata || {},
+ pageStyle: tabData.pageStyle || {},
+ scrollPositions: tabData.scroll || {}};
+
+ // 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.
+ history.reloadCurrentEntry();
+ } else {
+ // If there's nothing to restore, we should still blank the page.
+ webNavigation.loadURI("about:blank",
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ null, null, null);
+ }
+
+ return true;
+ } catch (ex if ex instanceof Ci.nsIException) {
+ // Ignore page load errors, but return false to signal that the load never
+ // happened.
+ return false;
+ }
+ },
+
+ /**
+ * To be called after restoreHistory(). Removes all listeners needed for
+ * pending tabs and makes sure to notify when the tab finished loading.
+ */
+ restoreTabContentStarted(finishCallback) {
+ // The reload listener is no longer needed.
+ this._historyListener.uninstall();
+ this._historyListener = null;
+
+ // Remove the old progress listener.
+ this._progressListener.uninstall();
+
+ // We're about to start a load. This listener will be called when the load
+ // has finished getting everything from the network.
+ this._progressListener = new ProgressListener(this.docShell, {
+ onStopRequest: () => {
+ // Call resetRestore() to reset the state back to normal. The data
+ // needed for restoreDocument() (which hasn't happened yet) will
+ // remain in _restoringDocument.
+ this.resetRestore();
+
+ finishCallback();
+ }
+ });
+ },
+
+ /**
+ * Finish restoring the tab by filling in form data and setting the scroll
+ * position. The restore is complete when this function exits. It should be
+ * called when the "load" event fires for the restoring tab.
+ */
+ restoreDocument: function () {
+ if (!this._restoringDocument) {
+ return;
+ }
+ let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument;
+ this._restoringDocument = null;
+
+ let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ PageStyle.restoreTree(this.docShell, pageStyle);
+ FormData.restoreTree(window, formdata);
+ ScrollPosition.restoreTree(window, scrollPositions);
+ },
+
+ /**
+ * Cancel an ongoing restore. This function can be called any time between
+ * restoreHistory and restoreDocument.
+ *
+ * This function is called externally (if a restore is canceled) and
+ * internally (when the loads for a restore have finished). In the latter
+ * case, it's called before restoreDocument, so it cannot clear
+ * _restoringDocument.
+ */
+ resetRestore: function () {
+ this._tabData = null;
+
+ if (this._historyListener) {
+ this._historyListener.uninstall();
+ }
+ this._historyListener = null;
+
+ if (this._progressListener) {
+ this._progressListener.uninstall();
+ }
+ this._progressListener = null;
+ }
+};
+
+/*
+ * This listener detects when a page being restored is reloaded. It triggers a
+ * callback and cancels the reload. The callback will send a message to
+ * SessionStore.jsm so that it can restore the content immediately.
+ */
+function HistoryListener(docShell, callback) {
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNavigation.sessionHistory.addSHistoryListener(this);
+
+ this.webNavigation = webNavigation;
+ this.callback = callback;
+}
+HistoryListener.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsISHistoryListener,
+ Ci.nsISupportsWeakReference
+ ]),
+
+ uninstall: function () {
+ let shistory = this.webNavigation.sessionHistory;
+ if (shistory) {
+ shistory.removeSHistoryListener(this);
+ }
+ },
+
+ OnHistoryGoBack: function(backURI) { return true; },
+ OnHistoryGoForward: function(forwardURI) { return true; },
+ OnHistoryGotoIndex: function(index, gotoURI) { return true; },
+ OnHistoryPurge: function(numEntries) { return true; },
+ OnHistoryReplaceEntry: function(index) {},
+
+ // This will be called for a pending tab when loadURI(uri) is called where
+ // the given |uri| only differs in the fragment.
+ OnHistoryNewEntry(newURI) {
+ let currentURI = this.webNavigation.currentURI;
+
+ // Ignore new SHistory entries with the same URI as those do not indicate
+ // a navigation inside a document by changing the #hash part of the URL.
+ // We usually hit this when purging session history for browsers.
+ if (currentURI && (currentURI.spec == newURI.spec)) {
+ return;
+ }
+
+ // Reset the tab's URL to what it's actually showing. Without this loadURI()
+ // would use the current document and change the displayed URL only.
+ this.webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
+
+ // Kick off a new load so that we navigate away from about:blank to the
+ // new URL that was passed to loadURI(). The new load will cause a
+ // STATE_START notification to be sent and the ProgressListener will then
+ // notify the parent and do the rest.
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ this.webNavigation.loadURI(newURI.spec, flags, null, null, null);
+ },
+
+ OnHistoryReload(reloadURI, reloadFlags) {
+ this.callback();
+
+ // Cancel the load.
+ return false;
+ },
+}
+
+/**
+ * This class informs SessionStore.jsm whenever the network requests for a
+ * restoring page have completely finished. We only restore three tabs
+ * simultaneously, so this is the signal for SessionStore.jsm to kick off
+ * another restore (if there are more to do).
+ *
+ * The progress listener is also used to be notified when a load not initiated
+ * by sessionstore starts. Pending tabs will then need to be marked as no
+ * longer pending.
+ */
+function ProgressListener(docShell, callbacks) {
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
+
+ this.webProgress = webProgress;
+ this.callbacks = callbacks;
+}
+
+ProgressListener.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference
+ ]),
+
+ uninstall: function() {
+ this.webProgress.removeProgressListener(this);
+ },
+
+ onStateChange: function(webProgress, request, stateFlags, status) {
+ let {STATE_IS_WINDOW, STATE_STOP, STATE_START} = Ci.nsIWebProgressListener;
+ if (!webProgress.isTopLevel || !(stateFlags & STATE_IS_WINDOW)) {
+ return;
+ }
+
+ if (stateFlags & STATE_START && this.callbacks.onStartRequest) {
+ this.callbacks.onStartRequest();
+ }
+
+ if (stateFlags & STATE_STOP && this.callbacks.onStopRequest) {
+ this.callbacks.onStopRequest();
+ }
+ },
+
+ onLocationChange: function() {},
+ onProgressChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {},
+};
diff --git a/browser/components/sessionstore/DocShellCapabilities.jsm b/browser/components/sessionstore/DocShellCapabilities.jsm
new file mode 100644
index 000000000..098aae86f
--- /dev/null
+++ b/browser/components/sessionstore/DocShellCapabilities.jsm
@@ -0,0 +1,50 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["DocShellCapabilities"];
+
+/**
+ * The external API exported by this module.
+ */
+this.DocShellCapabilities = Object.freeze({
+ collect: function (docShell) {
+ return DocShellCapabilitiesInternal.collect(docShell);
+ },
+
+ restore: function (docShell, disallow) {
+ return DocShellCapabilitiesInternal.restore(docShell, disallow);
+ },
+});
+
+/**
+ * Internal functionality to save and restore the docShell.allow* properties.
+ */
+var DocShellCapabilitiesInternal = {
+ // 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.
+ caps: null,
+
+ allCapabilities: function (docShell) {
+ if (!this.caps) {
+ let keys = Object.keys(docShell);
+ this.caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5));
+ }
+ return this.caps;
+ },
+
+ collect: function (docShell) {
+ let caps = this.allCapabilities(docShell);
+ return caps.filter(cap => !docShell["allow" + cap]);
+ },
+
+ restore: function (docShell, disallow) {
+ let caps = this.allCapabilities(docShell);
+ for (let cap of caps)
+ docShell["allow" + cap] = !disallow.has(cap);
+ },
+};
diff --git a/browser/components/sessionstore/FrameTree.jsm b/browser/components/sessionstore/FrameTree.jsm
new file mode 100644
index 000000000..e8ed12a8f
--- /dev/null
+++ b/browser/components/sessionstore/FrameTree.jsm
@@ -0,0 +1,254 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FrameTree"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const EXPORTED_METHODS = ["addObserver", "contains", "map", "forEach"];
+
+/**
+ * A FrameTree represents all frames that were reachable when the document
+ * was loaded. We use this information to ignore frames when collecting
+ * sessionstore data as we can't currently restore anything for frames that
+ * have been created dynamically after or at the load event.
+ *
+ * @constructor
+ */
+function FrameTree(chromeGlobal) {
+ let internal = new FrameTreeInternal(chromeGlobal);
+ let external = {};
+
+ for (let method of EXPORTED_METHODS) {
+ external[method] = internal[method].bind(internal);
+ }
+
+ return Object.freeze(external);
+}
+
+/**
+ * The internal frame tree API that the public one points to.
+ *
+ * @constructor
+ */
+function FrameTreeInternal(chromeGlobal) {
+ // A WeakMap that uses frames (DOMWindows) as keys and their initial indices
+ // in their parents' child lists as values. Suppose we have a root frame with
+ // three subframes i.e. a page with three iframes. The WeakMap would have
+ // four entries and look as follows:
+ //
+ // root -> 0
+ // subframe1 -> 0
+ // subframe2 -> 1
+ // subframe3 -> 2
+ //
+ // Should one of the subframes disappear we will stop collecting data for it
+ // as |this._frames.has(frame) == false|. All other subframes will maintain
+ // their initial indices to ensure we can restore frame data appropriately.
+ this._frames = new WeakMap();
+
+ // The Set of observers that will be notified when the frame changes.
+ this._observers = new Set();
+
+ // The chrome global we use to retrieve the current DOMWindow.
+ this._chromeGlobal = chromeGlobal;
+
+ // Register a web progress listener to be notified about new page loads.
+ let docShell = chromeGlobal.docShell;
+ let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
+ let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+}
+
+FrameTreeInternal.prototype = {
+
+ // Returns the docShell's current global.
+ get content() {
+ return this._chromeGlobal.content;
+ },
+
+ /**
+ * Adds a given observer |obs| to the set of observers that will be notified
+ * when the frame tree is reset (when a new document starts loading) or
+ * recollected (when a document finishes loading).
+ *
+ * @param obs (object)
+ */
+ addObserver: function (obs) {
+ this._observers.add(obs);
+ },
+
+ /**
+ * Notifies all observers that implement the given |method|.
+ *
+ * @param method (string)
+ */
+ notifyObservers: function (method) {
+ for (let obs of this._observers) {
+ if (obs.hasOwnProperty(method)) {
+ obs[method]();
+ }
+ }
+ },
+
+ /**
+ * Checks whether a given |frame| is contained in the collected frame tree.
+ * If it is not, this indicates that we should not collect data for it.
+ *
+ * @param frame (nsIDOMWindow)
+ * @return bool
+ */
+ contains: function (frame) {
+ return this._frames.has(frame);
+ },
+
+ /**
+ * Recursively applies the given function |cb| to the stored frame tree. Use
+ * this method to collect sessionstore data for all reachable frames stored
+ * in the frame tree.
+ *
+ * If a given function |cb| returns a value, it must be an object. It may
+ * however return "null" to indicate that there is no data to be stored for
+ * the given frame.
+ *
+ * The object returned by |cb| cannot have any property named "children" as
+ * that is used to store information about subframes in the tree returned
+ * by |map()| and might be overridden.
+ *
+ * @param cb (function)
+ * @return object
+ */
+ map: function (cb) {
+ let frames = this._frames;
+
+ function walk(frame) {
+ let obj = cb(frame) || {};
+
+ if (frames.has(frame)) {
+ let children = [];
+
+ Array.forEach(frame.frames, subframe => {
+ // Don't collect any data if the frame is not contained in the
+ // initial frame tree. It's a dynamic frame added later.
+ if (!frames.has(subframe)) {
+ return;
+ }
+
+ // Retrieve the frame's original position in its parent's child list.
+ let index = frames.get(subframe);
+
+ // Recursively collect data for the current subframe.
+ let result = walk(subframe, cb);
+ if (result && Object.keys(result).length) {
+ children[index] = result;
+ }
+ });
+
+ if (children.length) {
+ obj.children = children;
+ }
+ }
+
+ return Object.keys(obj).length ? obj : null;
+ }
+
+ return walk(this.content);
+ },
+
+ /**
+ * Applies the given function |cb| to all frames stored in the tree. Use this
+ * method if |map()| doesn't suit your needs and you want more control over
+ * how data is collected.
+ *
+ * @param cb (function)
+ * This callback receives the current frame as the only argument.
+ */
+ forEach: function (cb) {
+ let frames = this._frames;
+
+ function walk(frame) {
+ cb(frame);
+
+ if (!frames.has(frame)) {
+ return;
+ }
+
+ Array.forEach(frame.frames, subframe => {
+ if (frames.has(subframe)) {
+ cb(subframe);
+ }
+ });
+ }
+
+ walk(this.content);
+ },
+
+ /**
+ * Stores a given |frame| and its children in the frame tree.
+ *
+ * @param frame (nsIDOMWindow)
+ * @param index (int)
+ * The index in the given frame's parent's child list.
+ */
+ collect: function (frame, index = 0) {
+ // Mark the given frame as contained in the frame tree.
+ this._frames.set(frame, index);
+
+ // Mark the given frame's subframes as contained in the tree.
+ Array.forEach(frame.frames, this.collect, this);
+ },
+
+ /**
+ * @see nsIWebProgressListener.onStateChange
+ *
+ * We want to be notified about:
+ * - new documents that start loading to clear the current frame tree;
+ * - completed document loads to recollect reachable frames.
+ */
+ onStateChange: function (webProgress, request, stateFlags, status) {
+ // Ignore state changes for subframes because we're only interested in the
+ // top-document starting or stopping its load. We thus only care about any
+ // changes to the root of the frame tree, not to any of its nodes/leafs.
+ if (!webProgress.isTopLevel || webProgress.DOMWindow != this.content) {
+ return;
+ }
+
+ // onStateChange will be fired when loading the initial about:blank URI for
+ // a browser, which we don't actually care about. This is particularly for
+ // the case of unrestored background tabs, where the content has not yet
+ // been restored: we don't want to accidentally send any updates to the
+ // parent when the about:blank placeholder page has loaded.
+ if (!this._chromeGlobal.docShell.hasLoadedNonBlankURI) {
+ return;
+ }
+
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ // Clear the list of frames until we can recollect it.
+ this._frames = new WeakMap();
+
+ // Notify observers that the frame tree has been reset.
+ this.notifyObservers("onFrameTreeReset");
+ } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ // The document and its resources have finished loading.
+ this.collect(webProgress.DOMWindow);
+
+ // Notify observers that the frame tree has been reset.
+ this.notifyObservers("onFrameTreeCollected");
+ }
+ },
+
+ // Unused nsIWebProgressListener methods.
+ onLocationChange: function () {},
+ onProgressChange: function () {},
+ onSecurityChange: function () {},
+ onStatusChange: function () {},
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+};
diff --git a/browser/components/sessionstore/GlobalState.jsm b/browser/components/sessionstore/GlobalState.jsm
new file mode 100644
index 000000000..ac2d7c81b
--- /dev/null
+++ b/browser/components/sessionstore/GlobalState.jsm
@@ -0,0 +1,84 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["GlobalState"];
+
+const EXPORTED_METHODS = ["getState", "clear", "get", "set", "delete", "setFromState"];
+/**
+ * Module that contains global session data.
+ */
+function GlobalState() {
+ let internal = new GlobalStateInternal();
+ let external = {};
+ for (let method of EXPORTED_METHODS) {
+ external[method] = internal[method].bind(internal);
+ }
+ return Object.freeze(external);
+}
+
+function GlobalStateInternal() {
+ // Storage for global state.
+ this.state = {};
+}
+
+GlobalStateInternal.prototype = {
+ /**
+ * Get all value from the global state.
+ */
+ getState: function() {
+ return this.state;
+ },
+
+ /**
+ * Clear all currently stored global state.
+ */
+ clear: function() {
+ this.state = {};
+ },
+
+ /**
+ * Retrieve a value from the global state.
+ *
+ * @param aKey
+ * A key the value is stored under.
+ * @return The value stored at aKey, or an empty string if no value is set.
+ */
+ get: function(aKey) {
+ return this.state[aKey] || "";
+ },
+
+ /**
+ * Set a global value.
+ *
+ * @param aKey
+ * A key to store the value under.
+ */
+ set: function(aKey, aStringValue) {
+ this.state[aKey] = aStringValue;
+ },
+
+ /**
+ * Delete a global value.
+ *
+ * @param aKey
+ * A key to delete the value for.
+ */
+ delete: function(aKey) {
+ delete this.state[aKey];
+ },
+
+ /**
+ * Set the current global state from a state object. Any previous global
+ * state will be removed, even if the new state does not contain a matching
+ * key.
+ *
+ * @param aState
+ * A state object to extract global state from to be set.
+ */
+ setFromState: function (aState) {
+ this.state = (aState && aState.global) || {};
+ }
+};
diff --git a/browser/components/sessionstore/PageStyle.jsm b/browser/components/sessionstore/PageStyle.jsm
new file mode 100644
index 000000000..0424ef6b1
--- /dev/null
+++ b/browser/components/sessionstore/PageStyle.jsm
@@ -0,0 +1,100 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PageStyle"];
+
+const Ci = Components.interfaces;
+
+/**
+ * The external API exported by this module.
+ */
+this.PageStyle = Object.freeze({
+ collect: function (docShell, frameTree) {
+ return PageStyleInternal.collect(docShell, frameTree);
+ },
+
+ restoreTree: function (docShell, data) {
+ PageStyleInternal.restoreTree(docShell, data);
+ }
+});
+
+// Signifies that author style level is disabled for the page.
+const NO_STYLE = "_nostyle";
+
+var PageStyleInternal = {
+ /**
+ * Collects the selected style sheet sets for all reachable frames.
+ */
+ collect: function (docShell, frameTree) {
+ let result = frameTree.map(({document: doc}) => {
+ let style;
+
+ if (doc) {
+ // http://dev.w3.org/csswg/cssom/#persisting-the-selected-css-style-sheet-set
+ style = doc.selectedStyleSheetSet || doc.lastStyleSheetSet;
+ }
+
+ return style ? {pageStyle: style} : null;
+ });
+
+ let markupDocumentViewer =
+ docShell.contentViewer;
+
+ if (markupDocumentViewer.authorStyleDisabled) {
+ result = result || {};
+ result.disabled = true;
+ }
+
+ return result && Object.keys(result).length ? result : null;
+ },
+
+ /**
+ * Restores pageStyle data for the current frame hierarchy starting at the
+ * |docShell's| current DOMWindow using the given pageStyle |data|.
+ *
+ * Warning: If the current frame hierarchy doesn't match that of the given
+ * |data| object we will silently discard data for unreachable frames. We may
+ * as well assign page styles to the wrong frames if some were reordered or
+ * removed.
+ *
+ * @param docShell (nsIDocShell)
+ * @param data (object)
+ * {
+ * disabled: true, // when true, author styles will be disabled
+ * pageStyle: "Dusk",
+ * children: [
+ * null,
+ * {pageStyle: "Mozilla", children: [ ... ]}
+ * ]
+ * }
+ */
+ restoreTree: function (docShell, data) {
+ let disabled = data.disabled || false;
+ let markupDocumentViewer =
+ docShell.contentViewer;
+ markupDocumentViewer.authorStyleDisabled = disabled;
+
+ function restoreFrame(root, data) {
+ if (data.hasOwnProperty("pageStyle")) {
+ root.document.selectedStyleSheetSet = data.pageStyle;
+ }
+
+ if (!data.hasOwnProperty("children")) {
+ return;
+ }
+
+ let frames = root.frames;
+ data.children.forEach((child, index) => {
+ if (child && index < frames.length) {
+ restoreFrame(frames[index], child);
+ }
+ });
+ }
+
+ let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
+ restoreFrame(ifreq.getInterface(Ci.nsIDOMWindow), data);
+ }
+};
diff --git a/browser/components/sessionstore/PrivacyFilter.jsm b/browser/components/sessionstore/PrivacyFilter.jsm
new file mode 100644
index 000000000..88713b402
--- /dev/null
+++ b/browser/components/sessionstore/PrivacyFilter.jsm
@@ -0,0 +1,135 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PrivacyFilter"];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
+ "resource:///modules/sessionstore/PrivacyLevel.jsm");
+
+/**
+ * A module that provides methods to filter various kinds of data collected
+ * from a tab by the current privacy level as set by the user.
+ */
+this.PrivacyFilter = Object.freeze({
+ /**
+ * Filters the given (serialized) session storage |data| according to the
+ * current privacy level and returns a new object containing only data that
+ * we're allowed to store.
+ *
+ * @param data The session storage data as collected from a tab.
+ * @return object
+ */
+ filterSessionStorageData: function (data) {
+ let retval = {};
+
+ for (let host of Object.keys(data)) {
+ if (PrivacyLevel.check(host)) {
+ retval[host] = data[host];
+ }
+ }
+
+ return Object.keys(retval).length ? retval : null;
+ },
+
+ /**
+ * Filters the given (serialized) form |data| according to the current
+ * privacy level and returns a new object containing only data that we're
+ * allowed to store.
+ *
+ * @param data The form data as collected from a tab.
+ * @return object
+ */
+ filterFormData: function (data) {
+ // If the given form data object has an associated URL that we are not
+ // allowed to store data for, bail out. We explicitly discard data for any
+ // children as well even if storing data for those frames would be allowed.
+ if (data.url && !PrivacyLevel.check(data.url)) {
+ return;
+ }
+
+ let retval = {};
+
+ for (let key of Object.keys(data)) {
+ if (key === "children") {
+ let recurse = child => this.filterFormData(child);
+ let children = data.children.map(recurse).filter(child => child);
+
+ if (children.length) {
+ retval.children = children;
+ }
+ // Only copy keys other than "children" if we have a valid URL in
+ // data.url and we thus passed the privacy level check.
+ } else if (data.url) {
+ retval[key] = data[key];
+ }
+ }
+
+ return Object.keys(retval).length ? retval : null;
+ },
+
+ /**
+ * Removes any private windows and tabs from a given browser state object.
+ *
+ * @param browserState (object)
+ * The browser state for which we remove any private windows and tabs.
+ * The given object will be modified.
+ */
+ filterPrivateWindowsAndTabs: function (browserState) {
+ // Remove private opened windows.
+ for (let i = browserState.windows.length - 1; i >= 0; i--) {
+ let win = browserState.windows[i];
+
+ if (win.isPrivate) {
+ browserState.windows.splice(i, 1);
+
+ if (browserState.selectedWindow >= i) {
+ browserState.selectedWindow--;
+ }
+ } else {
+ // Remove private tabs from all open non-private windows.
+ this.filterPrivateTabs(win);
+ }
+ }
+
+ // Remove private closed windows.
+ browserState._closedWindows =
+ browserState._closedWindows.filter(win => !win.isPrivate);
+
+ // Remove private tabs from all remaining closed windows.
+ browserState._closedWindows.forEach(win => this.filterPrivateTabs(win));
+ },
+
+ /**
+ * Removes open private tabs from a given window state object.
+ *
+ * @param winState (object)
+ * The window state for which we remove any private tabs.
+ * The given object will be modified.
+ */
+ filterPrivateTabs: function (winState) {
+ // Remove open private tabs.
+ for (let i = winState.tabs.length - 1; i >= 0 ; i--) {
+ let tab = winState.tabs[i];
+
+ if (tab.isPrivate) {
+ winState.tabs.splice(i, 1);
+
+ if (winState.selected >= i) {
+ winState.selected--;
+ }
+ }
+ }
+
+ // Note that closed private tabs are only stored for private windows.
+ // There is no need to call this function for private windows as the
+ // whole window state should just be discarded so we explicitly don't
+ // try to remove closed private tabs as an optimization.
+ }
+});
diff --git a/browser/components/sessionstore/PrivacyLevel.jsm b/browser/components/sessionstore/PrivacyLevel.jsm
new file mode 100644
index 000000000..135f1f959
--- /dev/null
+++ b/browser/components/sessionstore/PrivacyLevel.jsm
@@ -0,0 +1,64 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PrivacyLevel"];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF = "browser.sessionstore.privacy_level";
+
+// The following constants represent the different possible privacy levels that
+// can be set by the user and that we need to consider when collecting text
+// data, and cookies.
+//
+// Collect data from all sites (http and https).
+const PRIVACY_NONE = 0;
+// Collect data from unencrypted sites (http), only.
+const PRIVACY_ENCRYPTED = 1;
+// Collect no data.
+const PRIVACY_FULL = 2;
+
+/**
+ * The external API as exposed by this module.
+ */
+var PrivacyLevel = Object.freeze({
+ /**
+ * Returns whether the current privacy level allows saving data for the given
+ * |url|.
+ *
+ * @param url The URL we want to save data for.
+ * @return bool
+ */
+ check: function (url) {
+ return PrivacyLevel.canSave({ isHttps: url.startsWith("https:") });
+ },
+
+ /**
+ * Checks whether we're allowed to save data for a specific site.
+ *
+ * @param {isHttps: boolean}
+ * An object that must have one property: 'isHttps'.
+ * 'isHttps' tells whether the site us secure communication (HTTPS).
+ * @return {bool} Whether we can save data for the specified site.
+ */
+ canSave: function ({isHttps}) {
+ let level = Services.prefs.getIntPref(PREF);
+
+ // Never save any data when full privacy is requested.
+ if (level == PRIVACY_FULL) {
+ return false;
+ }
+
+ // Don't save data for encrypted sites when requested.
+ if (isHttps && level == PRIVACY_ENCRYPTED) {
+ return false;
+ }
+
+ return true;
+ }
+});
diff --git a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm
new file mode 100644
index 000000000..ac5731160
--- /dev/null
+++ b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm
@@ -0,0 +1,214 @@
+/* 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 = ["RecentlyClosedTabsAndWindowsMenuUtils"];
+
+const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+ "resource:///modules/sessionstore/SessionStore.jsm");
+
+var navigatorBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+this.RecentlyClosedTabsAndWindowsMenuUtils = {
+
+ /**
+ * Builds up a document fragment of UI items for the recently closed tabs.
+ * @param aWindow
+ * The window that the tabs were closed in.
+ * @param aTagName
+ * The tag name that will be used when creating the UI items.
+ * @param aPrefixRestoreAll (defaults to false)
+ * Whether the 'restore all tabs' item is suffixed or prefixed to the list.
+ * If suffixed (the default) a separator will be inserted before it.
+ * @param aRestoreAllLabel (defaults to "menuRestoreAllTabs.label")
+ * Which localizable string to use for the 'restore all tabs' item.
+ * @returns A document fragment with UI items for each recently closed tab.
+ */
+ getTabsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false,
+ aRestoreAllLabel="menuRestoreAllTabs.label") {
+ let doc = aWindow.document;
+ let fragment = doc.createDocumentFragment();
+ if (SessionStore.getClosedTabCount(aWindow) != 0) {
+ let closedTabs = SessionStore.getClosedTabData(aWindow, false);
+ for (let i = 0; i < closedTabs.length; i++) {
+ createEntry(aTagName, false, i, closedTabs[i], doc,
+ closedTabs[i].title, fragment);
+ }
+
+ createRestoreAllEntry(doc, fragment, aPrefixRestoreAll, false,
+ aRestoreAllLabel, closedTabs.length, aTagName)
+ }
+ return fragment;
+ },
+
+ /**
+ * Builds up a document fragment of UI items for the recently closed windows.
+ * @param aWindow
+ * A window that can be used to create the elements and document fragment.
+ * @param aTagName
+ * The tag name that will be used when creating the UI items.
+ * @param aPrefixRestoreAll (defaults to false)
+ * Whether the 'restore all windows' item is suffixed or prefixed to the list.
+ * If suffixed (the default) a separator will be inserted before it.
+ * @param aRestoreAllLabel (defaults to "menuRestoreAllWindows.label")
+ * Which localizable string to use for the 'restore all windows' item.
+ * @returns A document fragment with UI items for each recently closed window.
+ */
+ getWindowsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false,
+ aRestoreAllLabel="menuRestoreAllWindows.label") {
+ let closedWindowData = SessionStore.getClosedWindowData(false);
+ let doc = aWindow.document;
+ let fragment = doc.createDocumentFragment();
+ if (closedWindowData.length != 0) {
+ let menuLabelString = navigatorBundle.GetStringFromName("menuUndoCloseWindowLabel");
+ let menuLabelStringSingleTab =
+ navigatorBundle.GetStringFromName("menuUndoCloseWindowSingleTabLabel");
+
+ for (let i = 0; i < closedWindowData.length; i++) {
+ let undoItem = closedWindowData[i];
+ let otherTabsCount = undoItem.tabs.length - 1;
+ let label = (otherTabsCount == 0) ? menuLabelStringSingleTab
+ : PluralForm.get(otherTabsCount, menuLabelString);
+ let menuLabel = label.replace("#1", undoItem.title)
+ .replace("#2", otherTabsCount);
+ let selectedTab = undoItem.tabs[undoItem.selected - 1];
+
+ createEntry(aTagName, true, i, selectedTab, doc, menuLabel,
+ fragment);
+ }
+
+ createRestoreAllEntry(doc, fragment, aPrefixRestoreAll, true,
+ aRestoreAllLabel, closedWindowData.length,
+ aTagName);
+ }
+ return fragment;
+ },
+
+
+ /**
+ * Re-open a closed tab and put it to the end of the tab strip.
+ * Used for a middle click.
+ * @param aEvent
+ * The event when the user clicks the menu item
+ */
+ _undoCloseMiddleClick: function(aEvent) {
+ if (aEvent.button != 1)
+ return;
+
+ aEvent.view.undoCloseTab(aEvent.originalTarget.getAttribute("value"));
+ aEvent.view.gBrowser.moveTabToEnd();
+ },
+};
+
+function setImage(aItem, aElement) {
+ let iconURL = aItem.image;
+ // don't initiate a connection just to fetch a favicon (see bug 467828)
+ if (/^https?:/.test(iconURL))
+ iconURL = "moz-anno:favicon:" + iconURL;
+
+ aElement.setAttribute("image", iconURL);
+}
+
+/**
+ * Create a UI entry for a recently closed tab or window.
+ * @param aTagName
+ * the tag name that will be used when creating the UI entry
+ * @param aIsWindowsFragment
+ * whether or not this entry will represent a closed window
+ * @param aIndex
+ * the index of the closed tab
+ * @param aClosedTab
+ * the closed tab
+ * @param aDocument
+ * a document that can be used to create the entry
+ * @param aMenuLabel
+ * the label the created entry will have
+ * @param aFragment
+ * the fragment the created entry will be in
+ */
+function createEntry(aTagName, aIsWindowsFragment, aIndex, aClosedTab,
+ aDocument, aMenuLabel, aFragment) {
+ let element = aDocument.createElementNS(kNSXUL, aTagName);
+
+ element.setAttribute("label", aMenuLabel);
+ if (aClosedTab.image) {
+ setImage(aClosedTab, element);
+ }
+ if (!aIsWindowsFragment) {
+ element.setAttribute("value", aIndex);
+ }
+
+ if (aTagName == "menuitem") {
+ element.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon");
+ }
+
+ element.setAttribute("oncommand", "undoClose" + (aIsWindowsFragment ? "Window" : "Tab") +
+ "(" + aIndex + ");");
+
+ // Set the targetURI attribute so it will be shown in tooltip.
+ // SessionStore uses one-based indexes, so we need to normalize them.
+ let tabData;
+ tabData = aIsWindowsFragment ? aClosedTab
+ : aClosedTab.state;
+ let activeIndex = (tabData.index || tabData.entries.length) - 1;
+ if (activeIndex >= 0 && tabData.entries[activeIndex]) {
+ element.setAttribute("targetURI", tabData.entries[activeIndex].url);
+ }
+
+ if (!aIsWindowsFragment) {
+ element.addEventListener("click", RecentlyClosedTabsAndWindowsMenuUtils._undoCloseMiddleClick, false);
+ }
+ if (aIndex == 0) {
+ element.setAttribute("key", "key_undoClose" + (aIsWindowsFragment? "Window" : "Tab"));
+ }
+
+ aFragment.appendChild(element);
+}
+
+/**
+ * Create an entry to restore all closed windows or tabs.
+ * @param aDocument
+ * a document that can be used to create the entry
+ * @param aFragment
+ * the fragment the created entry will be in
+ * @param aPrefixRestoreAll
+ * whether the 'restore all windows' item is suffixed or prefixed to the list
+ * If suffixed a separator will be inserted before it.
+ * @param aIsWindowsFragment
+ * whether or not this entry will represent a closed window
+ * @param aRestoreAllLabel
+ * which localizable string to use for the entry
+ * @param aEntryCount
+ * the number of elements to be restored by this entry
+ * @param aTagName
+ * the tag name that will be used when creating the UI entry
+ */
+function createRestoreAllEntry(aDocument, aFragment, aPrefixRestoreAll,
+ aIsWindowsFragment, aRestoreAllLabel,
+ aEntryCount, aTagName) {
+ let restoreAllElements = aDocument.createElementNS(kNSXUL, aTagName);
+ restoreAllElements.classList.add("restoreallitem");
+ restoreAllElements.setAttribute("label", navigatorBundle.GetStringFromName(aRestoreAllLabel));
+ restoreAllElements.setAttribute("oncommand",
+ "for (var i = 0; i < " + aEntryCount + "; i++) undoClose" +
+ (aIsWindowsFragment? "Window" : "Tab") + "();");
+ if (aPrefixRestoreAll) {
+ aFragment.insertBefore(restoreAllElements, aFragment.firstChild);
+ } else {
+ aFragment.appendChild(aDocument.createElementNS(kNSXUL, "menuseparator"));
+ aFragment.appendChild(restoreAllElements);
+ }
+} \ No newline at end of file
diff --git a/browser/components/sessionstore/RunState.jsm b/browser/components/sessionstore/RunState.jsm
new file mode 100644
index 000000000..3cdf47718
--- /dev/null
+++ b/browser/components/sessionstore/RunState.jsm
@@ -0,0 +1,96 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["RunState"];
+
+const STATE_STOPPED = 0;
+const STATE_RUNNING = 1;
+const STATE_QUITTING = 2;
+const STATE_CLOSING = 3;
+const STATE_CLOSED = 4;
+
+// We're initially stopped.
+var state = STATE_STOPPED;
+
+/**
+ * This module keeps track of SessionStore's current run state. We will
+ * always start out at STATE_STOPPED. After the session was read from disk and
+ * the initial browser window has loaded we switch to STATE_RUNNING. On the
+ * first notice that a browser shutdown was granted we switch to STATE_QUITTING.
+ */
+this.RunState = Object.freeze({
+ // If we're stopped then SessionStore hasn't been initialized yet. As soon
+ // as the session is read from disk and the initial browser window has loaded
+ // the run state will change to STATE_RUNNING.
+ get isStopped() {
+ return state == STATE_STOPPED;
+ },
+
+ // STATE_RUNNING is our default mode of operation that we'll spend most of
+ // the time in. After the session was read from disk and the first browser
+ // window has loaded we remain running until the browser quits.
+ get isRunning() {
+ return state == STATE_RUNNING;
+ },
+
+ // We will enter STATE_QUITTING as soon as we receive notice that a browser
+ // shutdown was granted. SessionStore will use this information to prevent
+ // us from collecting partial information while the browser is shutting down
+ // as well as to allow a last single write to disk and block all writes after
+ // that.
+ get isQuitting() {
+ return state >= STATE_QUITTING;
+ },
+
+ // We will enter STATE_CLOSING as soon as SessionStore is uninitialized.
+ // The SessionFile module will know that a last write will happen in this
+ // state and it can do some necessary cleanup.
+ get isClosing() {
+ return state == STATE_CLOSING;
+ },
+
+ // We will enter STATE_CLOSED as soon as SessionFile has written to disk for
+ // the last time before shutdown and will not accept any further writes.
+ get isClosed() {
+ return state == STATE_CLOSED;
+ },
+
+ // Switch the run state to STATE_RUNNING. This must be called after the
+ // session was read from, the initial browser window has loaded and we're
+ // now ready to restore session data.
+ setRunning() {
+ if (this.isStopped) {
+ state = STATE_RUNNING;
+ }
+ },
+
+ // Switch the run state to STATE_CLOSING. This must be called *before* the
+ // last SessionFile.write() call so that SessionFile knows we're closing and
+ // can do some last cleanups and write a proper sessionstore.js file.
+ setClosing() {
+ if (this.isQuitting) {
+ state = STATE_CLOSING;
+ }
+ },
+
+ // Switch the run state to STATE_CLOSED. This must be called by SessionFile
+ // after the last write to disk was accepted and no further writes will be
+ // allowed. Any writes after this stage will cause exceptions.
+ setClosed() {
+ if (this.isClosing) {
+ state = STATE_CLOSED;
+ }
+ },
+
+ // Switch the run state to STATE_QUITTING. This should be called once we're
+ // certain that the browser is going away and before we start collecting the
+ // final window states to save in the session file.
+ setQuitting() {
+ if (this.isRunning) {
+ state = STATE_QUITTING;
+ }
+ },
+});
diff --git a/browser/components/sessionstore/SessionCookies.jsm b/browser/components/sessionstore/SessionCookies.jsm
new file mode 100644
index 000000000..b99ab927b
--- /dev/null
+++ b/browser/components/sessionstore/SessionCookies.jsm
@@ -0,0 +1,476 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SessionCookies"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+ "resource://gre/modules/sessionstore/Utils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
+ "resource:///modules/sessionstore/PrivacyLevel.jsm");
+
+// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
+const MAX_EXPIRY = Math.pow(2, 62);
+
+/**
+ * The external API implemented by the SessionCookies module.
+ */
+this.SessionCookies = Object.freeze({
+ update: function (windows) {
+ SessionCookiesInternal.update(windows);
+ },
+
+ getHostsForWindow: function (window, checkPrivacy = false) {
+ return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy);
+ },
+
+ restore(cookies) {
+ SessionCookiesInternal.restore(cookies);
+ }
+});
+
+/**
+ * The internal API.
+ */
+var SessionCookiesInternal = {
+ /**
+ * Stores whether we're initialized, yet.
+ */
+ _initialized: false,
+
+ /**
+ * Retrieve the list of all hosts contained in the given windows' session
+ * history entries (per window) and collect the associated cookies for those
+ * hosts, if any. The given state object is being modified.
+ *
+ * @param windows
+ * Array of window state objects.
+ * [{ tabs: [...], cookies: [...] }, ...]
+ */
+ update: function (windows) {
+ this._ensureInitialized();
+
+ for (let window of windows) {
+ let cookies = [];
+
+ // Collect all hosts for the current window.
+ let hosts = this.getHostsForWindow(window, true);
+
+ for (let host of Object.keys(hosts)) {
+ let isPinned = hosts[host];
+
+ for (let cookie of CookieStore.getCookiesForHost(host)) {
+ // _getCookiesForHost() will only return hosts with the right privacy
+ // rules, so there is no need to do anything special with this call
+ // to PrivacyLevel.canSave().
+ if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) {
+ cookies.push(cookie);
+ }
+ }
+ }
+
+ // Don't include/keep empty cookie sections.
+ if (cookies.length) {
+ window.cookies = cookies;
+ } else if ("cookies" in window) {
+ delete window.cookies;
+ }
+ }
+ },
+
+ /**
+ * Returns a map of all hosts for a given window that we might want to
+ * collect cookies for.
+ *
+ * @param window
+ * A window state object containing tabs with history entries.
+ * @param checkPrivacy (bool)
+ * Whether to check the privacy level for each host.
+ * @return {object} A map of hosts for a given window state object. The keys
+ * will be hosts, the values are boolean and determine
+ * whether we will use the deferred privacy level when
+ * checking how much data to save on quitting.
+ */
+ getHostsForWindow: function (window, checkPrivacy = false) {
+ let hosts = {};
+
+ for (let tab of window.tabs) {
+ for (let entry of tab.entries) {
+ this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned);
+ }
+ }
+
+ return hosts;
+ },
+
+ /**
+ * Restores a given list of session cookies.
+ */
+ restore(cookies) {
+
+ for (let cookie of cookies) {
+ let expiry = "expiry" in cookie ? cookie.expiry : MAX_EXPIRY;
+ let cookieObj = {
+ host: cookie.host,
+ path: cookie.path || "",
+ name: cookie.name || ""
+ };
+ if (!Services.cookies.cookieExists(cookieObj, cookie.originAttributes || {})) {
+ Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "",
+ cookie.value, !!cookie.secure, !!cookie.httponly,
+ /* isSession = */ true, expiry, cookie.originAttributes || {});
+ }
+ }
+ },
+
+ /**
+ * Handles observers notifications that are sent whenever cookies are added,
+ * changed, or removed. Ensures that the storage is updated accordingly.
+ */
+ observe: function (subject, topic, data) {
+ switch (data) {
+ case "added":
+ case "changed":
+ this._updateCookie(subject);
+ break;
+ case "deleted":
+ this._removeCookie(subject);
+ break;
+ case "cleared":
+ CookieStore.clear();
+ break;
+ case "batch-deleted":
+ this._removeCookies(subject);
+ break;
+ case "reload":
+ CookieStore.clear();
+ this._reloadCookies();
+ break;
+ default:
+ throw new Error("Unhandled cookie-changed notification.");
+ }
+ },
+
+ /**
+ * If called for the first time in a session, iterates all cookies in the
+ * cookies service and puts them into the store if they're session cookies.
+ */
+ _ensureInitialized: function () {
+ if (!this._initialized) {
+ this._reloadCookies();
+ this._initialized = true;
+ Services.obs.addObserver(this, "cookie-changed", false);
+ }
+ },
+
+ /**
+ * Fill a given map with hosts found in the given entry's session history and
+ * any child entries.
+ *
+ * @param entry
+ * the history entry, serialized
+ * @param hosts
+ * the hash that will be used to store hosts eg, { hostname: true }
+ * @param checkPrivacy
+ * should we check the privacy level for https
+ * @param isPinned
+ * is the entry we're evaluating for a pinned tab; used only if
+ * checkPrivacy
+ */
+ _extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) {
+ let host = entry._host;
+ let scheme = entry._scheme;
+
+ // If host & scheme aren't defined, then we are likely here in the startup
+ // process via _splitCookiesFromWindow. In that case, we'll turn entry.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 = Utils.makeURI(entry.url);
+ host = uri.host;
+ scheme = uri.scheme;
+ this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned);
+ }
+ catch (ex) { }
+ }
+
+ if (entry.children) {
+ for (let child of entry.children) {
+ this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned);
+ }
+ }
+ },
+
+ /**
+ * Add a given host to a given map of hosts if the privacy level allows
+ * saving cookie data for it.
+ *
+ * @param host
+ * the host of a uri (usually via nsIURI.host)
+ * @param scheme
+ * the scheme of a uri (usually via nsIURI.scheme)
+ * @param hosts
+ * the hash that will be used to store hosts eg, { hostname: true }
+ * @param checkPrivacy
+ * should we check the privacy level for https
+ * @param isPinned
+ * is the entry we're evaluating for a pinned tab; used only if
+ * checkPrivacy
+ */
+ _extractHostsFromHostScheme:
+ function (host, scheme, hosts, checkPrivacy, isPinned) {
+ // host and scheme may not be set (for about: urls for example), in which
+ // case testing scheme will be sufficient.
+ if (/https?/.test(scheme) && !hosts[host] &&
+ (!checkPrivacy ||
+ PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) {
+ // By setting this to true or false, we can determine when looking at
+ // the host in update() if we should check for privacy.
+ hosts[host] = isPinned;
+ } else if (scheme == "file") {
+ hosts[host] = true;
+ }
+ },
+
+ /**
+ * Updates or adds a given cookie to the store.
+ */
+ _updateCookie: function (cookie) {
+ cookie.QueryInterface(Ci.nsICookie2);
+
+ if (cookie.isSession) {
+ CookieStore.set(cookie);
+ } else {
+ CookieStore.delete(cookie);
+ }
+ },
+
+ /**
+ * Removes a given cookie from the store.
+ */
+ _removeCookie: function (cookie) {
+ cookie.QueryInterface(Ci.nsICookie2);
+
+ if (cookie.isSession) {
+ CookieStore.delete(cookie);
+ }
+ },
+
+ /**
+ * Removes a given list of cookies from the store.
+ */
+ _removeCookies: function (cookies) {
+ for (let i = 0; i < cookies.length; i++) {
+ this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2));
+ }
+ },
+
+ /**
+ * Iterates all cookies in the cookies service and puts them into the store
+ * if they're session cookies.
+ */
+ _reloadCookies: function () {
+ let iter = Services.cookies.enumerator;
+ while (iter.hasMoreElements()) {
+ this._updateCookie(iter.getNext());
+ }
+ }
+};
+
+/**
+ * Generates all possible subdomains for a given host and prepends a leading
+ * dot to all variants.
+ *
+ * See http://tools.ietf.org/html/rfc6265#section-5.1.3
+ * http://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_Path
+ *
+ * All cookies belonging to a web page will be internally represented by a
+ * nsICookie object. nsICookie.host will be the request host if no domain
+ * parameter was given when setting the cookie. If a specific domain was given
+ * then nsICookie.host will contain that specific domain and prepend a leading
+ * dot to it.
+ *
+ * We thus generate all possible subdomains for a given domain and prepend a
+ * leading dot to them as that is the value that was used as the map key when
+ * the cookie was set.
+ */
+function* getPossibleSubdomainVariants(host) {
+ // Try given domain with a leading dot (.www.example.com).
+ yield "." + host;
+
+ // Stop if there are only two parts left (e.g. example.com was given).
+ let parts = host.split(".");
+ if (parts.length < 3) {
+ return;
+ }
+
+ // Remove the first subdomain (www.example.com -> example.com).
+ let rest = parts.slice(1).join(".");
+
+ // Try possible parent subdomains.
+ yield* getPossibleSubdomainVariants(rest);
+}
+
+/**
+ * The internal cookie storage that keeps track of every active session cookie.
+ * These are stored using maps per host, path, and cookie name.
+ */
+var CookieStore = {
+ /**
+ * The internal structure holding all known cookies.
+ *
+ * Host =>
+ * Path =>
+ * Name => {path: "/", name: "sessionid", secure: true}
+ *
+ * Maps are used for storage but the data structure is equivalent to this:
+ *
+ * this._hosts = {
+ * "www.mozilla.org": {
+ * "/": {
+ * "username": {name: "username", value: "my_name_is", etc...},
+ * "sessionid": {name: "sessionid", value: "1fdb3a", etc...}
+ * }
+ * },
+ * "tbpl.mozilla.org": {
+ * "/path": {
+ * "cookiename": {name: "cookiename", value: "value", etc...}
+ * }
+ * },
+ * ".example.com": {
+ * "/path": {
+ * "cookiename": {name: "cookiename", value: "value", etc...}
+ * }
+ * }
+ * };
+ */
+ _hosts: new Map(),
+
+ /**
+ * Returns the list of stored session cookies for a given host.
+ *
+ * @param host
+ * A string containing the host name we want to get cookies for.
+ */
+ getCookiesForHost: function (host) {
+ let cookies = [];
+
+ let appendCookiesForHost = host => {
+ if (!this._hosts.has(host)) {
+ return;
+ }
+
+ for (let pathToNamesMap of this._hosts.get(host).values()) {
+ for (let nameToCookiesMap of pathToNamesMap.values()) {
+ cookies.push(...nameToCookiesMap.values());
+ }
+ }
+ }
+
+ // Try to find cookies for the given host, e.g. <www.example.com>.
+ // The full hostname will be in the map if the Set-Cookie header did not
+ // have a domain= attribute, i.e. the cookie will only be stored for the
+ // request domain. Also, try to find cookies for subdomains, e.g.
+ // <.example.com>. We will find those variants with a leading dot in the
+ // map if the Set-Cookie header had a domain= attribute, i.e. the cookie
+ // will be stored for a parent domain and we send it for any subdomain.
+ for (let variant of [host, ...getPossibleSubdomainVariants(host)]) {
+ appendCookiesForHost(variant);
+ }
+
+ return cookies;
+ },
+
+ /**
+ * Stores a given cookie.
+ *
+ * @param cookie
+ * The nsICookie2 object to add to the storage.
+ */
+ set: function (cookie) {
+ let jscookie = {host: cookie.host, value: cookie.value};
+
+ // Only add properties with non-default values to save a few bytes.
+ 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;
+ }
+
+ if (cookie.originAttributes) {
+ jscookie.originAttributes = cookie.originAttributes;
+ }
+
+ this._ensureMap(cookie).set(cookie.name, jscookie);
+ },
+
+ /**
+ * Removes a given cookie.
+ *
+ * @param cookie
+ * The nsICookie2 object to be removed from storage.
+ */
+ delete: function (cookie) {
+ this._ensureMap(cookie).delete(cookie.name);
+ },
+
+ /**
+ * Removes all cookies.
+ */
+ clear: function () {
+ this._hosts.clear();
+ },
+
+ /**
+ * Creates all maps necessary to store a given cookie.
+ *
+ * @param cookie
+ * The nsICookie2 object to create maps for.
+ *
+ * @return The newly created Map instance mapping cookie names to
+ * internal jscookies, in the given path of the given host.
+ */
+ _ensureMap: function (cookie) {
+ if (!this._hosts.has(cookie.host)) {
+ this._hosts.set(cookie.host, new Map());
+ }
+
+ let originAttributesMap = this._hosts.get(cookie.host);
+ // If cookie.originAttributes is null, originAttributes will be an empty string.
+ let originAttributes = ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
+ if (!originAttributesMap.has(originAttributes)) {
+ originAttributesMap.set(originAttributes, new Map());
+ }
+
+ let pathToNamesMap = originAttributesMap.get(originAttributes);
+
+ if (!pathToNamesMap.has(cookie.path)) {
+ pathToNamesMap.set(cookie.path, new Map());
+ }
+
+ return pathToNamesMap.get(cookie.path);
+ }
+};
diff --git a/browser/components/sessionstore/SessionFile.jsm b/browser/components/sessionstore/SessionFile.jsm
new file mode 100644
index 000000000..80c4e7790
--- /dev/null
+++ b/browser/components/sessionstore/SessionFile.jsm
@@ -0,0 +1,399 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SessionFile"];
+
+/**
+ * Implementation of all the disk I/O required by the session store.
+ * This is a private API, meant to be used only by the session store.
+ * It will change. Do not use it for any other purpose.
+ *
+ * Note that this module implicitly depends on one of two things:
+ * 1. either the asynchronous file I/O system enqueues its requests
+ * and never attempts to simultaneously execute two I/O requests on
+ * the files used by this module from two distinct threads; or
+ * 2. the clients of this API are well-behaved and do not place
+ * concurrent requests to the files used by this module.
+ *
+ * Otherwise, we could encounter bugs, especially under Windows,
+ * e.g. if a request attempts to write sessionstore.js while
+ * another attempts to copy that file.
+ *
+ * This implementation uses OS.File, which guarantees property 1.
+ */
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RunState",
+ "resource:///modules/sessionstore/RunState.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
+ "@mozilla.org/base/telemetry;1", "nsITelemetry");
+XPCOMUtils.defineLazyServiceGetter(this, "sessionStartup",
+ "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionWorker",
+ "resource:///modules/sessionstore/SessionWorker.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+ "resource:///modules/sessionstore/SessionStore.jsm");
+
+const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
+const PREF_MAX_UPGRADE_BACKUPS = "browser.sessionstore.upgradeBackup.maxUpgradeBackups";
+
+const PREF_MAX_SERIALIZE_BACK = "browser.sessionstore.max_serialize_back";
+const PREF_MAX_SERIALIZE_FWD = "browser.sessionstore.max_serialize_forward";
+
+this.SessionFile = {
+ /**
+ * Read the contents of the session file, asynchronously.
+ */
+ read: function () {
+ return SessionFileInternal.read();
+ },
+ /**
+ * Write the contents of the session file, asynchronously.
+ */
+ write: function (aData) {
+ return SessionFileInternal.write(aData);
+ },
+ /**
+ * Wipe the contents of the session file, asynchronously.
+ */
+ wipe: function () {
+ return SessionFileInternal.wipe();
+ },
+
+ /**
+ * Return the paths to the files used to store, backup, etc.
+ * the state of the file.
+ */
+ get Paths() {
+ return SessionFileInternal.Paths;
+ }
+};
+
+Object.freeze(SessionFile);
+
+var Path = OS.Path;
+var profileDir = OS.Constants.Path.profileDir;
+
+var SessionFileInternal = {
+ Paths: Object.freeze({
+ // The path to the latest version of sessionstore written during a clean
+ // shutdown. After startup, it is renamed `cleanBackup`.
+ clean: Path.join(profileDir, "sessionstore.js"),
+
+ // The path at which we store the previous version of `clean`. Updated
+ // whenever we successfully load from `clean`.
+ cleanBackup: Path.join(profileDir, "sessionstore-backups", "previous.js"),
+
+ // The directory containing all sessionstore backups.
+ backups: Path.join(profileDir, "sessionstore-backups"),
+
+ // The path to the latest version of the sessionstore written
+ // during runtime. Generally, this file contains more
+ // privacy-sensitive information than |clean|, and this file is
+ // therefore removed during clean shutdown. This file is designed to protect
+ // against crashes / sudden shutdown.
+ recovery: Path.join(profileDir, "sessionstore-backups", "recovery.js"),
+
+ // The path to the previous version of the sessionstore written
+ // during runtime (e.g. 15 seconds before recovery). In case of a
+ // clean shutdown, this file is removed. Generally, this file
+ // contains more privacy-sensitive information than |clean|, and
+ // this file is therefore removed during clean shutdown. This
+ // file is designed to protect against crashes that are nasty
+ // enough to corrupt |recovery|.
+ recoveryBackup: Path.join(profileDir, "sessionstore-backups", "recovery.bak"),
+
+ // The path to a backup created during an upgrade of Firefox.
+ // Having this backup protects the user essentially from bugs in
+ // Firefox or add-ons, especially for users of Nightly. This file
+ // does not contain any information more sensitive than |clean|.
+ upgradeBackupPrefix: Path.join(profileDir, "sessionstore-backups", "upgrade.js-"),
+
+ // The path to the backup of the version of the session store used
+ // during the latest upgrade of Firefox. During load/recovery,
+ // this file should be used if both |path|, |backupPath| and
+ // |latestStartPath| are absent/incorrect. May be "" if no
+ // upgrade backup has ever been performed. This file does not
+ // contain any information more sensitive than |clean|.
+ get upgradeBackup() {
+ let latestBackupID = SessionFileInternal.latestUpgradeBackupID;
+ if (!latestBackupID) {
+ return "";
+ }
+ return this.upgradeBackupPrefix + latestBackupID;
+ },
+
+ // The path to a backup created during an upgrade of Firefox.
+ // Having this backup protects the user essentially from bugs in
+ // Firefox, especially for users of Nightly.
+ get nextUpgradeBackup() {
+ return this.upgradeBackupPrefix + Services.appinfo.platformBuildID;
+ },
+
+ /**
+ * The order in which to search for a valid sessionstore file.
+ */
+ get loadOrder() {
+ // If `clean` exists and has been written without corruption during
+ // the latest shutdown, we need to use it.
+ //
+ // Otherwise, `recovery` and `recoveryBackup` represent the most
+ // recent state of the session store.
+ //
+ // Finally, if nothing works, fall back to the last known state
+ // that can be loaded (`cleanBackup`) or, if available, to the
+ // backup performed during the latest upgrade.
+ let order = ["clean",
+ "recovery",
+ "recoveryBackup",
+ "cleanBackup"];
+ if (SessionFileInternal.latestUpgradeBackupID) {
+ // We have an upgradeBackup
+ order.push("upgradeBackup");
+ }
+ return order;
+ },
+ }),
+
+ // Number of attempted calls to `write`.
+ // Note that we may have _attempts > _successes + _failures,
+ // if attempts never complete.
+ // Used for error reporting.
+ _attempts: 0,
+
+ // Number of successful calls to `write`.
+ // Used for error reporting.
+ _successes: 0,
+
+ // Number of failed calls to `write`.
+ // Used for error reporting.
+ _failures: 0,
+
+ // Resolved once initialization is complete.
+ // The promise never rejects.
+ _deferredInitialized: PromiseUtils.defer(),
+
+ // `true` once we have started initialization, i.e. once something
+ // has been scheduled that will eventually resolve `_deferredInitialized`.
+ _initializationStarted: false,
+
+ // The ID of the latest version of Gecko for which we have an upgrade backup
+ // or |undefined| if no upgrade backup was ever written.
+ get latestUpgradeBackupID() {
+ try {
+ return Services.prefs.getCharPref(PREF_UPGRADE_BACKUP);
+ } catch (ex) {
+ return undefined;
+ }
+ },
+
+ // Find the correct session file, read it and setup the worker.
+ read: Task.async(function* () {
+ this._initializationStarted = true;
+
+ let result;
+ let noFilesFound = true;
+ // Attempt to load by order of priority from the various backups
+ for (let key of this.Paths.loadOrder) {
+ let corrupted = false;
+ let exists = true;
+ try {
+ let path = this.Paths[key];
+ let startMs = Date.now();
+
+ let source = yield OS.File.read(path, { encoding: "utf-8" });
+ let parsed = JSON.parse(source);
+
+ if (!SessionStore.isFormatVersionCompatible(parsed.version || ["sessionrestore", 0] /*fallback for old versions*/)) {
+ // Skip sessionstore files that we don't understand.
+ Cu.reportError("Cannot extract data from Session Restore file " + path + ". Wrong format/version: " + JSON.stringify(parsed.version) + ".");
+ continue;
+ }
+ result = {
+ origin: key,
+ source: source,
+ parsed: parsed
+ };
+ Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
+ add(false);
+ Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS").
+ add(Date.now() - startMs);
+ break;
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ exists = false;
+ } catch (ex if ex instanceof OS.File.Error) {
+ // The file might be inaccessible due to wrong permissions
+ // or similar failures. We'll just count it as "corrupted".
+ console.error("Could not read session file ", ex, ex.stack);
+ corrupted = true;
+ } catch (ex if ex instanceof SyntaxError) {
+ console.error("Corrupt session file (invalid JSON found) ", ex, ex.stack);
+ // File is corrupted, try next file
+ corrupted = true;
+ } finally {
+ if (exists) {
+ noFilesFound = false;
+ Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").
+ add(corrupted);
+ }
+ }
+ }
+
+ // All files are corrupted if files found but none could deliver a result.
+ let allCorrupt = !noFilesFound && !result;
+ Telemetry.getHistogramById("FX_SESSION_RESTORE_ALL_FILES_CORRUPT").
+ add(allCorrupt);
+
+ if (!result) {
+ // If everything fails, start with an empty session.
+ result = {
+ origin: "empty",
+ source: "",
+ parsed: null
+ };
+ }
+
+ result.noFilesFound = noFilesFound;
+
+ // Initialize the worker (in the background) to let it handle backups and also
+ // as a workaround for bug 964531.
+ let promiseInitialized = SessionWorker.post("init", [result.origin, this.Paths, {
+ maxUpgradeBackups: Preferences.get(PREF_MAX_UPGRADE_BACKUPS, 3),
+ maxSerializeBack: Preferences.get(PREF_MAX_SERIALIZE_BACK, 10),
+ maxSerializeForward: Preferences.get(PREF_MAX_SERIALIZE_FWD, -1)
+ }]);
+
+ promiseInitialized.catch(err => {
+ // Ensure that we report errors but that they do not stop us.
+ Promise.reject(err);
+ }).then(() => this._deferredInitialized.resolve());
+
+ return result;
+ }),
+
+ // Post a message to the worker, making sure that it has been initialized
+ // first.
+ _postToWorker: Task.async(function*(...args) {
+ if (!this._initializationStarted) {
+ // Initializing the worker is somewhat complex, as proper handling of
+ // backups requires us to first read and check the session. Consequently,
+ // the only way to initialize the worker is to first call `this.read()`.
+
+ // The call to `this.read()` causes background initialization of the worker.
+ // Initialization will be complete once `this._deferredInitialized.promise`
+ // resolves.
+ this.read();
+ }
+ yield this._deferredInitialized.promise;
+ return SessionWorker.post(...args)
+ }),
+
+ write: function (aData) {
+ if (RunState.isClosed) {
+ return Promise.reject(new Error("SessionFile is closed"));
+ }
+
+ let isFinalWrite = false;
+ if (RunState.isClosing) {
+ // If shutdown has started, we will want to stop receiving
+ // write instructions.
+ isFinalWrite = true;
+ RunState.setClosed();
+ }
+
+ let performShutdownCleanup = isFinalWrite &&
+ !sessionStartup.isAutomaticRestoreEnabled();
+
+ this._attempts++;
+ let options = {isFinalWrite, performShutdownCleanup};
+ let promise = this._postToWorker("write", [aData, options]);
+
+ // Wait until the write is done.
+ promise = promise.then(msg => {
+ // Record how long the write took.
+ this._recordTelemetry(msg.telemetry);
+ this._successes++;
+ if (msg.result.upgradeBackup) {
+ // We have just completed a backup-on-upgrade, store the information
+ // in preferences.
+ Services.prefs.setCharPref(PREF_UPGRADE_BACKUP,
+ Services.appinfo.platformBuildID);
+ }
+ }, err => {
+ // Catch and report any errors.
+ console.error("Could not write session state file ", err, err.stack);
+ this._failures++;
+ // By not doing anything special here we ensure that |promise| cannot
+ // be rejected anymore. The shutdown/cleanup code at the end of the
+ // function will thus always be executed.
+ });
+
+ // Ensure that we can write sessionstore.js cleanly before the profile
+ // becomes unaccessible.
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ "SessionFile: Finish writing Session Restore data",
+ promise,
+ {
+ fetchState: () => ({
+ options,
+ attempts: this._attempts,
+ successes: this._successes,
+ failures: this._failures,
+ })
+ });
+
+ // This code will always be executed because |promise| can't fail anymore.
+ // We ensured that by having a reject handler that reports the failure but
+ // doesn't forward the rejection.
+ return promise.then(() => {
+ // Remove the blocker, no matter if writing failed or not.
+ AsyncShutdown.profileBeforeChange.removeBlocker(promise);
+
+ if (isFinalWrite) {
+ Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
+ }
+ });
+ },
+
+ wipe: function () {
+ return this._postToWorker("wipe");
+ },
+
+ _recordTelemetry: function(telemetry) {
+ for (let id of Object.keys(telemetry)){
+ let value = telemetry[id];
+ let samples = [];
+ if (Array.isArray(value)) {
+ samples.push(...value);
+ } else {
+ samples.push(value);
+ }
+ let histogram = Telemetry.getHistogramById(id);
+ for (let sample of samples) {
+ histogram.add(sample);
+ }
+ }
+ }
+};
diff --git a/browser/components/sessionstore/SessionHistory.jsm b/browser/components/sessionstore/SessionHistory.jsm
new file mode 100644
index 000000000..aa9c10379
--- /dev/null
+++ b/browser/components/sessionstore/SessionHistory.jsm
@@ -0,0 +1,428 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SessionHistory"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+ "resource://gre/modules/sessionstore/Utils.jsm");
+
+function debug(msg) {
+ Services.console.logStringMessage("SessionHistory: " + msg);
+}
+
+/**
+ * The external API exported by this module.
+ */
+this.SessionHistory = Object.freeze({
+ isEmpty: function (docShell) {
+ return SessionHistoryInternal.isEmpty(docShell);
+ },
+
+ collect: function (docShell) {
+ return SessionHistoryInternal.collect(docShell);
+ },
+
+ restore: function (docShell, tabData) {
+ SessionHistoryInternal.restore(docShell, tabData);
+ }
+});
+
+/**
+ * The internal API for the SessionHistory module.
+ */
+var SessionHistoryInternal = {
+ /**
+ * Returns whether the given docShell's session history is empty.
+ *
+ * @param docShell
+ * The docShell that owns the session history.
+ */
+ isEmpty: function (docShell) {
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let history = webNavigation.sessionHistory;
+ if (!webNavigation.currentURI) {
+ return true;
+ }
+ let uri = webNavigation.currentURI.spec;
+ return uri == "about:blank" && history.count == 0;
+ },
+
+ /**
+ * Collects session history data for a given docShell.
+ *
+ * @param docShell
+ * The docShell that owns the session history.
+ */
+ collect: function (docShell) {
+ let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
+
+ let data = {entries: [], userContextId: loadContext.originAttributes.userContextId };
+
+ if (history && history.count > 0) {
+ // Loop over the transaction linked list directly so we can get the
+ // persist property for each transaction.
+ for (let txn = history.rootTransaction; txn; txn = txn.next) {
+ let entry = this.serializeEntry(txn.sHEntry);
+ entry.persist = txn.persist;
+ data.entries.push(entry);
+ }
+
+ // Ensure the index isn't out of bounds if an exception was thrown above.
+ data.index = Math.min(history.index + 1, data.entries.length);
+ }
+
+ // If either the session history isn't available yet or doesn't have any
+ // valid entries, make sure we at least include the current page.
+ if (data.entries.length == 0) {
+ let uri = webNavigation.currentURI.spec;
+ let body = webNavigation.document.body;
+ // We landed here because the history is inaccessible or there are no
+ // history entries. In that case we should at least record the docShell's
+ // current URL as a single history entry. If the URL is not about:blank
+ // or it's a blank tab that was modified (like a custom newtab page),
+ // record it. For about:blank we explicitly want an empty array without
+ // an 'index' property to denote that there are no history entries.
+ if (uri != "about:blank" || (body && body.hasChildNodes())) {
+ data.entries.push({ url: uri });
+ data.index = 1;
+ }
+ }
+
+ return data;
+ },
+
+ /**
+ * Get an object that is a serialized representation of a History entry.
+ *
+ * @param shEntry
+ * nsISHEntry instance
+ * @return object
+ */
+ serializeEntry: function (shEntry) {
+ let entry = { url: shEntry.URI.spec };
+
+ // Save some bytes and don't include the title property
+ // if that's identical to the current entry's URL.
+ if (shEntry.title && shEntry.title != entry.url) {
+ entry.title = shEntry.title;
+ }
+ if (shEntry.isSubFrame) {
+ entry.subframe = true;
+ }
+
+ entry.charset = shEntry.URI.originCharset;
+
+ let cacheKey = shEntry.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 = shEntry.ID;
+ entry.docshellID = shEntry.docshellID;
+
+ // We will include the property only if it's truthy to save a couple of
+ // bytes when the resulting object is stringified and saved to disk.
+ if (shEntry.referrerURI) {
+ entry.referrer = shEntry.referrerURI.spec;
+ entry.referrerPolicy = shEntry.referrerPolicy;
+ }
+
+ if (shEntry.originalURI) {
+ entry.originalURI = shEntry.originalURI.spec;
+ }
+
+ if (shEntry.loadReplace) {
+ entry.loadReplace = shEntry.loadReplace;
+ }
+
+ if (shEntry.srcdocData)
+ entry.srcdocData = shEntry.srcdocData;
+
+ if (shEntry.isSrcdocEntry)
+ entry.isSrcdocEntry = shEntry.isSrcdocEntry;
+
+ if (shEntry.baseURI)
+ entry.baseURI = shEntry.baseURI.spec;
+
+ if (shEntry.contentType)
+ entry.contentType = shEntry.contentType;
+
+ if (shEntry.scrollRestorationIsManual) {
+ entry.scrollRestorationIsManual = true;
+ } else {
+ let x = {}, y = {};
+ shEntry.getScrollPosition(x, y);
+ if (x.value != 0 || y.value != 0)
+ entry.scroll = x.value + "," + y.value;
+ }
+
+ // Collect triggeringPrincipal data for the current history entry.
+ // Please note that before Bug 1297338 there was no concept of a
+ // principalToInherit. To remain backward/forward compatible we
+ // serialize the principalToInherit as triggeringPrincipal_b64.
+ // Once principalToInherit is well established (within FF55)
+ // we can update this code, remove triggeringPrincipal_b64 and
+ // just keep triggeringPrincipal_base64 as well as
+ // principalToInherit_base64; see Bug 1301666.
+ if (shEntry.principalToInherit) {
+ try {
+ let principalToInherit = Utils.serializePrincipal(shEntry.principalToInherit);
+ if (principalToInherit) {
+ entry.triggeringPrincipal_b64 = principalToInherit;
+ entry.principalToInherit_base64 = principalToInherit;
+ }
+ } catch (e) {
+ debug(e);
+ }
+ }
+
+ if (shEntry.triggeringPrincipal) {
+ try {
+ let triggeringPrincipal = Utils.serializePrincipal(shEntry.triggeringPrincipal);
+ if (triggeringPrincipal) {
+ entry.triggeringPrincipal_base64 = triggeringPrincipal;
+ }
+ } catch (e) {
+ debug(e);
+ }
+ }
+
+ entry.docIdentifier = shEntry.BFCacheEntry.ID;
+
+ if (shEntry.stateData != null) {
+ entry.structuredCloneState = shEntry.stateData.getDataAsBase64();
+ entry.structuredCloneVersion = shEntry.stateData.formatVersion;
+ }
+
+ if (!(shEntry instanceof Ci.nsISHContainer)) {
+ return entry;
+ }
+
+ if (shEntry.childCount > 0 && !shEntry.hasDynamicallyAddedChild()) {
+ let children = [];
+ for (let i = 0; i < shEntry.childCount; i++) {
+ let child = shEntry.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.length = 0;
+ break;
+ }
+
+ children.push(this.serializeEntry(child));
+ }
+ }
+
+ if (children.length) {
+ entry.children = children;
+ }
+ }
+
+ return entry;
+ },
+
+ /**
+ * Restores session history data for a given docShell.
+ *
+ * @param docShell
+ * The docShell that owns the session history.
+ * @param tabData
+ * The tabdata including all history entries.
+ */
+ restore: function (docShell, tabData) {
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let history = webNavigation.sessionHistory;
+ if (history.count > 0) {
+ history.PurgeHistory(history.count);
+ }
+ history.QueryInterface(Ci.nsISHistoryInternal);
+
+ let idMap = { used: {} };
+ let docIdentMap = {};
+ for (let i = 0; i < tabData.entries.length; i++) {
+ let entry = tabData.entries[i];
+ //XXXzpao Wallpaper patch for bug 514751
+ if (!entry.url)
+ continue;
+ let persist = "persist" in entry ? entry.persist : true;
+ history.addEntry(this.deserializeEntry(entry, idMap, docIdentMap), persist);
+ }
+
+ // Select the right history entry.
+ let index = tabData.index - 1;
+ if (index < history.count && history.index != index) {
+ history.getEntryAtIndex(index, true);
+ }
+ },
+
+ /**
+ * Expands serialized history data into a session-history-entry instance.
+ *
+ * @param entry
+ * Object containing serialized history data for a URL
+ * @param idMap
+ * Hash for ensuring unique frame IDs
+ * @param docIdentMap
+ * Hash to ensure reuse of BFCache entries
+ * @returns nsISHEntry
+ */
+ deserializeEntry: function (entry, idMap, docIdentMap) {
+
+ var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].
+ createInstance(Ci.nsISHEntry);
+
+ shEntry.setURI(Utils.makeURI(entry.url, entry.charset));
+ shEntry.setTitle(entry.title || entry.url);
+ if (entry.subframe)
+ shEntry.setIsSubFrame(entry.subframe || false);
+ shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
+ if (entry.contentType)
+ shEntry.contentType = entry.contentType;
+ if (entry.referrer) {
+ shEntry.referrerURI = Utils.makeURI(entry.referrer);
+ shEntry.referrerPolicy = entry.referrerPolicy;
+ }
+ if (entry.originalURI) {
+ shEntry.originalURI = Utils.makeURI(entry.originalURI);
+ }
+ if (entry.loadReplace) {
+ shEntry.loadReplace = entry.loadReplace;
+ }
+ if (entry.isSrcdocEntry)
+ shEntry.srcdocData = entry.srcdocData;
+ if (entry.baseURI)
+ shEntry.baseURI = Utils.makeURI(entry.baseURI);
+
+ if (entry.cacheKey) {
+ var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].
+ createInstance(Ci.nsISupportsPRUint32);
+ cacheKey.data = entry.cacheKey;
+ shEntry.cacheKey = cacheKey;
+ }
+
+ if (entry.ID) {
+ // get a new unique ID for this frame (since the one from the last
+ // start might already be in use)
+ var id = idMap[entry.ID] || 0;
+ if (!id) {
+ for (id = Date.now(); id in idMap.used; id++);
+ idMap[entry.ID] = id;
+ idMap.used[id] = true;
+ }
+ shEntry.ID = id;
+ }
+
+ if (entry.docshellID)
+ shEntry.docshellID = entry.docshellID;
+
+ if (entry.structuredCloneState && entry.structuredCloneVersion) {
+ shEntry.stateData =
+ Cc["@mozilla.org/docshell/structured-clone-container;1"].
+ createInstance(Ci.nsIStructuredCloneContainer);
+
+ shEntry.stateData.initFromBase64(entry.structuredCloneState,
+ entry.structuredCloneVersion);
+ }
+
+ if (entry.scrollRestorationIsManual) {
+ shEntry.scrollRestorationIsManual = true;
+ } else if (entry.scroll) {
+ var scrollPos = (entry.scroll || "0,0").split(",");
+ scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
+ shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
+ }
+
+ let childDocIdents = {};
+ if (entry.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 = docIdentMap[entry.docIdentifier];
+ if (!matchingEntry) {
+ matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
+ docIdentMap[entry.docIdentifier] = matchingEntry;
+ }
+ else {
+ shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
+ childDocIdents = matchingEntry.childDocIdents;
+ }
+ }
+
+ // The field entry.owner_b64 got renamed to entry.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 (entry.owner_b64) {
+ entry.triggeringPricipal_b64 = entry.owner_b64;
+ delete entry.owner_b64;
+ }
+
+ // Before introducing the concept of principalToInherit we only had
+ // a triggeringPrincipal within every entry which basically is the
+ // equivalent of the new principalToInherit. To avoid compatibility
+ // issues, we first check if the entry has entries for
+ // triggeringPrincipal_base64 and principalToInherit_base64. If not
+ // we fall back to using the principalToInherit (which is stored
+ // as triggeringPrincipal_b64) as the triggeringPrincipal and
+ // the principalToInherit.
+ // FF55 will remove the triggeringPrincipal_b64, see Bug 1301666.
+ if (entry.triggeringPrincipal_base64 || entry.principalToInherit_base64) {
+ if (entry.triggeringPrincipal_base64) {
+ shEntry.triggeringPrincipal =
+ Utils.deserializePrincipal(entry.triggeringPrincipal_base64);
+ }
+ if (entry.principalToInherit_base64) {
+ shEntry.principalToInherit =
+ Utils.deserializePrincipal(entry.principalToInherit_base64);
+ }
+ } else if (entry.triggeringPrincipal_b64) {
+ shEntry.triggeringPrincipal = Utils.deserializePrincipal(entry.triggeringPrincipal_b64);
+ shEntry.principalToInherit = shEntry.triggeringPrincipal;
+ }
+
+ if (entry.children && shEntry instanceof Ci.nsISHContainer) {
+ for (var i = 0; i < entry.children.length; i++) {
+ //XXXzpao Wallpaper patch for bug 514751
+ if (!entry.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.deserializeEntry(entry.children[i], idMap,
+ childDocIdents), i);
+ }
+ }
+
+ return shEntry;
+ },
+
+};
diff --git a/browser/components/sessionstore/SessionMigration.jsm b/browser/components/sessionstore/SessionMigration.jsm
new file mode 100644
index 000000000..ff339eba9
--- /dev/null
+++ b/browser/components/sessionstore/SessionMigration.jsm
@@ -0,0 +1,100 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SessionMigration"];
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+// An encoder to UTF-8.
+XPCOMUtils.defineLazyGetter(this, "gEncoder", function () {
+ return new TextEncoder();
+});
+
+// A decoder.
+XPCOMUtils.defineLazyGetter(this, "gDecoder", function () {
+ return new TextDecoder();
+});
+
+var SessionMigrationInternal = {
+ /**
+ * Convert the original session restore state into a minimal state. It will
+ * only contain:
+ * - open windows
+ * - with tabs
+ * - with history entries with only title, url
+ * - with pinned state
+ * - with tab group info (hidden + group id)
+ * - with selected tab info
+ * - with selected window info
+ *
+ * The complete state is then wrapped into the "about:welcomeback" page as
+ * form field info to be restored when restoring the state.
+ */
+ convertState: function(aStateObj) {
+ let state = {
+ selectedWindow: aStateObj.selectedWindow,
+ _closedWindows: []
+ };
+ state.windows = aStateObj.windows.map(function(oldWin) {
+ var win = {extData: {}};
+ win.tabs = oldWin.tabs.map(function(oldTab) {
+ var tab = {};
+ // Keep only titles and urls for history entries
+ tab.entries = oldTab.entries.map(function(entry) {
+ return {url: entry.url, title: entry.title};
+ });
+ tab.index = oldTab.index;
+ tab.hidden = oldTab.hidden;
+ tab.pinned = oldTab.pinned;
+ return tab;
+ });
+ win.selected = oldWin.selected;
+ win._closedTabs = [];
+ return win;
+ });
+ let url = "about:welcomeback";
+ let formdata = {id: {sessionData: state}, url};
+ return {windows: [{tabs: [{entries: [{url}], formdata}]}]};
+ },
+ /**
+ * Asynchronously read session restore state (JSON) from a path
+ */
+ readState: function(aPath) {
+ return Task.spawn(function() {
+ let bytes = yield OS.File.read(aPath);
+ let text = gDecoder.decode(bytes);
+ let state = JSON.parse(text);
+ throw new Task.Result(state);
+ });
+ },
+ /**
+ * Asynchronously write session restore state as JSON to a path
+ */
+ writeState: function(aPath, aState) {
+ let bytes = gEncoder.encode(JSON.stringify(aState));
+ return OS.File.writeAtomic(aPath, bytes, {tmpPath: aPath + ".tmp"});
+ }
+}
+
+var SessionMigration = {
+ /**
+ * Migrate a limited set of session data from one path to another.
+ */
+ migrate: function(aFromPath, aToPath) {
+ return Task.spawn(function() {
+ let inState = yield SessionMigrationInternal.readState(aFromPath);
+ let outState = SessionMigrationInternal.convertState(inState);
+ // Unfortunately, we can't use SessionStore's own SessionFile to
+ // write out the data because it has a dependency on the profile dir
+ // being known. When the migration runs, there is no guarantee that
+ // that's true.
+ yield SessionMigrationInternal.writeState(aToPath, outState);
+ });
+ }
+};
diff --git a/browser/components/sessionstore/SessionSaver.jsm b/browser/components/sessionstore/SessionSaver.jsm
new file mode 100644
index 000000000..d672f8877
--- /dev/null
+++ b/browser/components/sessionstore/SessionSaver.jsm
@@ -0,0 +1,264 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SessionSaver"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
+ "resource:///modules/sessionstore/PrivacyFilter.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RunState",
+ "resource:///modules/sessionstore/RunState.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+ "resource:///modules/sessionstore/SessionStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
+ "resource:///modules/sessionstore/SessionFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+// Minimal interval between two save operations (in milliseconds).
+XPCOMUtils.defineLazyGetter(this, "gInterval", function () {
+ const PREF = "browser.sessionstore.interval";
+
+ // Observer that updates the cached value when the preference changes.
+ Services.prefs.addObserver(PREF, () => {
+ this.gInterval = Services.prefs.getIntPref(PREF);
+
+ // Cancel any pending runs and call runDelayed() with
+ // zero to apply the newly configured interval.
+ SessionSaverInternal.cancel();
+ SessionSaverInternal.runDelayed(0);
+ }, false);
+
+ return Services.prefs.getIntPref(PREF);
+});
+
+// Notify observers about a given topic with a given subject.
+function notify(subject, topic) {
+ Services.obs.notifyObservers(subject, topic, "");
+}
+
+// TelemetryStopwatch helper functions.
+function stopWatch(method) {
+ return function (...histograms) {
+ for (let hist of histograms) {
+ TelemetryStopwatch[method]("FX_SESSION_RESTORE_" + hist);
+ }
+ };
+}
+
+var stopWatchStart = stopWatch("start");
+var stopWatchCancel = stopWatch("cancel");
+var stopWatchFinish = stopWatch("finish");
+
+/**
+ * The external API implemented by the SessionSaver module.
+ */
+this.SessionSaver = Object.freeze({
+ /**
+ * Immediately saves the current session to disk.
+ */
+ run: function () {
+ return SessionSaverInternal.run();
+ },
+
+ /**
+ * Saves the current session to disk delayed by a given amount of time. Should
+ * another delayed run be scheduled already, we will ignore the given delay
+ * and state saving may occur a little earlier.
+ */
+ runDelayed: function () {
+ SessionSaverInternal.runDelayed();
+ },
+
+ /**
+ * Sets the last save time to the current time. This will cause us to wait for
+ * at least the configured interval when runDelayed() is called next.
+ */
+ updateLastSaveTime: function () {
+ SessionSaverInternal.updateLastSaveTime();
+ },
+
+ /**
+ * Cancels all pending session saves.
+ */
+ cancel: function () {
+ SessionSaverInternal.cancel();
+ }
+});
+
+/**
+ * The internal API.
+ */
+var SessionSaverInternal = {
+ /**
+ * The timeout ID referencing an active timer for a delayed save. When no
+ * save is pending, this is null.
+ */
+ _timeoutID: null,
+
+ /**
+ * A timestamp that keeps track of when we saved the session last. We will
+ * this to determine the correct interval between delayed saves to not deceed
+ * the configured session write interval.
+ */
+ _lastSaveTime: 0,
+
+ /**
+ * Immediately saves the current session to disk.
+ */
+ run: function () {
+ return this._saveState(true /* force-update all windows */);
+ },
+
+ /**
+ * Saves the current session to disk delayed by a given amount of time. Should
+ * another delayed run be scheduled already, we will ignore the given delay
+ * and state saving may occur a little earlier.
+ *
+ * @param delay (optional)
+ * The minimum delay in milliseconds to wait for until we collect and
+ * save the current session.
+ */
+ runDelayed: function (delay = 2000) {
+ // Bail out if there's a pending run.
+ if (this._timeoutID) {
+ return;
+ }
+
+ // Interval until the next disk operation is allowed.
+ delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0);
+
+ // Schedule a state save.
+ this._timeoutID = setTimeout(() => this._saveStateAsync(), delay);
+ },
+
+ /**
+ * Sets the last save time to the current time. This will cause us to wait for
+ * at least the configured interval when runDelayed() is called next.
+ */
+ updateLastSaveTime: function () {
+ this._lastSaveTime = Date.now();
+ },
+
+ /**
+ * Cancels all pending session saves.
+ */
+ cancel: function () {
+ clearTimeout(this._timeoutID);
+ this._timeoutID = null;
+ },
+
+ /**
+ * Saves the current session state. Collects data and writes to disk.
+ *
+ * @param forceUpdateAllWindows (optional)
+ * Forces us to recollect data for all windows and will bypass and
+ * update the corresponding caches.
+ */
+ _saveState: function (forceUpdateAllWindows = false) {
+ // Cancel any pending timeouts.
+ this.cancel();
+
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ // Don't save (or even collect) anything in permanent private
+ // browsing mode
+
+ this.updateLastSaveTime();
+ return Promise.resolve();
+ }
+
+ stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
+ let state = SessionStore.getCurrentState(forceUpdateAllWindows);
+ PrivacyFilter.filterPrivateWindowsAndTabs(state);
+
+ // 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 (state.deferredInitialState) {
+ state.windows = state.deferredInitialState.windows || [];
+ delete state.deferredInitialState;
+ }
+
+ if (AppConstants.platform != "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 (state._closedWindows.length) {
+ let i = state._closedWindows.length - 1;
+
+ if (!state._closedWindows[i]._shouldRestore) {
+ // We only need to go until _shouldRestore
+ // is falsy since we're going in reverse.
+ break;
+ }
+
+ delete state._closedWindows[i]._shouldRestore;
+ state.windows.unshift(state._closedWindows.pop());
+ }
+ }
+
+ // Clear all cookies on clean shutdown according to user preferences
+ if (RunState.isClosing) {
+ let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
+ Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION;
+ let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");
+ let restart = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
+ // Don't clear cookies when restarting
+ if ((expireCookies || sanitizeCookies) && !restart) {
+ for (let window of state.windows) {
+ delete window.cookies;
+ }
+ }
+ }
+
+ stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
+ return this._writeState(state);
+ },
+
+ /**
+ * Saves the current session state. Collects data asynchronously and calls
+ * _saveState() to collect data again (with a cache hit rate of hopefully
+ * 100%) and write to disk afterwards.
+ */
+ _saveStateAsync: function () {
+ // Allow scheduling delayed saves again.
+ this._timeoutID = null;
+
+ // Write to disk.
+ this._saveState();
+ },
+
+ /**
+ * Write the given state object to disk.
+ */
+ _writeState: function (state) {
+ // We update the time stamp before writing so that we don't write again
+ // too soon, if saving is requested before the write completes. Without
+ // this update we may save repeatedly if actions cause a runDelayed
+ // before writing has completed. See Bug 902280
+ this.updateLastSaveTime();
+
+ // Write (atomically) to a session file, using a tmp file. Once the session
+ // file is successfully updated, save the time stamp of the last save and
+ // notify the observers.
+ return SessionFile.write(state).then(() => {
+ this.updateLastSaveTime();
+ notify(null, "sessionstore-state-write-complete");
+ }, console.error);
+ },
+};
diff --git a/browser/components/sessionstore/SessionStorage.jsm b/browser/components/sessionstore/SessionStorage.jsm
new file mode 100644
index 000000000..705139ebf
--- /dev/null
+++ b/browser/components/sessionstore/SessionStorage.jsm
@@ -0,0 +1,173 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SessionStorage"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+// Returns the principal for a given |frame| contained in a given |docShell|.
+function getPrincipalForFrame(docShell, frame) {
+ let ssm = Services.scriptSecurityManager;
+ let uri = frame.document.documentURIObject;
+ return ssm.getDocShellCodebasePrincipal(uri, docShell);
+}
+
+this.SessionStorage = Object.freeze({
+ /**
+ * Updates all sessionStorage "super cookies"
+ * @param docShell
+ * That tab's docshell (containing the sessionStorage)
+ * @param frameTree
+ * The docShell's FrameTree instance.
+ * @return Returns a nested object that will have hosts as keys and per-host
+ * session storage data as strings. For example:
+ * {"example.com": {"key": "value", "my_number": "123"}}
+ */
+ collect: function (docShell, frameTree) {
+ return SessionStorageInternal.collect(docShell, frameTree);
+ },
+
+ /**
+ * Restores all sessionStorage "super cookies".
+ * @param aDocShell
+ * A tab's docshell (containing the sessionStorage)
+ * @param aStorageData
+ * A nested object with storage data to be restored that has hosts as
+ * keys and per-host session storage data as strings. For example:
+ * {"example.com": {"key": "value", "my_number": "123"}}
+ */
+ restore: function (aDocShell, aStorageData) {
+ SessionStorageInternal.restore(aDocShell, aStorageData);
+ },
+});
+
+var SessionStorageInternal = {
+ /**
+ * Reads all session storage data from the given docShell.
+ * @param docShell
+ * A tab's docshell (containing the sessionStorage)
+ * @param frameTree
+ * The docShell's FrameTree instance.
+ * @return Returns a nested object that will have hosts as keys and per-host
+ * session storage data as strings. For example:
+ * {"example.com": {"key": "value", "my_number": "123"}}
+ */
+ collect: function (docShell, frameTree) {
+ let data = {};
+ let visitedOrigins = new Set();
+
+ frameTree.forEach(frame => {
+ let principal = getPrincipalForFrame(docShell, frame);
+ if (!principal) {
+ return;
+ }
+
+ // Get the origin of the current history entry
+ // and use that as a key for the per-principal storage data.
+ let origin = principal.origin;
+ if (visitedOrigins.has(origin)) {
+ // Don't read a host twice.
+ return;
+ }
+
+ // Mark the current origin as visited.
+ visitedOrigins.add(origin);
+
+ let originData = this._readEntry(principal, docShell);
+ if (Object.keys(originData).length) {
+ data[origin] = originData;
+ }
+ });
+
+ return Object.keys(data).length ? data : null;
+ },
+
+ /**
+ * Writes session storage data to the given tab.
+ * @param aDocShell
+ * A tab's docshell (containing the sessionStorage)
+ * @param aStorageData
+ * A nested object with storage data to be restored that has hosts as
+ * keys and per-host session storage data as strings. For example:
+ * {"example.com": {"key": "value", "my_number": "123"}}
+ */
+ restore: function (aDocShell, aStorageData) {
+ for (let origin of Object.keys(aStorageData)) {
+ let data = aStorageData[origin];
+
+ let principal;
+
+ try {
+ let attrs = aDocShell.getOriginAttributes();
+ let originURI = Services.io.newURI(origin, null, null);
+ principal = Services.scriptSecurityManager.createCodebasePrincipal(originURI, attrs);
+ } catch (e) {
+ console.error(e);
+ continue;
+ }
+
+ let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
+ let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+
+ // There is no need to pass documentURI, it's only used to fill documentURI property of
+ // domstorage event, which in this case has no consumer. Prevention of events in case
+ // of missing documentURI will be solved in a followup bug to bug 600307.
+ let storage = storageManager.createStorage(window, principal, "", aDocShell.usePrivateBrowsing);
+
+ for (let key of Object.keys(data)) {
+ try {
+ storage.setItem(key, data[key]);
+ } catch (e) {
+ // throws e.g. for URIs that can't have sessionStorage
+ console.error(e);
+ }
+ }
+ }
+ },
+
+ /**
+ * Reads an entry in the session storage data contained in a tab's history.
+ * @param aURI
+ * That history entry uri
+ * @param aDocShell
+ * A tab's docshell (containing the sessionStorage)
+ */
+ _readEntry: function (aPrincipal, aDocShell) {
+ let hostData = {};
+ let storage;
+
+ let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+
+ try {
+ let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
+ storage = storageManager.getStorage(window, aPrincipal);
+ storage.length; // XXX: Bug 1232955 - storage.length can throw, catch that failure
+ } catch (e) {
+ // sessionStorage might throw if it's turned off, see bug 458954
+ storage = null;
+ }
+
+ if (storage && storage.length) {
+ for (let i = 0; i < storage.length; i++) {
+ try {
+ let key = storage.key(i);
+ hostData[key] = storage.getItem(key);
+ } catch (e) {
+ // This currently throws for secured items (cf. bug 442048).
+ }
+ }
+ }
+
+ return hostData;
+ }
+};
diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm
new file mode 100644
index 000000000..2f44b2af3
--- /dev/null
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -0,0 +1,4719 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SessionStore"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+// Current version of the format used by Session Restore.
+const FORMAT_VERSION = 1;
+
+const TAB_STATE_NEEDS_RESTORE = 1;
+const TAB_STATE_RESTORING = 2;
+const TAB_STATE_WILL_RESTORE = 3;
+
+// A new window has just been restored. At this stage, tabs are generally
+// not restored.
+const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
+const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
+const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
+const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
+const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
+const NOTIFY_INITIATING_MANUAL_RESTORE = "sessionstore-initiating-manual-restore";
+
+const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
+
+// Maximum number of tabs to restore simultaneously. Previously controlled by
+// the browser.sessionstore.max_concurrent_tabs pref.
+const MAX_CONCURRENT_TAB_RESTORES = 3;
+
+// Amount (in CSS px) by which we allow window edges to be off-screen
+// when restoring a window, before we override the saved position to
+// pull the window back within the available screen area.
+const SCREEN_EDGE_SLOP = 8;
+
+// global notifications observed
+const OBSERVING = [
+ "browser-window-before-show", "domwindowclosed",
+ "quit-application-granted", "browser-lastwindow-close-granted",
+ "quit-application", "browser:purge-session-history",
+ "browser:purge-domain-data",
+ "idle-daily",
+];
+
+// 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"
+];
+
+// Messages that will be received via the Frame Message Manager.
+const MESSAGES = [
+ // The content script sends us data that has been invalidated and needs to
+ // be saved to disk.
+ "SessionStore:update",
+
+ // The restoreHistory code has run. This is a good time to run SSTabRestoring.
+ "SessionStore:restoreHistoryComplete",
+
+ // The load for the restoring tab has begun. We update the URL bar at this
+ // time; if we did it before, the load would overwrite it.
+ "SessionStore:restoreTabContentStarted",
+
+ // All network loads for a restoring tab are done, so we should
+ // consider restoring another tab in the queue. The document has
+ // been restored, and forms have been filled. We trigger
+ // SSTabRestored at this time.
+ "SessionStore:restoreTabContentComplete",
+
+ // A crashed tab was revived by navigating to a different page. Remove its
+ // browser from the list of crashed browsers to stop ignoring its messages.
+ "SessionStore:crashedTabRevived",
+
+ // The content script encountered an error.
+ "SessionStore:error",
+];
+
+// The list of messages we accept from <xul:browser>s that have no tab
+// assigned, or whose windows have gone away. Those are for example the
+// ones that preload about:newtab pages, or from browsers where the window
+// has just been closed.
+const NOTAB_MESSAGES = new Set([
+ // For a description see above.
+ "SessionStore:crashedTabRevived",
+
+ // For a description see above.
+ "SessionStore:update",
+
+ // For a description see above.
+ "SessionStore:error",
+]);
+
+// The list of messages we accept without an "epoch" parameter.
+// See getCurrentEpoch() and friends to find out what an "epoch" is.
+const NOEPOCH_MESSAGES = new Set([
+ // For a description see above.
+ "SessionStore:crashedTabRevived",
+
+ // For a description see above.
+ "SessionStore:error",
+]);
+
+// The list of messages we want to receive even during the short period after a
+// frame has been removed from the DOM and before its frame script has finished
+// unloading.
+const CLOSED_MESSAGES = new Set([
+ // For a description see above.
+ "SessionStore:crashedTabRevived",
+
+ // For a description see above.
+ "SessionStore:update",
+
+ // For a description see above.
+ "SessionStore:error",
+]);
+
+// These are tab events that we listen to.
+const TAB_EVENTS = [
+ "TabOpen", "TabBrowserInserted", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
+ "TabUnpinned"
+];
+
+const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
+Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/debug.js", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
+ "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
+XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
+ "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
+XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
+ "@mozilla.org/base/telemetry;1", "nsITelemetry");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+ "resource:///modules/RecentWindow.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "GlobalState",
+ "resource:///modules/sessionstore/GlobalState.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
+ "resource:///modules/sessionstore/PrivacyFilter.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RunState",
+ "resource:///modules/sessionstore/RunState.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
+ "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
+ "resource:///modules/sessionstore/SessionSaver.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies",
+ "resource:///modules/sessionstore/SessionCookies.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
+ "resource:///modules/sessionstore/SessionFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
+ "resource:///modules/sessionstore/TabAttributes.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
+ "resource:///modules/ContentCrashHandlers.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabState",
+ "resource:///modules/sessionstore/TabState.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
+ "resource:///modules/sessionstore/TabStateCache.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabStateFlusher",
+ "resource:///modules/sessionstore/TabStateFlusher.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+ "resource://gre/modules/sessionstore/Utils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ViewSourceBrowser",
+ "resource://gre/modules/ViewSourceBrowser.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+
+/**
+ * |true| if we are in debug mode, |false| otherwise.
+ * Debug mode is controlled by preference browser.sessionstore.debug
+ */
+var gDebuggingEnabled = false;
+function debug(aMsg) {
+ if (gDebuggingEnabled) {
+ aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
+ Services.console.logStringMessage(aMsg);
+ }
+}
+
+this.SessionStore = {
+ get promiseInitialized() {
+ return SessionStoreInternal.promiseInitialized;
+ },
+
+ get canRestoreLastSession() {
+ return SessionStoreInternal.canRestoreLastSession;
+ },
+
+ set canRestoreLastSession(val) {
+ SessionStoreInternal.canRestoreLastSession = val;
+ },
+
+ get lastClosedObjectType() {
+ return SessionStoreInternal.lastClosedObjectType;
+ },
+
+ init: function ss_init() {
+ SessionStoreInternal.init();
+ },
+
+ getBrowserState: function ss_getBrowserState() {
+ return SessionStoreInternal.getBrowserState();
+ },
+
+ setBrowserState: function ss_setBrowserState(aState) {
+ SessionStoreInternal.setBrowserState(aState);
+ },
+
+ getWindowState: function ss_getWindowState(aWindow) {
+ return SessionStoreInternal.getWindowState(aWindow);
+ },
+
+ setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) {
+ SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
+ },
+
+ getTabState: function ss_getTabState(aTab) {
+ return SessionStoreInternal.getTabState(aTab);
+ },
+
+ setTabState: function ss_setTabState(aTab, aState) {
+ SessionStoreInternal.setTabState(aTab, aState);
+ },
+
+ duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) {
+ return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
+ },
+
+ getClosedTabCount: function ss_getClosedTabCount(aWindow) {
+ return SessionStoreInternal.getClosedTabCount(aWindow);
+ },
+
+ getClosedTabData: function ss_getClosedTabData(aWindow, aAsString = true) {
+ return SessionStoreInternal.getClosedTabData(aWindow, aAsString);
+ },
+
+ undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
+ return SessionStoreInternal.undoCloseTab(aWindow, aIndex);
+ },
+
+ forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
+ return SessionStoreInternal.forgetClosedTab(aWindow, aIndex);
+ },
+
+ getClosedWindowCount: function ss_getClosedWindowCount() {
+ return SessionStoreInternal.getClosedWindowCount();
+ },
+
+ getClosedWindowData: function ss_getClosedWindowData(aAsString = true) {
+ return SessionStoreInternal.getClosedWindowData(aAsString);
+ },
+
+ undoCloseWindow: function ss_undoCloseWindow(aIndex) {
+ return SessionStoreInternal.undoCloseWindow(aIndex);
+ },
+
+ forgetClosedWindow: function ss_forgetClosedWindow(aIndex) {
+ return SessionStoreInternal.forgetClosedWindow(aIndex);
+ },
+
+ getWindowValue: function ss_getWindowValue(aWindow, aKey) {
+ return SessionStoreInternal.getWindowValue(aWindow, aKey);
+ },
+
+ setWindowValue: function ss_setWindowValue(aWindow, aKey, aStringValue) {
+ SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue);
+ },
+
+ deleteWindowValue: function ss_deleteWindowValue(aWindow, aKey) {
+ SessionStoreInternal.deleteWindowValue(aWindow, aKey);
+ },
+
+ getTabValue: function ss_getTabValue(aTab, aKey) {
+ return SessionStoreInternal.getTabValue(aTab, aKey);
+ },
+
+ setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
+ SessionStoreInternal.setTabValue(aTab, aKey, aStringValue);
+ },
+
+ deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
+ SessionStoreInternal.deleteTabValue(aTab, aKey);
+ },
+
+ getGlobalValue: function ss_getGlobalValue(aKey) {
+ return SessionStoreInternal.getGlobalValue(aKey);
+ },
+
+ setGlobalValue: function ss_setGlobalValue(aKey, aStringValue) {
+ SessionStoreInternal.setGlobalValue(aKey, aStringValue);
+ },
+
+ deleteGlobalValue: function ss_deleteGlobalValue(aKey) {
+ SessionStoreInternal.deleteGlobalValue(aKey);
+ },
+
+ persistTabAttribute: function ss_persistTabAttribute(aName) {
+ SessionStoreInternal.persistTabAttribute(aName);
+ },
+
+ restoreLastSession: function ss_restoreLastSession() {
+ SessionStoreInternal.restoreLastSession();
+ },
+
+ getCurrentState: function (aUpdateAll) {
+ return SessionStoreInternal.getCurrentState(aUpdateAll);
+ },
+
+ reviveCrashedTab(aTab) {
+ return SessionStoreInternal.reviveCrashedTab(aTab);
+ },
+
+ reviveAllCrashedTabs() {
+ return SessionStoreInternal.reviveAllCrashedTabs();
+ },
+
+ navigateAndRestore(tab, loadArguments, historyIndex) {
+ return SessionStoreInternal.navigateAndRestore(tab, loadArguments, historyIndex);
+ },
+
+ getSessionHistory(tab, updatedCallback) {
+ return SessionStoreInternal.getSessionHistory(tab, updatedCallback);
+ },
+
+ undoCloseById(aClosedId) {
+ return SessionStoreInternal.undoCloseById(aClosedId);
+ },
+
+ /**
+ * Determines whether the passed version number is compatible with
+ * the current version number of the SessionStore.
+ *
+ * @param version The format and version of the file, as an array, e.g.
+ * ["sessionrestore", 1]
+ */
+ isFormatVersionCompatible(version) {
+ if (!version) {
+ return false;
+ }
+ if (!Array.isArray(version)) {
+ // Improper format.
+ return false;
+ }
+ if (version[0] != "sessionrestore") {
+ // Not a Session Restore file.
+ return false;
+ }
+ let number = Number.parseFloat(version[1]);
+ if (Number.isNaN(number)) {
+ return false;
+ }
+ return number <= FORMAT_VERSION;
+ },
+};
+
+// 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
+ ]),
+
+ _globalState: new GlobalState(),
+
+ // A counter to be used to generate a unique ID for each closed tab or window.
+ _nextClosedId: 0,
+
+ // During the initial restore and setBrowserState calls tracks the number of
+ // windows yet to be restored
+ _restoreCount: -1,
+
+ // For each <browser> element, records the current epoch.
+ _browserEpochs: new WeakMap(),
+
+ // Any browsers that fires the oop-browser-crashed event gets stored in
+ // here - that way we know which browsers to ignore messages from (until
+ // they get restored).
+ _crashedBrowsers: new WeakSet(),
+
+ // A map (xul:browser -> nsIFrameLoader) that maps a browser to the last
+ // associated frameLoader we heard about.
+ _lastKnownFrameLoader: new WeakMap(),
+
+ // A map (xul:browser -> object) that maps a browser associated with a
+ // recently closed tab to all its necessary state information we need to
+ // properly handle final update message.
+ _closedTabs: new WeakMap(),
+
+ // A map (xul:browser -> object) that maps a browser associated with a
+ // recently closed tab due to a window closure to the tab state information
+ // that is being stored in _closedWindows for that tab.
+ _closedWindowTabs: new WeakMap(),
+
+ // A set of window data that has the potential to be saved in the _closedWindows
+ // array for the session. We will remove window data from this set whenever
+ // forgetClosedWindow is called for the window, or when session history is
+ // purged, so that we don't accidentally save that data after the flush has
+ // completed. Closed tabs use a more complicated mechanism for this particular
+ // problem. When forgetClosedTab is called, the browser is removed from the
+ // _closedTabs map, so its data is not recorded. In the purge history case,
+ // the closedTabs array per window is overwritten so that once the flush is
+ // complete, the tab would only ever add itself to an array that SessionStore
+ // no longer cares about. Bug 1230636 has been filed to make the tab case
+ // work more like the window case, which is more explicit, and easier to
+ // reason about.
+ _saveableClosedWindowData: new WeakSet(),
+
+ // A map (xul:browser -> object) that maps a browser that is switching
+ // remoteness via navigateAndRestore, to the loadArguments that were
+ // most recently passed when calling navigateAndRestore.
+ _remotenessChangingBrowsers: new WeakMap(),
+
+ // whether a setBrowserState call is in progress
+ _browserSetState: false,
+
+ // 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: {},
+
+ // counter for creating unique window IDs
+ _nextWindowID: 0,
+
+ // states for all recently closed windows
+ _closedWindows: [],
+
+ // 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,
+
+ // 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
+ _deferredInitialized: (function () {
+ let deferred = {};
+
+ deferred.promise = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+
+ return deferred;
+ })(),
+
+ // Whether session has been initialized
+ _sessionInitialized: false,
+
+ // Promise that is resolved when we're ready to initialize
+ // and restore the session.
+ _promiseReadyForInitialization: null,
+
+ // Keep busy state counters per window.
+ _windowBusyStates: new WeakMap(),
+
+ /**
+ * A promise fulfilled once initialization is complete.
+ */
+ get promiseInitialized() {
+ return this._deferredInitialized.promise;
+ },
+
+ get canRestoreLastSession() {
+ return LastSession.canRestore;
+ },
+
+ set canRestoreLastSession(val) {
+ // Cheat a bit; only allow false.
+ if (!val) {
+ LastSession.clear();
+ }
+ },
+
+ /**
+ * Returns a string describing the last closed object, either "tab" or "window".
+ *
+ * This was added to support the sessions.restore WebExtensions API.
+ */
+ get lastClosedObjectType() {
+ if (this._closedWindows.length) {
+ // Since there are closed windows, we need to check if there's a closed tab
+ // in one of the currently open windows that was closed after the
+ // last-closed window.
+ let tabTimestamps = [];
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ let windowState = this._windows[window.__SSi];
+ if (windowState && windowState._closedTabs[0]) {
+ tabTimestamps.push(windowState._closedTabs[0].closedAt);
+ }
+ }
+ if (!tabTimestamps.length ||
+ (tabTimestamps.sort((a, b) => b - a)[0] < this._closedWindows[0].closedAt)) {
+ return "window";
+ }
+ }
+ return "tab";
+ },
+
+ /**
+ * Initialize the sessionstore service.
+ */
+ init: function () {
+ if (this._initialized) {
+ throw new Error("SessionStore.init() must only be called once!");
+ }
+
+ TelemetryTimestamps.add("sessionRestoreInitialized");
+ OBSERVING.forEach(function(aTopic) {
+ Services.obs.addObserver(this, aTopic, true);
+ }, this);
+
+ this._initPrefs();
+ this._initialized = true;
+ },
+
+ /**
+ * Initialize the session using the state provided by SessionStartup
+ */
+ initSession: function () {
+ TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
+ let state;
+ let ss = gSessionStartup;
+
+ if (ss.doRestore() ||
+ ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
+ state = ss.state;
+ }
+
+ if (state) {
+ 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(state);
+ // If we have a iniState with windows, that means that we have windows
+ // with app tabs to restore.
+ if (iniState.windows.length)
+ state = iniState;
+ else
+ state = null;
+
+ if (remainingState.windows.length) {
+ LastSession.setState(remainingState);
+ }
+ }
+ else {
+ // Get the last deferred session in case the user still wants to
+ // restore it
+ LastSession.setState(state.lastSessionState);
+
+ if (ss.previousSessionCrashed) {
+ this._recentCrashes = (state.session &&
+ state.session.recentCrashes || 0) + 1;
+
+ if (this._needsRestorePage(state, this._recentCrashes)) {
+ // replace the crashed session with a restore-page-only session
+ let url = "about:sessionrestore";
+ let formdata = {id: {sessionData: state}, url};
+ state = { windows: [{ tabs: [{ entries: [{url}], formdata }] }] };
+ } else if (this._hasSingleTabWithURL(state.windows,
+ "about:welcomeback")) {
+ // On a single about:welcomeback URL that crashed, replace about:welcomeback
+ // with about:sessionrestore, to make clear to the user that we crashed.
+ state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
+ }
+ }
+
+ // Update the session start time using the restored session state.
+ this._updateSessionStartTime(state);
+
+ // make sure that at least the first window doesn't have anything hidden
+ delete state.windows[0].hidden;
+ // Since nothing is hidden in the first window, it cannot be a popup
+ delete state.windows[0].isPopup;
+ // We don't want to minimize and then open a window at startup.
+ if (state.windows[0].sizemode == "minimized")
+ state.windows[0].sizemode = "normal";
+ // clear any lastSessionWindowID attributes since those don't matter
+ // during normal restore
+ state.windows.forEach(function(aWindow) {
+ delete aWindow.__lastSessionWindowID;
+ });
+ }
+ }
+ catch (ex) { debug("The session file is invalid: " + ex); }
+ }
+
+ // 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 (!RunState.isQuitting &&
+ this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
+ this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
+
+ TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
+ return state;
+ },
+
+ _initPrefs : function() {
+ this._prefBranch = Services.prefs.getBranch("browser.");
+
+ gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
+
+ Services.prefs.addObserver("browser.sessionstore.debug", () => {
+ gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
+ }, false);
+
+ 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);
+ },
+
+ /**
+ * Called on application shutdown, after notifications:
+ * quit-application-granted, quit-application
+ */
+ _uninit: function ssi_uninit() {
+ if (!this._initialized) {
+ throw new Error("SessionStore is not initialized.");
+ }
+
+ // Prepare to close the session file and write the last state.
+ RunState.setClosing();
+
+ // save all data for session resuming
+ if (this._sessionInitialized) {
+ SessionSaver.run();
+ }
+
+ // clear out priority queue in case it's still holding refs
+ TabRestoreQueue.reset();
+
+ // Make sure to cancel pending saves.
+ SessionSaver.cancel();
+ },
+
+ /**
+ * Handle notifications
+ */
+ observe: function ssi_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "browser-window-before-show": // catch new windows
+ this.onBeforeBrowserWindowShown(aSubject);
+ break;
+ case "domwindowclosed": // catch closed windows
+ this.onClose(aSubject);
+ break;
+ case "quit-application-granted":
+ let syncShutdown = aData == "syncShutdown";
+ this.onQuitApplicationGranted(syncShutdown);
+ 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 "idle-daily":
+ this.onIdleDaily();
+ break;
+ }
+ },
+
+ /**
+ * This method handles incoming messages sent by the session store content
+ * script via the Frame Message Manager or Parent Process Message Manager,
+ * and thus enables communication with OOP tabs.
+ */
+ receiveMessage(aMessage) {
+ // If we got here, that means we're dealing with a frame message
+ // manager message, so the target will be a <xul:browser>.
+ var browser = aMessage.target;
+ let win = browser.ownerGlobal;
+ let tab = win ? win.gBrowser.getTabForBrowser(browser) : null;
+
+ // Ensure we receive only specific messages from <xul:browser>s that
+ // have no tab or window assigned, e.g. the ones that preload
+ // about:newtab pages, or windows that have closed.
+ if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
+ throw new Error(`received unexpected message '${aMessage.name}' ` +
+ `from a browser that has no tab or window`);
+ }
+
+ let data = aMessage.data || {};
+ let hasEpoch = data.hasOwnProperty("epoch");
+
+ // Most messages sent by frame scripts require to pass an epoch.
+ if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) {
+ throw new Error(`received message '${aMessage.name}' without an epoch`);
+ }
+
+ // Ignore messages from previous epochs.
+ if (hasEpoch && !this.isCurrentEpoch(browser, data.epoch)) {
+ return;
+ }
+
+ switch (aMessage.name) {
+ case "SessionStore:update":
+ // |browser.frameLoader| might be empty if the browser was already
+ // destroyed and its tab removed. In that case we still have the last
+ // frameLoader we know about to compare.
+ let frameLoader = browser.frameLoader ||
+ this._lastKnownFrameLoader.get(browser.permanentKey);
+
+ // If the message isn't targeting the latest frameLoader discard it.
+ if (frameLoader != aMessage.targetFrameLoader) {
+ return;
+ }
+
+ if (aMessage.data.isFinal) {
+ // If this the final message we need to resolve all pending flush
+ // requests for the given browser as they might have been sent too
+ // late and will never respond. If they have been sent shortly after
+ // switching a browser's remoteness there isn't too much data to skip.
+ TabStateFlusher.resolveAll(browser);
+ } else if (aMessage.data.flushID) {
+ // This is an update kicked off by an async flush request. Notify the
+ // TabStateFlusher so that it can finish the request and notify its
+ // consumer that's waiting for the flush to be done.
+ TabStateFlusher.resolve(browser, aMessage.data.flushID);
+ }
+
+ // Ignore messages from <browser> elements that have crashed
+ // and not yet been revived.
+ if (this._crashedBrowsers.has(browser.permanentKey)) {
+ return;
+ }
+
+ // Record telemetry measurements done in the child and update the tab's
+ // cached state. Mark the window as dirty and trigger a delayed write.
+ this.recordTelemetry(aMessage.data.telemetry);
+ TabState.update(browser, aMessage.data);
+ this.saveStateDelayed(win);
+
+ // Handle any updates sent by the child after the tab was closed. This
+ // might be the final update as sent by the "unload" handler but also
+ // any async update message that was sent before the child unloaded.
+ if (this._closedTabs.has(browser.permanentKey)) {
+ let {closedTabs, tabData} = this._closedTabs.get(browser.permanentKey);
+
+ // Update the closed tab's state. This will be reflected in its
+ // window's list of closed tabs as that refers to the same object.
+ TabState.copyFromCache(browser, tabData.state);
+
+ // Is this the tab's final message?
+ if (aMessage.data.isFinal) {
+ // We expect no further updates.
+ this._closedTabs.delete(browser.permanentKey);
+ // The tab state no longer needs this reference.
+ delete tabData.permanentKey;
+
+ // Determine whether the tab state is worth saving.
+ let shouldSave = this._shouldSaveTabState(tabData.state);
+ let index = closedTabs.indexOf(tabData);
+
+ if (shouldSave && index == -1) {
+ // If the tab state is worth saving and we didn't push it onto
+ // the list of closed tabs when it was closed (because we deemed
+ // the state not worth saving) then add it to the window's list
+ // of closed tabs now.
+ this.saveClosedTabData(closedTabs, tabData);
+ } else if (!shouldSave && index > -1) {
+ // Remove from the list of closed tabs. The update messages sent
+ // after the tab was closed changed enough state so that we no
+ // longer consider its data interesting enough to keep around.
+ this.removeClosedTabData(closedTabs, index);
+ }
+ }
+ }
+ break;
+ case "SessionStore:restoreHistoryComplete":
+ // Notify the tabbrowser that the tab chrome has been restored.
+ let tabData = TabState.collect(tab);
+
+ // wall-paper fix for bug 439675: make sure that the URL to be loaded
+ // is always visible in the address bar if no other value is present
+ let activePageData = tabData.entries[tabData.index - 1] || null;
+ let uri = activePageData ? activePageData.url || null : null;
+ // NB: we won't set initial URIs (about:home, about:newtab, etc.) here
+ // because their load will not normally trigger a location bar clearing
+ // when they finish loading (to avoid race conditions where we then
+ // clear user input instead), so we shouldn't set them here either.
+ // They also don't fall under the issues in bug 439675 where user input
+ // needs to be preserved if the load doesn't succeed.
+ // We also don't do this for remoteness updates, where it should not
+ // be necessary.
+ if (!browser.userTypedValue && uri && !data.isRemotenessUpdate &&
+ !win.gInitialPages.includes(uri)) {
+ browser.userTypedValue = 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";
+ }
+ } else if (tab.hasAttribute("customizemode")) {
+ win.gCustomizeMode.setTab(tab);
+ }
+
+ // Restore the tab icon.
+ if ("image" in tabData) {
+ // Use the serialized contentPrincipal with the new icon load.
+ let loadingPrincipal = Utils.deserializePrincipal(tabData.iconLoadingPrincipal);
+ win.gBrowser.setIcon(tab, tabData.image, loadingPrincipal);
+ TabStateCache.update(browser, { image: null, iconLoadingPrincipal: null });
+ }
+
+ let event = win.document.createEvent("Events");
+ event.initEvent("SSTabRestoring", true, false);
+ tab.dispatchEvent(event);
+ break;
+ case "SessionStore:restoreTabContentStarted":
+ if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
+ // If a load not initiated by sessionstore was started in a
+ // previously pending tab. Mark the tab as no longer pending.
+ this.markTabAsRestoring(tab);
+ } else if (!data.isRemotenessUpdate) {
+ // If the user was typing into the URL bar when we crashed, but hadn't hit
+ // enter yet, then we just need to write that value to the URL bar without
+ // loading anything. This must happen after the load, as the load will clear
+ // userTypedValue.
+ let tabData = TabState.collect(tab);
+ if (tabData.userTypedValue && !tabData.userTypedClear && !browser.userTypedValue) {
+ browser.userTypedValue = tabData.userTypedValue;
+ win.URLBarSetURI();
+ }
+
+ // Remove state we don't need any longer.
+ TabStateCache.update(browser, {
+ userTypedValue: null, userTypedClear: null
+ });
+ }
+ break;
+ case "SessionStore:restoreTabContentComplete":
+ // This callback is used exclusively by tests that want to
+ // monitor the progress of network loads.
+ if (gDebuggingEnabled) {
+ Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null);
+ }
+
+ SessionStoreInternal._resetLocalTabRestoringState(tab);
+ SessionStoreInternal.restoreNextTab();
+
+ this._sendTabRestoredNotification(tab, data.isRemotenessUpdate);
+ break;
+ case "SessionStore:crashedTabRevived":
+ // The browser was revived by navigating to a different page
+ // manually, so we remove it from the ignored browser set.
+ this._crashedBrowsers.delete(browser.permanentKey);
+ break;
+ case "SessionStore:error":
+ this.reportInternalError(data);
+ TabStateFlusher.resolveAll(browser, false, "Received error from the content process");
+ break;
+ default:
+ throw new Error(`received unknown message '${aMessage.name}'`);
+ break;
+ }
+ },
+
+ /**
+ * Record telemetry measurements stored in an object.
+ * @param telemetry
+ * {histogramID: value, ...} An object mapping histogramIDs to the
+ * value to be recorded for that ID,
+ */
+ recordTelemetry: function (telemetry) {
+ for (let histogramId in telemetry){
+ Telemetry.getHistogramById(histogramId).add(telemetry[histogramId]);
+ }
+ },
+
+ /* ........ Window Event Handlers .............. */
+
+ /**
+ * Implement nsIDOMEventListener for handling various window and tab events
+ */
+ handleEvent: function ssi_handleEvent(aEvent) {
+ let win = aEvent.currentTarget.ownerGlobal;
+ let target = aEvent.originalTarget;
+ switch (aEvent.type) {
+ case "TabOpen":
+ this.onTabAdd(win);
+ break;
+ case "TabBrowserInserted":
+ this.onTabBrowserInserted(win, target);
+ break;
+ case "TabClose":
+ // `adoptedBy` will be set if the tab was closed because it is being
+ // moved to a new window.
+ if (!aEvent.detail.adoptedBy)
+ this.onTabClose(win, target);
+ this.onTabRemove(win, target);
+ break;
+ case "TabSelect":
+ this.onTabSelect(win);
+ break;
+ case "TabShow":
+ this.onTabShow(win, target);
+ break;
+ case "TabHide":
+ this.onTabHide(win, target);
+ break;
+ case "TabPinned":
+ case "TabUnpinned":
+ case "SwapDocShells":
+ this.saveStateDelayed(win);
+ break;
+ case "oop-browser-crashed":
+ this.onBrowserCrashed(target);
+ break;
+ case "XULFrameLoaderCreated":
+ if (target.namespaceURI == NS_XUL &&
+ target.localName == "browser" &&
+ target.frameLoader &&
+ target.permanentKey) {
+ this._lastKnownFrameLoader.set(target.permanentKey, target.frameLoader);
+ this.resetEpoch(target);
+ }
+ break;
+ default:
+ throw new Error(`unhandled event ${aEvent.type}?`);
+ }
+ this._clearRestoringWindows();
+ },
+
+ /**
+ * Generate a unique window identifier
+ * @return string
+ * A unique string to identify a window
+ */
+ _generateWindowID: function ssi_generateWindowID() {
+ return "window" + (this._nextWindowID++);
+ },
+
+ /**
+ * Registers and tracks a given window.
+ *
+ * @param aWindow
+ * Window reference
+ */
+ onLoad(aWindow) {
+ // return if window has already been initialized
+ if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi])
+ return;
+
+ // ignore windows opened while shutting down
+ if (RunState.isQuitting)
+ return;
+
+ // Assign the window a unique identifier we can use to reference
+ // internal data about the window.
+ aWindow.__SSi = this._generateWindowID();
+
+ let mm = aWindow.getGroupMessageManager("browsers");
+ MESSAGES.forEach(msg => {
+ let listenWhenClosed = CLOSED_MESSAGES.has(msg);
+ mm.addMessageListener(msg, this, listenWhenClosed);
+ });
+
+ // Load the frame script after registering listeners.
+ mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
+
+ // and create its data object
+ this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
+
+ if (PrivateBrowsingUtils.isWindowPrivate(aWindow))
+ this._windows[aWindow.__SSi].isPrivate = true;
+ if (!this._isWindowLoaded(aWindow))
+ this._windows[aWindow.__SSi]._restoring = true;
+ if (!aWindow.toolbar.visible)
+ this._windows[aWindow.__SSi].isPopup = true;
+
+ let tabbrowser = aWindow.gBrowser;
+
+ // add tab change listeners to all already existing tabs
+ for (let i = 0; i < tabbrowser.tabs.length; i++) {
+ this.onTabBrowserInserted(aWindow, tabbrowser.tabs[i]);
+ }
+ // notification of tab add/remove/selection/show/hide
+ TAB_EVENTS.forEach(function(aEvent) {
+ tabbrowser.tabContainer.addEventListener(aEvent, this, true);
+ }, this);
+
+ // Keep track of a browser's latest frameLoader.
+ aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
+ },
+
+ /**
+ * Initializes a given window.
+ *
+ * Windows are registered as soon as they are created but we need to wait for
+ * the session file to load, and the initial window's delayed startup to
+ * finish before initializing a window, i.e. restoring data into it.
+ *
+ * @param aWindow
+ * Window reference
+ * @param aInitialState
+ * The initial state to be loaded after startup (optional)
+ */
+ initializeWindow(aWindow, aInitialState = null) {
+ let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
+
+ // perform additional initialization when the first window is loading
+ if (RunState.isStopped) {
+ RunState.setRunning();
+
+ // restore a crashed session resp. resume the last session if requested
+ if (aInitialState) {
+ // Don't write to disk right after startup. Set the last time we wrote
+ // to disk to NOW() to enforce a full interval before the next write.
+ SessionSaver.updateLastSaveTime();
+
+ 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;
+
+ // Nothing to restore now, notify observers things are complete.
+ Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
+ } else {
+ TelemetryTimestamps.add("sessionRestoreRestoring");
+ this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0;
+
+ // global data must be restored before restoreWindow is called so that
+ // it happens before observers are notified
+ this._globalState.setFromState(aInitialState);
+
+ let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
+ let options = {firstWindow: true, overwriteTabs: overwrite};
+ this.restoreWindows(aWindow, aInitialState, options);
+ }
+ }
+ else {
+ // Nothing to restore, notify observers things are complete.
+ Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
+ }
+ }
+ // this window was opened by _openWindowWithState
+ else if (!this._isWindowLoaded(aWindow)) {
+ let state = this._statesToRestore[aWindow.__SS_restoreID];
+ let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1};
+ this.restoreWindow(aWindow, state.windows[0], options);
+ }
+ // 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) {
+
+ // global data must be restored before restoreWindow is called so that
+ // it happens before observers are notified
+ this._globalState.setFromState(this._deferredInitialState);
+
+ this._restoreCount = this._deferredInitialState.windows ?
+ this._deferredInitialState.windows.length : 0;
+ this.restoreWindows(aWindow, this._deferredInitialState, {firstWindow: true});
+ 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;
+ if (AppConstants.platform == "macosx" || !this._doResumeSession()) {
+ // 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];
+ }
+ }
+ 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;
+ }
+
+ if (newWindowState) {
+ // Ensure that the window state isn't hidden
+ this._restoreCount = 1;
+ let state = { windows: [newWindowState] };
+ let options = {overwriteTabs: this._isCmdLineEmpty(aWindow, state)};
+ this.restoreWindow(aWindow, newWindowState, options);
+ }
+ }
+ // 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;
+ }
+ },
+
+ /**
+ * Called right before a new browser window is shown.
+ * @param aWindow
+ * Window reference
+ */
+ onBeforeBrowserWindowShown: function (aWindow) {
+ // Register the window.
+ this.onLoad(aWindow);
+
+ // Just call initializeWindow() directly if we're initialized already.
+ if (this._sessionInitialized) {
+ this.initializeWindow(aWindow);
+ return;
+ }
+
+ // The very first window that is opened creates a promise that is then
+ // re-used by all subsequent windows. The promise will be used to tell
+ // when we're ready for initialization.
+ if (!this._promiseReadyForInitialization) {
+ // Wait for the given window's delayed startup to be finished.
+ let promise = new Promise(resolve => {
+ Services.obs.addObserver(function obs(subject, topic) {
+ if (aWindow == subject) {
+ Services.obs.removeObserver(obs, topic);
+ resolve();
+ }
+ }, "browser-delayed-startup-finished", false);
+ });
+
+ // We are ready for initialization as soon as the session file has been
+ // read from disk and the initial window's delayed startup has finished.
+ this._promiseReadyForInitialization =
+ Promise.all([promise, gSessionStartup.onceInitialized]);
+ }
+
+ // We can't call this.onLoad since initialization
+ // hasn't completed, so we'll wait until it is done.
+ // Even if additional windows are opened and wait
+ // for initialization as well, the first opened
+ // window should execute first, and this.onLoad
+ // will be called with the initialState.
+ this._promiseReadyForInitialization.then(() => {
+ if (aWindow.closed) {
+ return;
+ }
+
+ if (this._sessionInitialized) {
+ this.initializeWindow(aWindow);
+ } else {
+ let initialState = this.initSession();
+ this._sessionInitialized = true;
+
+ if (initialState) {
+ Services.obs.notifyObservers(null, NOTIFY_RESTORING_ON_STARTUP, "");
+ }
+ TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS");
+ this.initializeWindow(aWindow, initialState);
+ TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS");
+
+ // Let everyone know we're done.
+ this._deferredInitialized.resolve();
+ }
+ }, console.error);
+ },
+
+ /**
+ * On window close...
+ * - remove event listeners from tabs
+ * - save all window data
+ * @param aWindow
+ * Window reference
+ */
+ onClose: function ssi_onClose(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 = this._generateWindowID();
+ }
+
+ 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;
+
+ let browsers = Array.from(tabbrowser.browsers);
+
+ TAB_EVENTS.forEach(function(aEvent) {
+ tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
+ }, this);
+
+ aWindow.gBrowser.removeEventListener("XULFrameLoaderCreated", this);
+
+ let winData = this._windows[aWindow.__SSi];
+
+ // Collect window data only when *not* closed during shutdown.
+ if (RunState.isRunning) {
+ // Grab the most recent window data. The tab data will be updated
+ // once we finish flushing all of the messages from the tabs.
+ let tabMap = this._collectWindowData(aWindow);
+
+ for (let [tab, tabData] of tabMap) {
+ let permanentKey = tab.linkedBrowser.permanentKey;
+ this._closedWindowTabs.set(permanentKey, tabData);
+ }
+
+ if (isFullyLoaded) {
+ winData.title = tabbrowser.selectedBrowser.contentTitle || tabbrowser.selectedTab.label;
+ winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
+ tabbrowser.selectedTab);
+ SessionCookies.update([winData]);
+ }
+
+ if (AppConstants.platform != "macosx") {
+ // Until we decide otherwise elsewhere, this window is part of a series
+ // of closing windows to quit.
+ winData._shouldRestore = true;
+ }
+
+ // Store the window's close date to figure out when each individual tab
+ // was closed. This timestamp should allow re-arranging data based on how
+ // recently something was closed.
+ winData.closedAt = Date.now();
+
+ // we don't want to save the busy state
+ delete winData.busy;
+
+ // When closing windows one after the other until Firefox quits, we
+ // will move those closed in series back to the "open windows" bucket
+ // before writing to disk. If however there is only a single window
+ // with tabs we deem not worth saving then we might end up with a
+ // random closed or even a pop-up window re-opened. To prevent that
+ // we explicitly allow saving an "empty" window state.
+ let isLastWindow =
+ Object.keys(this._windows).length == 1 &&
+ !this._closedWindows.some(win => win._shouldRestore || false);
+
+ // clear this window from the list, since it has definitely been closed.
+ delete this._windows[aWindow.__SSi];
+
+ // This window has the potential to be saved in the _closedWindows
+ // array (maybeSaveClosedWindows gets the final call on that).
+ this._saveableClosedWindowData.add(winData);
+
+ // Now we have to figure out if this window is worth saving in the _closedWindows
+ // Object.
+ //
+ // We're about to flush the tabs from this window, but it's possible that we
+ // might never hear back from the content process(es) in time before the user
+ // chooses to restore the closed window. So we do the following:
+ //
+ // 1) Use the tab state cache to determine synchronously if the window is
+ // worth stashing in _closedWindows.
+ // 2) Flush the window.
+ // 3) When the flush is complete, revisit our decision to store the window
+ // in _closedWindows, and add/remove as necessary.
+ if (!winData.isPrivate) {
+ // Remove any open private tabs the window may contain.
+ PrivacyFilter.filterPrivateTabs(winData);
+ this.maybeSaveClosedWindow(winData, isLastWindow);
+ }
+
+ TabStateFlusher.flushWindow(aWindow).then(() => {
+ // At this point, aWindow is closed! You should probably not try to
+ // access any DOM elements from aWindow within this callback unless
+ // you're holding on to them in the closure.
+
+ for (let browser of browsers) {
+ if (this._closedWindowTabs.has(browser.permanentKey)) {
+ let tabData = this._closedWindowTabs.get(browser.permanentKey);
+ TabState.copyFromCache(browser, tabData);
+ this._closedWindowTabs.delete(browser.permanentKey);
+ }
+ }
+
+ // Save non-private windows if they have at
+ // least one saveable tab or are the last window.
+ if (!winData.isPrivate) {
+ // It's possible that a tab switched its privacy state at some point
+ // before our flush, so we need to filter again.
+ PrivacyFilter.filterPrivateTabs(winData);
+ this.maybeSaveClosedWindow(winData, isLastWindow);
+ }
+
+ // Update the tabs data now that we've got the most
+ // recent information.
+ this.cleanUpWindow(aWindow, winData, browsers);
+
+ // save the state without this window to disk
+ this.saveStateDelayed();
+ });
+ } else {
+ this.cleanUpWindow(aWindow, winData, browsers);
+ }
+
+ for (let i = 0; i < tabbrowser.tabs.length; i++) {
+ this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
+ }
+ },
+
+ /**
+ * Clean up the message listeners on a window that has finally
+ * gone away. Call this once you're sure you don't want to hear
+ * from any of this windows tabs from here forward.
+ *
+ * @param aWindow
+ * The browser window we're cleaning up.
+ * @param winData
+ * The data for the window that we should hold in the
+ * DyingWindowCache in case anybody is still holding a
+ * reference to it.
+ */
+ cleanUpWindow(aWindow, winData, browsers) {
+ // Any leftover TabStateFlusher Promises need to be resolved now,
+ // since we're about to remove the message listeners.
+ for (let browser of browsers) {
+ TabStateFlusher.resolveAll(browser);
+ }
+
+ // Cache the window state until it is completely gone.
+ DyingWindowCache.set(aWindow, winData);
+
+ let mm = aWindow.getGroupMessageManager("browsers");
+ MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
+
+ this._saveableClosedWindowData.delete(winData);
+ delete aWindow.__SSi;
+ },
+
+ /**
+ * Decides whether or not a closed window should be put into the
+ * _closedWindows Object. This might be called multiple times per
+ * window, and will do the right thing of moving the window data
+ * in or out of _closedWindows if the winData indicates that our
+ * need for saving it has changed.
+ *
+ * @param winData
+ * The data for the closed window that we might save.
+ * @param isLastWindow
+ * Whether or not the window being closed is the last
+ * browser window. Callers of this function should pass
+ * in the value of SessionStoreInternal.atLastWindow for
+ * this argument, and pass in the same value if they happen
+ * to call this method again asynchronously (for example, after
+ * a window flush).
+ */
+ maybeSaveClosedWindow(winData, isLastWindow) {
+ // Make sure SessionStore is still running, and make sure that we
+ // haven't chosen to forget this window.
+ if (RunState.isRunning && this._saveableClosedWindowData.has(winData)) {
+ // Determine whether the window has any tabs worth saving.
+ let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
+
+ // Note that we might already have this window stored in
+ // _closedWindows from a previous call to this function.
+ let winIndex = this._closedWindows.indexOf(winData);
+ let alreadyStored = (winIndex != -1);
+ let shouldStore = (hasSaveableTabs || isLastWindow);
+
+ if (shouldStore && !alreadyStored) {
+ let index = this._closedWindows.findIndex(win => {
+ return win.closedAt < winData.closedAt;
+ });
+
+ // If we found no tab closed before our
+ // tab then just append it to the list.
+ if (index == -1) {
+ index = this._closedWindows.length;
+ }
+
+ // About to save the closed window, add a unique ID.
+ winData.closedId = this._nextClosedId++;
+
+ // Insert tabData at the right position.
+ this._closedWindows.splice(index, 0, winData);
+ this._capClosedWindows();
+ } else if (!shouldStore && alreadyStored) {
+ this._closedWindows.splice(winIndex, 1);
+ }
+ }
+ },
+
+ /**
+ * On quit application granted
+ */
+ onQuitApplicationGranted: function ssi_onQuitApplicationGranted(syncShutdown=false) {
+ // Collect an initial snapshot of window data before we do the flush
+ this._forEachBrowserWindow((win) => {
+ this._collectWindowData(win);
+ });
+
+ // Now add an AsyncShutdown blocker that'll spin the event loop
+ // until the windows have all been flushed.
+
+ // This progress object will track the state of async window flushing
+ // and will help us debug things that go wrong with our AsyncShutdown
+ // blocker.
+ let progress = { total: -1, current: -1 };
+
+ // We're going down! Switch state so that we treat closing windows and
+ // tabs correctly.
+ RunState.setQuitting();
+
+ if (!syncShutdown) {
+ // We've got some time to shut down, so let's do this properly.
+ // To prevent blocker from breaking the 60 sec limit(which will cause a
+ // crash) of async shutdown during flushing all windows, we resolve the
+ // promise passed to blocker once:
+ // 1. the flushing exceed 50 sec, or
+ // 2. 'oop-frameloader-crashed' or 'ipc:content-shutdown' is observed.
+ // Thus, Firefox still can open the last session on next startup.
+ AsyncShutdown.quitApplicationGranted.addBlocker(
+ "SessionStore: flushing all windows",
+ () => {
+ var promises = [];
+ promises.push(this.flushAllWindowsAsync(progress));
+ promises.push(this.looseTimer(50000));
+
+ var promiseOFC = new Promise(resolve => {
+ Services.obs.addObserver(function obs(subject, topic) {
+ Services.obs.removeObserver(obs, topic);
+ resolve();
+ }, "oop-frameloader-crashed", false);
+ });
+ promises.push(promiseOFC);
+
+ var promiseICS = new Promise(resolve => {
+ Services.obs.addObserver(function obs(subject, topic) {
+ Services.obs.removeObserver(obs, topic);
+ resolve();
+ }, "ipc:content-shutdown", false);
+ });
+ promises.push(promiseICS);
+
+ return Promise.race(promises);
+ },
+ () => progress);
+ } else {
+ // We have to shut down NOW, which means we only get to save whatever
+ // we already had cached.
+ }
+ },
+
+ /**
+ * An async Task that iterates all open browser windows and flushes
+ * any outstanding messages from their tabs. This will also close
+ * all of the currently open windows while we wait for the flushes
+ * to complete.
+ *
+ * @param progress (Object)
+ * Optional progress object that will be updated as async
+ * window flushing progresses. flushAllWindowsSync will
+ * write to the following properties:
+ *
+ * total (int):
+ * The total number of windows to be flushed.
+ * current (int):
+ * The current window that we're waiting for a flush on.
+ *
+ * @return Promise
+ */
+ flushAllWindowsAsync: Task.async(function*(progress={}) {
+ let windowPromises = new Map();
+ // We collect flush promises and close each window immediately so that
+ // the user can't start changing any window state while we're waiting
+ // for the flushes to finish.
+ this._forEachBrowserWindow((win) => {
+ windowPromises.set(win, TabStateFlusher.flushWindow(win));
+
+ // We have to wait for these messages to come up from
+ // each window and each browser. In the meantime, hide
+ // the windows to improve perceived shutdown speed.
+ let baseWin = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .treeOwner
+ .QueryInterface(Ci.nsIBaseWindow);
+ baseWin.visibility = false;
+ });
+
+ progress.total = windowPromises.size;
+ progress.current = 0;
+
+ // We'll iterate through the Promise array, yielding each one, so as to
+ // provide useful progress information to AsyncShutdown.
+ for (let [win, promise] of windowPromises) {
+ yield promise;
+ this._collectWindowData(win);
+ progress.current++;
+ };
+
+ // 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 || "";
+ DirtyWindows.clear();
+ }),
+
+ /**
+ * On last browser window close
+ */
+ onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() {
+ // 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 ssi_onQuitApplication(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");
+ }
+
+ if (aData != "restart") {
+ // Throw away the previous session on shutdown
+ LastSession.clear();
+ }
+
+ this._uninit();
+ },
+
+ /**
+ * On purge of session history
+ */
+ onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
+ 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 (RunState.isQuitting)
+ return;
+ LastSession.clear();
+
+ let openWindows = {};
+ // Collect open windows.
+ this._forEachBrowserWindow(({__SSi: id}) => openWindows[id] = 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];
+ }
+ }
+ // 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(() => SessionSaver.run(), 0);
+ } else if (RunState.isRunning) {
+ SessionSaver.run();
+ }
+
+ this._clearRestoringWindows();
+ this._saveableClosedWindowData = new WeakSet();
+ },
+
+ /**
+ * On purge of domain data
+ * @param aData
+ * String domain data
+ */
+ onPurgeDomainData: function ssi_onPurgeDomainData(aData) {
+ // does a session history entry contain a url for the given domain?
+ function containsDomain(aEntry) {
+ if (Utils.hasRootDomain(aEntry.url, aData)) {
+ return true;
+ }
+ 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 (RunState.isRunning) {
+ SessionSaver.run();
+ }
+
+ this._clearRestoringWindows();
+ },
+
+ /**
+ * On preference change
+ * @param aData
+ * String preference changed
+ */
+ onPrefChange: function ssi_onPrefChange(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;
+ }
+ },
+
+ /**
+ * save state when new tab is added
+ * @param aWindow
+ * Window reference
+ */
+ onTabAdd: function ssi_onTabAdd(aWindow) {
+ this.saveStateDelayed(aWindow);
+ },
+
+ /**
+ * set up listeners for a new tab
+ * @param aWindow
+ * Window reference
+ * @param aTab
+ * Tab reference
+ */
+ onTabBrowserInserted: function ssi_onTabBrowserInserted(aWindow, aTab) {
+ let browser = aTab.linkedBrowser;
+ browser.addEventListener("SwapDocShells", this);
+ browser.addEventListener("oop-browser-crashed", this);
+
+ if (browser.frameLoader) {
+ this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
+ }
+ },
+
+ /**
+ * 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 ssi_onTabRemove(aWindow, aTab, aNoNotification) {
+ let browser = aTab.linkedBrowser;
+ browser.removeEventListener("SwapDocShells", this);
+ browser.removeEventListener("oop-browser-crashed", this);
+
+ // 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 ssi_onTabClose(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;
+ }
+
+ // Get the latest data for this tab (generally, from the cache)
+ let tabState = TabState.collect(aTab);
+
+ // Don't save private tabs
+ let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
+ if (!isPrivateWindow && tabState.isPrivate) {
+ return;
+ }
+
+ // Store closed-tab data for undo.
+ let tabbrowser = aWindow.gBrowser;
+ let tabTitle = this._replaceLoadingTitle(aTab.label, tabbrowser, aTab);
+ let {permanentKey} = aTab.linkedBrowser;
+
+ let tabData = {
+ permanentKey,
+ state: tabState,
+ title: tabTitle,
+ image: tabbrowser.getIcon(aTab),
+ iconLoadingPrincipal: Utils.serializePrincipal(aTab.linkedBrowser.contentPrincipal),
+ pos: aTab._tPos,
+ closedAt: Date.now()
+ };
+
+ let closedTabs = this._windows[aWindow.__SSi]._closedTabs;
+
+ // Determine whether the tab contains any information worth saving. Note
+ // that there might be pending state changes queued in the child that
+ // didn't reach the parent yet. If a tab is emptied before closing then we
+ // might still remove it from the list of closed tabs later.
+ if (this._shouldSaveTabState(tabState)) {
+ // Save the tab state, for now. We might push a valid tab out
+ // of the list but those cases should be extremely rare and
+ // do probably never occur when using the browser normally.
+ // (Tests or add-ons might do weird things though.)
+ this.saveClosedTabData(closedTabs, tabData);
+ }
+
+ // Remember the closed tab to properly handle any last updates included in
+ // the final "update" message sent by the frame script's unload handler.
+ this._closedTabs.set(permanentKey, {closedTabs, tabData});
+ },
+
+ /**
+ * Insert a given |tabData| object into the list of |closedTabs|. We will
+ * determine the right insertion point based on the .closedAt properties of
+ * all tabs already in the list. The list will be truncated to contain a
+ * maximum of |this._max_tabs_undo| entries.
+ *
+ * @param closedTabs (array)
+ * The list of closed tabs for a window.
+ * @param tabData (object)
+ * The tabData to be inserted.
+ */
+ saveClosedTabData(closedTabs, tabData) {
+ // Find the index of the first tab in the list
+ // of closed tabs that was closed before our tab.
+ let index = closedTabs.findIndex(tab => {
+ return tab.closedAt < tabData.closedAt;
+ });
+
+ // If we found no tab closed before our
+ // tab then just append it to the list.
+ if (index == -1) {
+ index = closedTabs.length;
+ }
+
+ // About to save the closed tab, add a unique ID.
+ tabData.closedId = this._nextClosedId++;
+
+ // Insert tabData at the right position.
+ closedTabs.splice(index, 0, tabData);
+
+ // Truncate the list of closed tabs, if needed.
+ if (closedTabs.length > this._max_tabs_undo) {
+ closedTabs.splice(this._max_tabs_undo, closedTabs.length);
+ }
+ },
+
+ /**
+ * Remove the closed tab data at |index| from the list of |closedTabs|. If
+ * the tab's final message is still pending we will simply discard it when
+ * it arrives so that the tab doesn't reappear in the list.
+ *
+ * @param closedTabs (array)
+ * The list of closed tabs for a window.
+ * @param index (uint)
+ * The index of the tab to remove.
+ */
+ removeClosedTabData(closedTabs, index) {
+ // Remove the given index from the list.
+ let [closedTab] = closedTabs.splice(index, 1);
+
+ // If the closed tab's state still has a .permanentKey property then we
+ // haven't seen its final update message yet. Remove it from the map of
+ // closed tabs so that we will simply discard its last messages and will
+ // not add it back to the list of closed tabs again.
+ if (closedTab.permanentKey) {
+ this._closedTabs.delete(closedTab.permanentKey);
+ this._closedWindowTabs.delete(closedTab.permanentKey);
+ delete closedTab.permanentKey;
+ }
+
+ return closedTab;
+ },
+
+ /**
+ * When a tab is selected, save session data
+ * @param aWindow
+ * Window reference
+ */
+ onTabSelect: function ssi_onTabSelect(aWindow) {
+ if (RunState.isRunning) {
+ this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
+
+ let tab = aWindow.gBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+
+ if (browser.__SS_restoreState &&
+ browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
+ // If __SS_restoreState is still on the browser and it is
+ // TAB_STATE_NEEDS_RESTORE, then then we haven't restored
+ // this tab yet.
+ //
+ // It's possible that this tab was recently revived, and that
+ // we've deferred showing the tab crashed page for it (if the
+ // tab crashed in the background). If so, we need to re-enter
+ // the crashed state, since we'll be showing the tab crashed
+ // page.
+ if (TabCrashHandler.willShowCrashedTab(browser)) {
+ this.enterCrashedState(browser);
+ } else {
+ this.restoreTabContent(tab);
+ }
+ }
+ }
+ },
+
+ onTabShow: function ssi_onTabShow(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. This used to be due to changing groups in 'tab groups'. We
+ // might be able to get rid of this now?
+ this.saveStateDelayed(aWindow);
+ },
+
+ onTabHide: function ssi_onTabHide(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. This used to be due to changing groups in 'tab groups'. We
+ // might be able to get rid of this now?
+ this.saveStateDelayed(aWindow);
+ },
+
+ /**
+ * Handler for the event that is fired when a <xul:browser> crashes.
+ *
+ * @param aWindow
+ * The window that the crashed browser belongs to.
+ * @param aBrowser
+ * The <xul:browser> that is now in the crashed state.
+ */
+ onBrowserCrashed: function(aBrowser) {
+ NS_ASSERT(aBrowser.isRemoteBrowser,
+ "Only remote browsers should be able to crash");
+
+ this.enterCrashedState(aBrowser);
+ // The browser crashed so we might never receive flush responses.
+ // Resolve all pending flush requests for the crashed browser.
+ TabStateFlusher.resolveAll(aBrowser);
+ },
+
+ /**
+ * Called when a browser is showing or is about to show the tab
+ * crashed page. This method causes SessionStore to ignore the
+ * tab until it's restored.
+ *
+ * @param browser
+ * The <xul:browser> that is about to show the crashed page.
+ */
+ enterCrashedState(browser) {
+ this._crashedBrowsers.add(browser.permanentKey);
+
+ let win = browser.ownerGlobal;
+
+ // If we hadn't yet restored, or were still in the midst of
+ // restoring this browser at the time of the crash, we need
+ // to reset its state so that we can try to restore it again
+ // when the user revives the tab from the crash.
+ if (browser.__SS_restoreState) {
+ let tab = win.gBrowser.getTabForBrowser(browser);
+ this._resetLocalTabRestoringState(tab);
+ }
+ },
+
+ // Clean up data that has been closed a long time ago.
+ // Do not reschedule a save. This will wait for the next regular
+ // save.
+ onIdleDaily: function() {
+ // Remove old closed windows
+ this._cleanupOldData([this._closedWindows]);
+
+ // Remove closed tabs of closed windows
+ this._cleanupOldData(this._closedWindows.map((winData) => winData._closedTabs));
+
+ // Remove closed tabs of open windows
+ this._cleanupOldData(Object.keys(this._windows).map((key) => this._windows[key]._closedTabs));
+ },
+
+ // Remove "old" data from an array
+ _cleanupOldData: function(targets) {
+ const TIME_TO_LIVE = this._prefBranch.getIntPref("sessionstore.cleanup.forget_closed_after");
+ const now = Date.now();
+
+ for (let array of targets) {
+ for (let i = array.length - 1; i >= 0; --i) {
+ let data = array[i];
+ // Make sure that we have a timestamp to tell us when the target
+ // has been closed. If we don't have a timestamp, default to a
+ // safe timestamp: just now.
+ data.closedAt = data.closedAt || now;
+ if (now - data.closedAt > TIME_TO_LIVE) {
+ array.splice(i, 1);
+ }
+ }
+ }
+ },
+
+ /* ........ nsISessionStore API .............. */
+
+ getBrowserState: function ssi_getBrowserState() {
+ let state = this.getCurrentState();
+
+ // Don't include the last session state in getBrowserState().
+ delete state.lastSessionState;
+
+ // Don't include any deferred initial state.
+ delete state.deferredInitialState;
+
+ return JSON.stringify(state);
+ },
+
+ setBrowserState: function ssi_setBrowserState(aState) {
+ this._handleClosedWindows();
+
+ try {
+ var state = JSON.parse(aState);
+ }
+ catch (ex) { /* invalid state object - don't restore anything */ }
+ if (!state) {
+ throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (!state.windows) {
+ throw Components.Exception("No windows", 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;
+
+ // global data must be restored before restoreWindow is called so that
+ // it happens before observers are notified
+ this._globalState.setFromState(state);
+
+ // restore to the given state
+ this.restoreWindows(window, state, {overwriteTabs: true});
+ },
+
+ getWindowState: function ssi_getWindowState(aWindow) {
+ if ("__SSi" in aWindow) {
+ return JSON.stringify(this._getWindowState(aWindow));
+ }
+
+ if (DyingWindowCache.has(aWindow)) {
+ let data = DyingWindowCache.get(aWindow);
+ return JSON.stringify({ windows: [data] });
+ }
+
+ throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
+ if (!aWindow.__SSi) {
+ throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ this.restoreWindows(aWindow, aState, {overwriteTabs: aOverwrite});
+ },
+
+ getTabState: function ssi_getTabState(aTab) {
+ if (!aTab.ownerGlobal.__SSi) {
+ throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let tabState = TabState.collect(aTab);
+
+ return JSON.stringify(tabState);
+ },
+
+ setTabState(aTab, aState) {
+ // Remove the tab state from the cache.
+ // Note that we cannot simply replace the contents of the cache
+ // as |aState| can be an incomplete state that will be completed
+ // by |restoreTabs|.
+ let tabState = JSON.parse(aState);
+ if (!tabState) {
+ throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (typeof tabState != "object") {
+ throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (!("entries" in tabState)) {
+ throw Components.Exception("Invalid state object: no entries", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let window = aTab.ownerGlobal;
+ if (!("__SSi" in window)) {
+ throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ if (aTab.linkedBrowser.__SS_restoreState) {
+ this._resetTabRestoringState(aTab);
+ }
+
+ this.restoreTab(aTab, tabState);
+ },
+
+ duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0, aRestoreImmediately = true) {
+ if (!aTab.ownerGlobal.__SSi) {
+ throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (!aWindow.gBrowser) {
+ throw Components.Exception("Invalid window object: no gBrowser", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Create a new tab.
+ let userContextId = aTab.getAttribute("usercontextid");
+ let newTab = aTab == aWindow.gBrowser.selectedTab ?
+ aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab, userContextId}) :
+ aWindow.gBrowser.addTab(null, {userContextId});
+
+ // Set tab title to "Connecting..." and start the throbber to pretend we're
+ // doing something while actually waiting for data from the frame script.
+ aWindow.gBrowser.setTabTitleLoading(newTab);
+ newTab.setAttribute("busy", "true");
+
+ // Collect state before flushing.
+ let tabState = TabState.clone(aTab);
+
+ // Flush to get the latest tab state to duplicate.
+ let browser = aTab.linkedBrowser;
+ TabStateFlusher.flush(browser).then(() => {
+ // The new tab might have been closed in the meantime.
+ if (newTab.closing || !newTab.linkedBrowser) {
+ return;
+ }
+
+ let window = newTab.ownerGlobal;
+
+ // The tab or its window might be gone.
+ if (!window || !window.__SSi) {
+ return;
+ }
+
+ // Update state with flushed data. We can't use TabState.clone() here as
+ // the tab to duplicate may have already been closed. In that case we
+ // only have access to the <xul:browser>.
+ let options = {includePrivateData: true};
+ TabState.copyFromCache(browser, tabState, options);
+
+ tabState.index += aDelta;
+ tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
+ tabState.pinned = false;
+
+ // Restore the state into the new tab.
+ this.restoreTab(newTab, tabState, {
+ restoreImmediately: aRestoreImmediately
+ });
+ });
+
+ return newTab;
+ },
+
+ getClosedTabCount: function ssi_getClosedTabCount(aWindow) {
+ if ("__SSi" in aWindow) {
+ return this._windows[aWindow.__SSi]._closedTabs.length;
+ }
+
+ if (!DyingWindowCache.has(aWindow)) {
+ throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return DyingWindowCache.get(aWindow)._closedTabs.length;
+ },
+
+ getClosedTabData: function ssi_getClosedTabData(aWindow, aAsString = true) {
+ if ("__SSi" in aWindow) {
+ return aAsString ?
+ JSON.stringify(this._windows[aWindow.__SSi]._closedTabs) :
+ Cu.cloneInto(this._windows[aWindow.__SSi]._closedTabs, {});
+ }
+
+ if (!DyingWindowCache.has(aWindow)) {
+ throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let data = DyingWindowCache.get(aWindow);
+ return aAsString ? JSON.stringify(data._closedTabs) : Cu.cloneInto(data._closedTabs, {});
+ },
+
+ undoCloseTab: function ssi_undoCloseTab(aWindow, aIndex) {
+ if (!aWindow.__SSi) {
+ throw Components.Exception("Window is not tracked", 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.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // fetch the data of closed tab, while removing it from the array
+ let {state, pos} = this.removeClosedTabData(closedTabs, aIndex);
+
+ // create a new tab
+ let tabbrowser = aWindow.gBrowser;
+ let tab = tabbrowser.selectedTab = tabbrowser.addTab(null, state);
+
+ // restore tab content
+ this.restoreTab(tab, state);
+
+ // restore the tab's position
+ tabbrowser.moveTabTo(tab, pos);
+
+ // focus the tab's content area (bug 342432)
+ tab.linkedBrowser.focus();
+
+ return tab;
+ },
+
+ forgetClosedTab: function ssi_forgetClosedTab(aWindow, aIndex) {
+ if (!aWindow.__SSi) {
+ throw Components.Exception("Window is not tracked", 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.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // remove closed tab from the array
+ this.removeClosedTabData(closedTabs, aIndex);
+ },
+
+ getClosedWindowCount: function ssi_getClosedWindowCount() {
+ return this._closedWindows.length;
+ },
+
+ getClosedWindowData: function ssi_getClosedWindowData(aAsString = true) {
+ return aAsString ? JSON.stringify(this._closedWindows) : Cu.cloneInto(this._closedWindows, {});
+ },
+
+ undoCloseWindow: function ssi_undoCloseWindow(aIndex) {
+ if (!(aIndex in this._closedWindows)) {
+ throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // reopen the window
+ let state = { windows: this._closedWindows.splice(aIndex, 1) };
+ delete state.windows[0].closedAt; // Window is now open.
+
+ let window = this._openWindowWithState(state);
+ this.windowToFocus = window;
+ return window;
+ },
+
+ forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
+ // default to the most-recently closed window
+ aIndex = aIndex || 0;
+ if (!(aIndex in this._closedWindows)) {
+ throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // remove closed window from the array
+ let winData = this._closedWindows[aIndex];
+ this._closedWindows.splice(aIndex, 1);
+ this._saveableClosedWindowData.delete(winData);
+ },
+
+ getWindowValue: function ssi_getWindowValue(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.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) {
+ if (typeof aStringValue != "string") {
+ throw new TypeError("setWindowValue only accepts string values");
+ }
+
+ if (!("__SSi" in aWindow)) {
+ throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+ }
+ if (!this._windows[aWindow.__SSi].extData) {
+ this._windows[aWindow.__SSi].extData = {};
+ }
+ this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
+ this.saveStateDelayed(aWindow);
+ },
+
+ deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) {
+ if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
+ this._windows[aWindow.__SSi].extData[aKey])
+ delete this._windows[aWindow.__SSi].extData[aKey];
+ this.saveStateDelayed(aWindow);
+ },
+
+ getTabValue: function ssi_getTabValue(aTab, aKey) {
+ return (aTab.__SS_extdata || {})[aKey] || "";
+ },
+
+ setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
+ if (typeof aStringValue != "string") {
+ throw new TypeError("setTabValue only accepts string values");
+ }
+
+ // If the tab hasn't been restored, then set the data there, otherwise we
+ // could lose newly added data.
+ if (!aTab.__SS_extdata) {
+ aTab.__SS_extdata = {};
+ }
+
+ aTab.__SS_extdata[aKey] = aStringValue;
+ this.saveStateDelayed(aTab.ownerGlobal);
+ },
+
+ deleteTabValue: function ssi_deleteTabValue(aTab, aKey) {
+ if (aTab.__SS_extdata && aKey in aTab.__SS_extdata) {
+ delete aTab.__SS_extdata[aKey];
+ this.saveStateDelayed(aTab.ownerGlobal);
+ }
+ },
+
+ getGlobalValue: function ssi_getGlobalValue(aKey) {
+ return this._globalState.get(aKey);
+ },
+
+ setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
+ if (typeof aStringValue != "string") {
+ throw new TypeError("setGlobalValue only accepts string values");
+ }
+
+ this._globalState.set(aKey, aStringValue);
+ this.saveStateDelayed();
+ },
+
+ deleteGlobalValue: function ssi_deleteGlobalValue(aKey) {
+ this._globalState.delete(aKey);
+ this.saveStateDelayed();
+ },
+
+ persistTabAttribute: function ssi_persistTabAttribute(aName) {
+ if (TabAttributes.persist(aName)) {
+ this.saveStateDelayed();
+ }
+ },
+
+
+ /**
+ * Undoes the closing of a tab or window which corresponds
+ * to the closedId passed in.
+ *
+ * @param aClosedId
+ * The closedId of the tab or window
+ *
+ * @returns a tab or window object
+ */
+ undoCloseById(aClosedId) {
+ // Check for a window first.
+ for (let i = 0, l = this._closedWindows.length; i < l; i++) {
+ if (this._closedWindows[i].closedId == aClosedId) {
+ return this.undoCloseWindow(i);
+ }
+ }
+
+ // Check for a tab.
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ let windowState = this._windows[window.__SSi];
+ if (windowState) {
+ for (let j = 0, l = windowState._closedTabs.length; j < l; j++) {
+ if (windowState._closedTabs[j].closedId == aClosedId) {
+ return this.undoCloseTab(window, j);
+ }
+ }
+ }
+ }
+
+ // Neither a tab nor a window was found, return undefined and let the caller decide what to do about it.
+ return undefined;
+ },
+
+ /**
+ * Restores the session state stored in LastSession. 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 window. Otherwise new windows will
+ * be opened.
+ */
+ restoreLastSession: function ssi_restoreLastSession() {
+ // Use the public getter since it also checks PB mode
+ if (!this.canRestoreLastSession) {
+ throw Components.Exception("Last session can not be restored");
+ }
+
+ Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE, "");
+
+ // First collect each window with its id...
+ let windows = {};
+ this._forEachBrowserWindow(function(aWindow) {
+ if (aWindow.__SS_lastSessionWindowID)
+ windows[aWindow.__SS_lastSessionWindowID] = aWindow;
+ });
+
+ let lastSessionState = LastSession.getState();
+
+ // This shouldn't ever be the case...
+ if (!lastSessionState.windows.length) {
+ throw Components.Exception("lastSessionState has no windows", 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;
+
+ // global data must be restored before restoreWindow is called so that
+ // it happens before observers are notified
+ this._globalState.setFromState(lastSessionState);
+
+ // 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._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.
+ let options = {overwriteTabs: canOverwriteTabs, isFollowUp: true};
+ this.restoreWindow(windowToUse, winState, options);
+ }
+ 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();
+ }
+
+ if (lastSessionState.scratchpads) {
+ ScratchpadManager.restoreSession(lastSessionState.scratchpads);
+ }
+
+ // Set data that persists between sessions
+ this._recentCrashes = lastSessionState.session &&
+ lastSessionState.session.recentCrashes || 0;
+
+ // Update the session start time using the restored session state.
+ this._updateSessionStartTime(lastSessionState);
+
+ LastSession.clear();
+ },
+
+ /**
+ * Revive a crashed tab and restore its state from before it crashed.
+ *
+ * @param aTab
+ * A <xul:tab> linked to a crashed browser. This is a no-op if the
+ * browser hasn't actually crashed, or is not associated with a tab.
+ * This function will also throw if the browser happens to be remote.
+ */
+ reviveCrashedTab(aTab) {
+ if (!aTab) {
+ throw new Error("SessionStore.reviveCrashedTab expected a tab, but got null.");
+ }
+
+ let browser = aTab.linkedBrowser;
+ if (!this._crashedBrowsers.has(browser.permanentKey)) {
+ return;
+ }
+
+ // Sanity check - the browser to be revived should not be remote
+ // at this point.
+ if (browser.isRemoteBrowser) {
+ throw new Error("SessionStore.reviveCrashedTab: " +
+ "Somehow a crashed browser is still remote.")
+ }
+
+ // We put the browser at about:blank in case the user is
+ // restoring tabs on demand. This way, the user won't see
+ // a flash of the about:tabcrashed page after selecting
+ // the revived tab.
+ aTab.removeAttribute("crashed");
+ browser.loadURI("about:blank", null, null);
+
+ let data = TabState.collect(aTab);
+ this.restoreTab(aTab, data, {
+ forceOnDemand: true,
+ });
+ },
+
+ /**
+ * Revive all crashed tabs and reset the crashed tabs count to 0.
+ */
+ reviveAllCrashedTabs() {
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ for (let tab of window.gBrowser.tabs) {
+ this.reviveCrashedTab(tab);
+ }
+ }
+ },
+
+ /**
+ * Navigate the given |tab| by first collecting its current state and then
+ * either changing only the index of the currently shown history entry,
+ * or restoring the exact same state again and passing the new URL to load
+ * in |loadArguments|. Use this method to seamlessly switch between pages
+ * loaded in the parent and pages loaded in the child process.
+ *
+ * This method might be called multiple times before it has finished
+ * flushing the browser tab. If that occurs, the loadArguments from
+ * the most recent call to navigateAndRestore will be used once the
+ * flush has finished.
+ */
+ navigateAndRestore(tab, loadArguments, historyIndex) {
+ let window = tab.ownerGlobal;
+ NS_ASSERT(window.__SSi, "tab's window must be tracked");
+ let browser = tab.linkedBrowser;
+
+ // Were we already waiting for a flush from a previous call to
+ // navigateAndRestore on this tab?
+ let alreadyRestoring =
+ this._remotenessChangingBrowsers.has(browser.permanentKey);
+
+ // Stash the most recent loadArguments in this WeakMap so that
+ // we know to use it when the TabStateFlusher.flush resolves.
+ this._remotenessChangingBrowsers.set(browser.permanentKey, loadArguments);
+
+ if (alreadyRestoring) {
+ // This tab was already being restored to run in the
+ // correct process. We're done here.
+ return;
+ }
+
+ // Set tab title to "Connecting..." and start the throbber to pretend we're
+ // doing something while actually waiting for data from the frame script.
+ window.gBrowser.setTabTitleLoading(tab);
+ tab.setAttribute("busy", "true");
+
+ // Flush to get the latest tab state.
+ TabStateFlusher.flush(browser).then(() => {
+ // loadArguments might have been overwritten by multiple calls
+ // to navigateAndRestore while we waited for the tab to flush,
+ // so we use the most recently stored one.
+ let recentLoadArguments =
+ this._remotenessChangingBrowsers.get(browser.permanentKey);
+ this._remotenessChangingBrowsers.delete(browser.permanentKey);
+
+ // The tab might have been closed/gone in the meantime.
+ if (tab.closing || !tab.linkedBrowser) {
+ return;
+ }
+
+ let window = tab.ownerGlobal;
+
+ // The tab or its window might be gone.
+ if (!window || !window.__SSi || window.closed) {
+ return;
+ }
+
+ let tabState = TabState.clone(tab);
+ let options = {
+ restoreImmediately: true,
+ // We want to make sure that this information is passed to restoreTab
+ // whether or not a historyIndex is passed in. Thus, we extract it from
+ // the loadArguments.
+ reloadInFreshProcess: !!recentLoadArguments.reloadInFreshProcess,
+ };
+
+ if (historyIndex >= 0) {
+ tabState.index = historyIndex + 1;
+ tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
+ } else {
+ options.loadArguments = recentLoadArguments;
+ }
+
+ // Need to reset restoring tabs.
+ if (tab.linkedBrowser.__SS_restoreState) {
+ this._resetLocalTabRestoringState(tab);
+ }
+
+ // Restore the state into the tab.
+ this.restoreTab(tab, tabState, options);
+ });
+
+ tab.linkedBrowser.__SS_restoreState = TAB_STATE_WILL_RESTORE;
+ },
+
+ /**
+ * Retrieves the latest session history information for a tab. The cached data
+ * is returned immediately, but a callback may be provided that supplies
+ * up-to-date data when or if it is available. The callback is passed a single
+ * argument with data in the same format as the return value.
+ *
+ * @param tab tab to retrieve the session history for
+ * @param updatedCallback function to call with updated data as the single argument
+ * @returns a object containing 'index' specifying the current index, and an
+ * array 'entries' containing an object for each history item.
+ */
+ getSessionHistory(tab, updatedCallback) {
+ if (updatedCallback) {
+ TabStateFlusher.flush(tab.linkedBrowser).then(() => {
+ let sessionHistory = this.getSessionHistory(tab);
+ if (sessionHistory) {
+ updatedCallback(sessionHistory);
+ }
+ });
+ }
+
+ // Don't continue if the tab was closed before TabStateFlusher.flush resolves.
+ if (tab.linkedBrowser) {
+ let tabState = TabState.collect(tab);
+ return { index: tabState.index - 1, entries: tabState.entries }
+ }
+ },
+
+ /**
+ * 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 ssi_prepWindowToRestoreInto(aWindow) {
+ if (!aWindow)
+ 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 window dimensions, visibility, sidebar
+ * @param aWindow
+ * Window reference
+ */
+ _updateWindowFeatures: function ssi_updateWindowFeatures(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
+ * @returns object
+ */
+ getCurrentState: function (aUpdateAll) {
+ this._handleClosedWindows();
+
+ var activeWindow = this._getMostRecentBrowserWindow();
+
+ TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
+ if (RunState.isRunning) {
+ // 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 || DirtyWindows.has(aWindow) || aWindow == activeWindow) {
+ this._collectWindowData(aWindow);
+ }
+ else { // always update the window features (whose change alone never triggers a save operation)
+ this._updateWindowFeatures(aWindow);
+ }
+ });
+ DirtyWindows.clear();
+ }
+ TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
+
+ // An array that at the end will hold all current window data.
+ var total = [];
+ // The ids of all windows contained in 'total' in the same order.
+ var ids = [];
+ // The number of window that are _not_ popups.
+ var nonPopupCount = 0;
+ var ix;
+
+ // collect the data for all windows
+ 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);
+ if (!this._windows[ix].isPopup)
+ nonPopupCount++;
+ }
+
+ TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
+ SessionCookies.update(total);
+ TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
+
+ // collect the data for all windows yet to be restored
+ for (ix in this._statesToRestore) {
+ for (let winData of this._statesToRestore[ix].windows) {
+ total.push(winData);
+ if (!winData.isPopup)
+ nonPopupCount++;
+ }
+ }
+
+ // shallow copy this._closedWindows to preserve current state
+ let lastClosedWindowsCopy = this._closedWindows.slice();
+
+ if (AppConstants.platform != "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 &&
+ RunState.isQuitting) {
+ // 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)
+ }
+ }
+
+ 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 = {
+ lastUpdate: Date.now(),
+ startTime: this._sessionStartTime,
+ recentCrashes: this._recentCrashes
+ };
+
+ let state = {
+ version: ["sessionrestore", FORMAT_VERSION],
+ windows: total,
+ selectedWindow: ix + 1,
+ _closedWindows: lastClosedWindowsCopy,
+ session: session,
+ global: this._globalState.getState()
+ };
+
+ if (Cu.isModuleLoaded("resource://devtools/client/scratchpad/scratchpad-manager.jsm")) {
+ // get open Scratchpad window states too
+ let scratchpads = ScratchpadManager.getSessionState();
+ if (scratchpads && scratchpads.length) {
+ state.scratchpads = scratchpads;
+ }
+ }
+
+ // Persist the last session if we deferred restoring it
+ if (LastSession.canRestore) {
+ state.lastSessionState = LastSession.getState();
+ }
+
+ // If we were called by the SessionSaver and started with only a private
+ // window we want to pass the deferred initial state to not lose the
+ // previous session.
+ if (this._deferredInitialState) {
+ state.deferredInitialState = this._deferredInitialState;
+ }
+
+ return state;
+ },
+
+ /**
+ * serialize session data for a window
+ * @param aWindow
+ * Window reference
+ * @returns string
+ */
+ _getWindowState: function ssi_getWindowState(aWindow) {
+ if (!this._isWindowLoaded(aWindow))
+ return this._statesToRestore[aWindow.__SS_restoreID];
+
+ if (RunState.isRunning) {
+ this._collectWindowData(aWindow);
+ }
+
+ let windows = [this._windows[aWindow.__SSi]];
+ SessionCookies.update(windows);
+
+ return { windows: windows };
+ },
+
+ /**
+ * Gathers data about a window and its tabs, and updates its
+ * entry in this._windows.
+ *
+ * @param aWindow
+ * Window references.
+ * @returns a Map mapping the browser tabs from aWindow to the tab
+ * entry that was put into the window data in this._windows.
+ */
+ _collectWindowData: function ssi_collectWindowData(aWindow) {
+ let tabMap = new Map();
+
+ if (!this._isWindowLoaded(aWindow))
+ return tabMap;
+
+ let tabbrowser = aWindow.gBrowser;
+ let tabs = tabbrowser.tabs;
+ let winData = this._windows[aWindow.__SSi];
+ let tabsData = winData.tabs = [];
+
+ // update the internal state data for this window
+ for (let tab of tabs) {
+ let tabData = TabState.collect(tab);
+ tabMap.set(tab, tabData);
+ tabsData.push(tabData);
+ }
+ winData.selected = tabbrowser.mTabBox.selectedIndex + 1;
+
+ 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;
+
+ DirtyWindows.remove(aWindow);
+ return tabMap;
+ },
+
+ /* ........ Restoring Functionality .............. */
+
+ /**
+ * restore features to a single window
+ * @param aWindow
+ * Window reference to the window to use for restoration
+ * @param winData
+ * JS object
+ * @param aOptions
+ * {overwriteTabs: true} to overwrite existing tabs w/ new ones
+ * {isFollowUp: true} if this is not the restoration of the 1st window
+ * {firstWindow: true} if this is the first non-private window we're
+ * restoring in this session, that might open an
+ * external link as well
+ */
+ restoreWindow: function ssi_restoreWindow(aWindow, winData, aOptions = {}) {
+ let overwriteTabs = aOptions && aOptions.overwriteTabs;
+ let isFollowUp = aOptions && aOptions.isFollowUp;
+ let firstWindow = aOptions && aOptions.firstWindow;
+
+ if (isFollowUp) {
+ this.windowToFocus = aWindow;
+ }
+
+ // initialize window if necessary
+ if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
+ this.onLoad(aWindow);
+
+ TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
+
+ // We're not returning from this before we end up calling restoreTabs
+ // for this window, so make sure we send the SSWindowStateBusy event.
+ this._setWindowStateBusy(aWindow);
+
+ 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 (firstWindow && !overwriteTabs && winData.tabs.length == 1 &&
+ (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) {
+ winData.tabs = [];
+ }
+
+ var tabbrowser = aWindow.gBrowser;
+ var openTabCount = overwriteTabs ? 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 (overwriteTabs) {
+ for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--)
+ tabbrowser.unpinTab(tabbrowser.tabs[t]);
+ }
+
+ // We need to keep track of the initially open tabs so that they
+ // can be moved to the end of the restored tabs.
+ let initialTabs = [];
+ if (!overwriteTabs && firstWindow) {
+ initialTabs = Array.slice(tabbrowser.tabs);
+ }
+
+ // make sure that the selected tab won't be closed in order to
+ // prevent unnecessary flickering
+ if (overwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount)
+ tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1);
+
+ let numVisibleTabs = 0;
+
+ for (var t = 0; t < newTabCount; t++) {
+ // When trying to restore into existing tab, we also take the userContextId
+ // into account if present.
+ let userContextId = winData.tabs[t].userContextId;
+ let reuseExisting = t < openTabCount &&
+ (tabbrowser.tabs[t].getAttribute("usercontextid") == (userContextId || ""));
+ // If the tab is pinned, then we'll be loading it right away, and
+ // there's no need to cause a remoteness flip by loading it initially
+ // non-remote.
+ let forceNotRemote = !winData.tabs[t].pinned;
+ let tab = reuseExisting ? tabbrowser.tabs[t] :
+ tabbrowser.addTab("about:blank",
+ {skipAnimation: true,
+ forceNotRemote,
+ userContextId});
+
+ // If we inserted a new tab because the userContextId didn't match with the
+ // open tab, even though `t < openTabCount`, we need to remove that open tab
+ // and put the newly added tab in its place.
+ if (!reuseExisting && t < openTabCount) {
+ tabbrowser.removeTab(tabbrowser.tabs[t]);
+ tabbrowser.moveTabTo(tab, t);
+ }
+
+ tabs.push(tab);
+
+ 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 (!!winData.tabs[t].muted != tabs[t].linkedBrowser.audioMuted) {
+ tabs[t].toggleMuteAudio(winData.tabs[t].muteReason);
+ }
+ }
+
+ if (!overwriteTabs && firstWindow) {
+ // Move the originally open tabs to the end
+ let endPosition = tabbrowser.tabs.length - 1;
+ for (let i = 0; i < initialTabs.length; i++) {
+ tabbrowser.moveTabTo(initialTabs[i], endPosition);
+ }
+ }
+
+ // 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 restoreTabs).
+ if (overwriteTabs) {
+ for (let i = 0; i < tabbrowser.tabs.length; i++) {
+ let tab = tabbrowser.tabs[i];
+ if (tabbrowser.browsers[i].__SS_restoreState)
+ this._resetTabRestoringState(tab);
+ }
+ }
+
+ // 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 (overwriteTabs && newTabCount < openTabCount) {
+ Array.slice(tabbrowser.tabs, newTabCount, openTabCount)
+ .forEach(tabbrowser.removeTab, tabbrowser);
+ }
+
+ if (overwriteTabs) {
+ this.restoreWindowFeatures(aWindow, winData);
+ delete this._windows[aWindow.__SSi].extData;
+ }
+ if (winData.cookies) {
+ SessionCookies.restore(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];
+ }
+ }
+
+ let newClosedTabsData = winData._closedTabs || [];
+
+ if (overwriteTabs || firstWindow) {
+ // Overwrite existing closed tabs data when overwriteTabs=true
+ // or we're the first window to be restored.
+ this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData;
+ } else if (this._max_tabs_undo > 0) {
+ // If we merge tabs, we also want to merge closed tabs data. We'll assume
+ // the restored tabs were closed more recently and append the current list
+ // of closed tabs to the new one...
+ newClosedTabsData =
+ newClosedTabsData.concat(this._windows[aWindow.__SSi]._closedTabs);
+
+ // ... and make sure that we don't exceed the max number of closed tabs
+ // we can restore.
+ this._windows[aWindow.__SSi]._closedTabs =
+ newClosedTabsData.slice(0, this._max_tabs_undo);
+ }
+
+ // Restore tabs, if any.
+ if (winData.tabs.length) {
+ this.restoreTabs(aWindow, tabs, winData.tabs,
+ (overwriteTabs ? (parseInt(winData.selected || "1")) : 0));
+ }
+
+ // set smoothScroll back to the original value
+ tabstrip.smoothScroll = smoothScroll;
+
+ TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
+
+ this._setWindowStateReady(aWindow);
+
+ this._sendWindowRestoredNotification(aWindow);
+
+ Services.obs.notifyObservers(aWindow, NOTIFY_SINGLE_WINDOW_RESTORED, "");
+
+ this._sendRestoreCompletedNotifications();
+ },
+
+ /**
+ * Restore multiple windows using the provided state.
+ * @param aWindow
+ * Window reference to the first window to use for restoration.
+ * Additionally required windows will be opened.
+ * @param aState
+ * JS object or JSON string
+ * @param aOptions
+ * {overwriteTabs: true} to overwrite existing tabs w/ new ones
+ * {isFollowUp: true} if this is not the restoration of the 1st window
+ * {firstWindow: true} if this is the first non-private window we're
+ * restoring in this session, that might open an
+ * external link as well
+ */
+ restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) {
+ let isFollowUp = aOptions && aOptions.isFollowUp;
+
+ if (isFollowUp) {
+ this.windowToFocus = aWindow;
+ }
+
+ // initialize window if necessary
+ if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
+ this.onLoad(aWindow);
+
+ let root;
+ try {
+ root = (typeof aState == "string") ? JSON.parse(aState) : aState;
+ }
+ catch (ex) { // invalid state object - don't restore anything
+ debug(ex);
+ this._sendRestoreCompletedNotifications();
+ return;
+ }
+
+ // Restore closed windows if any.
+ if (root._closedWindows) {
+ this._closedWindows = root._closedWindows;
+ }
+
+ // We're done here if there are no windows.
+ if (!root.windows || !root.windows.length) {
+ this._sendRestoreCompletedNotifications();
+ return;
+ }
+
+ 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)
+ let winData;
+ 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;
+ }
+ }
+ }
+
+ this.restoreWindow(aWindow, root.windows[0], aOptions);
+
+ if (aState.scratchpads) {
+ ScratchpadManager.restoreSession(aState.scratchpads);
+ }
+ },
+
+ /**
+ * 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 the tab to select. This is a 1-based index where "1"
+ * indicates the first tab should be selected, and "0" indicates that
+ * the currently selected tab will not be changed.
+ */
+ restoreTabs(aWindow, aTabs, aTabData, aSelectTab) {
+ var tabbrowser = aWindow.gBrowser;
+
+ 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;
+ }
+
+ let numTabsToRestore = aTabs.length;
+ let numTabsInWindow = tabbrowser.tabs.length;
+ let tabsDataArray = this._windows[aWindow.__SSi].tabs;
+
+ // Update the window state in case we shut down without being notified.
+ // Individual tab states will be taken care of by restoreTab() below.
+ if (numTabsInWindow == numTabsToRestore) {
+ // Remove all previous tab data.
+ tabsDataArray.length = 0;
+ } else {
+ // Remove all previous tab data except tabs that should not be overriden.
+ tabsDataArray.splice(numTabsInWindow - numTabsToRestore);
+ }
+
+ // Let the tab data array have the right number of slots.
+ tabsDataArray.length = numTabsInWindow;
+
+ // If provided, set the selected tab.
+ if (aSelectTab > 0 && aSelectTab <= aTabs.length) {
+ tabbrowser.selectedTab = aTabs[aSelectTab - 1];
+
+ // Update the window state in case we shut down without being notified.
+ this._windows[aWindow.__SSi].selected = aSelectTab;
+ }
+
+ // Restore all tabs.
+ for (let t = 0; t < aTabs.length; t++) {
+ this.restoreTab(aTabs[t], aTabData[t]);
+ }
+ },
+
+ // Restores the given tab state for a given tab.
+ restoreTab(tab, tabData, options = {}) {
+ NS_ASSERT(!tab.linkedBrowser.__SS_restoreState,
+ "must reset tab before calling restoreTab()");
+
+ let restoreImmediately = options.restoreImmediately;
+ let loadArguments = options.loadArguments;
+ let browser = tab.linkedBrowser;
+ let window = tab.ownerGlobal;
+ let tabbrowser = window.gBrowser;
+ let forceOnDemand = options.forceOnDemand;
+ let reloadInFreshProcess = options.reloadInFreshProcess;
+
+ let willRestoreImmediately = restoreImmediately ||
+ tabbrowser.selectedBrowser == browser ||
+ loadArguments;
+
+ if (!willRestoreImmediately && !forceOnDemand) {
+ TabRestoreQueue.add(tab);
+ }
+
+ this._maybeUpdateBrowserRemoteness({ tabbrowser, tab,
+ willRestoreImmediately });
+
+ // Increase the busy state counter before modifying the tab.
+ this._setWindowStateBusy(window);
+
+ // It's important to set the window state to dirty so that
+ // we collect their data for the first time when saving state.
+ DirtyWindows.add(window);
+
+ // In case we didn't collect/receive data for any tabs yet we'll have to
+ // fill the array with at least empty tabData objects until |_tPos| or
+ // we'll end up with |null| entries.
+ for (let otherTab of Array.slice(tabbrowser.tabs, 0, tab._tPos)) {
+ let emptyState = {entries: [], lastAccessed: otherTab.lastAccessed};
+ this._windows[window.__SSi].tabs.push(emptyState);
+ }
+
+ // Update the tab state in case we shut down without being notified.
+ this._windows[window.__SSi].tabs[tab._tPos] = tabData;
+
+ // Prepare the tab so that it can be properly restored. We'll pin/unpin
+ // and show/hide tabs as necessary. We'll also attach a copy of the tab's
+ // data in case we close it before it's been restored.
+ if (tabData.pinned) {
+ tabbrowser.pinTab(tab);
+ } else {
+ tabbrowser.unpinTab(tab);
+ }
+
+ if (tabData.hidden) {
+ tabbrowser.hideTab(tab);
+ } else {
+ tabbrowser.showTab(tab);
+ }
+
+ if (!!tabData.muted != browser.audioMuted) {
+ tab.toggleMuteAudio(tabData.muteReason);
+ }
+
+ if (tabData.lastAccessed) {
+ tab.updateLastAccessed(tabData.lastAccessed);
+ }
+
+ if ("attributes" in tabData) {
+ // Ensure that we persist tab attributes restored from previous sessions.
+ Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a));
+ }
+
+ if (!tabData.entries) {
+ tabData.entries = [];
+ }
+ if (tabData.extData) {
+ tab.__SS_extdata = Cu.cloneInto(tabData.extData, {});
+ } else {
+ delete tab.__SS_extdata;
+ }
+
+ // Tab is now open.
+ delete tabData.closedAt;
+
+ // Ensure the index is in bounds.
+ let activeIndex = (tabData.index || tabData.entries.length) - 1;
+ activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
+ activeIndex = Math.max(activeIndex, 0);
+
+ // Save the index in case we updated it above.
+ tabData.index = activeIndex + 1;
+
+ // Start a new epoch to discard all frame script messages relating to a
+ // previous epoch. All async messages that are still on their way to chrome
+ // will be ignored and don't override any tab data set when restoring.
+ let epoch = this.startNextEpoch(browser);
+
+ // keep the data around to prevent dataloss in case
+ // a tab gets closed before it's been properly restored
+ browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
+ browser.setAttribute("pending", "true");
+ tab.setAttribute("pending", "true");
+
+ // If we're restoring this tab, it certainly shouldn't be in
+ // the ignored set anymore.
+ this._crashedBrowsers.delete(browser.permanentKey);
+
+ // Update the persistent tab state cache with |tabData| information.
+ TabStateCache.update(browser, {
+ history: {entries: tabData.entries, index: tabData.index},
+ scroll: tabData.scroll || null,
+ storage: tabData.storage || null,
+ formdata: tabData.formdata || null,
+ disallow: tabData.disallow || null,
+ pageStyle: tabData.pageStyle || null,
+
+ // This information is only needed until the tab has finished restoring.
+ // When that's done it will be removed from the cache and we always
+ // collect it in TabState._collectBaseTabData().
+ image: tabData.image || "",
+ iconLoadingPrincipal: tabData.iconLoadingPrincipal || null,
+ userTypedValue: tabData.userTypedValue || "",
+ userTypedClear: tabData.userTypedClear || 0
+ });
+
+ browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
+ {tabData: tabData, epoch: epoch, loadArguments});
+
+ // Restore tab attributes.
+ if ("attributes" in tabData) {
+ TabAttributes.set(tab, tabData.attributes);
+ }
+
+ // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
+ // it ensures each window will have its selected tab loaded.
+ if (willRestoreImmediately) {
+ this.restoreTabContent(tab, loadArguments, reloadInFreshProcess);
+ } else if (!forceOnDemand) {
+ this.restoreNextTab();
+ }
+
+ // Decrease the busy state counter after we're done.
+ this._setWindowStateReady(window);
+ },
+
+ /**
+ * Kicks off restoring the given tab.
+ *
+ * @param aTab
+ * the tab to restore
+ * @param aLoadArguments
+ * optional load arguments used for loadURI()
+ * @param aReloadInFreshProcess
+ * true if we want to reload into a fresh process
+ */
+ restoreTabContent: function (aTab, aLoadArguments = null, aReloadInFreshProcess = false) {
+ if (aTab.hasAttribute("customizemode") && !aLoadArguments) {
+ return;
+ }
+
+ let browser = aTab.linkedBrowser;
+ let window = aTab.ownerGlobal;
+ let tabbrowser = window.gBrowser;
+ let tabData = TabState.clone(aTab);
+ let activeIndex = tabData.index - 1;
+ let activePageData = tabData.entries[activeIndex] || null;
+ let uri = activePageData ? activePageData.url || null : null;
+ if (aLoadArguments) {
+ uri = aLoadArguments.uri;
+ if (aLoadArguments.userContextId) {
+ browser.setAttribute("usercontextid", aLoadArguments.userContextId);
+ }
+ }
+
+ // We have to mark this tab as restoring first, otherwise
+ // the "pending" attribute will be applied to the linked
+ // browser, which removes it from the display list. We cannot
+ // flip the remoteness of any browser that is not being displayed.
+ this.markTabAsRestoring(aTab);
+
+ let isRemotenessUpdate = false;
+ if (aReloadInFreshProcess) {
+ isRemotenessUpdate = tabbrowser.switchBrowserIntoFreshProcess(browser);
+ } else {
+ isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL(browser, uri);
+ }
+
+ if (isRemotenessUpdate) {
+ // We updated the remoteness, so we need to send the history down again.
+ //
+ // Start a new epoch to discard all frame script messages relating to a
+ // previous epoch. All async messages that are still on their way to chrome
+ // will be ignored and don't override any tab data set when restoring.
+ let epoch = this.startNextEpoch(browser);
+
+ browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory", {
+ tabData: tabData,
+ epoch: epoch,
+ loadArguments: aLoadArguments,
+ isRemotenessUpdate,
+ });
+
+ }
+
+ // If the restored browser wants to show view source content, start up a
+ // view source browser that will load the required frame script.
+ if (uri && ViewSourceBrowser.isViewSource(uri)) {
+ new ViewSourceBrowser(browser);
+ }
+
+ browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
+ {loadArguments: aLoadArguments, isRemotenessUpdate});
+ },
+
+ /**
+ * Marks a given pending tab as restoring.
+ *
+ * @param aTab
+ * the pending tab to mark as restoring
+ */
+ markTabAsRestoring(aTab) {
+ let browser = aTab.linkedBrowser;
+ if (browser.__SS_restoreState != TAB_STATE_NEEDS_RESTORE) {
+ throw new Error("Given tab is not pending.");
+ }
+
+ // 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");
+ },
+
+ /**
+ * 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 ssi_restoreNextTab() {
+ // If we call in here while quitting, we don't actually want to do anything
+ if (RunState.isQuitting)
+ return;
+
+ // Don't exceed the maximum number of concurrent tab restores.
+ if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES)
+ return;
+
+ let tab = TabRestoreQueue.shift();
+ if (tab) {
+ this.restoreTabContent(tab);
+ }
+ },
+
+ /**
+ * Restore visibility and dimension features to a window
+ * @param aWindow
+ * Window reference
+ * @param aWinData
+ * Object containing session data for the window
+ */
+ restoreWindowFeatures: function ssi_restoreWindowFeatures(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 ssi_restoreDimensions(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) {
+ let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {};
+ screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight);
+ // screenX/Y are based on the origin of the screen's desktop-pixel coordinate space
+ let screenLeftCss = screenLeft.value;
+ let screenTopCss = screenTop.value;
+ // convert 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 (allowing a little slop
+ // for windows that may be deliberately placed with their border off-screen
+ // as when Win10 "snaps" a window to the left/right edge -- bug 1276516).
+ // First, ensure the left edge is large enough...
+ if (aLeft < screenLeftCss - SCREEN_EDGE_SLOP) {
+ aLeft = screenLeftCss;
+ }
+ // Then check the resulting right edge, and reduce it if necessary.
+ let right = aLeft + aWidth;
+ if (right > screenRightCss + SCREEN_EDGE_SLOP) {
+ 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;
+
+ // And do the same in the vertical dimension.
+ if (aTop < screenTopCss - SCREEN_EDGE_SLOP) {
+ aTop = screenTopCss;
+ }
+ let bottom = aTop + aHeight;
+ if (bottom > screenBottomCss + SCREEN_EDGE_SLOP) {
+ bottom = screenBottomCss;
+ if (aTop > screenTopCss) {
+ aTop = Math.max(bottom - aHeight, screenTopCss);
+ }
+ }
+ 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);
+ }
+ }
+ 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.SidebarUI.show(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();
+ }
+ },
+
+ /* ........ Disk Access .............. */
+
+ /**
+ * Save the current session state to disk, after a delay.
+ *
+ * @param aWindow (optional)
+ * Will mark the given window as dirty so that we will recollect its
+ * data before we start writing.
+ */
+ saveStateDelayed: function (aWindow = null) {
+ if (aWindow) {
+ DirtyWindows.add(aWindow);
+ }
+
+ SessionSaver.runDelayed();
+ },
+
+ /* ........ Auxiliary Functions .............. */
+
+ /**
+ * Determines whether or not a tab that is being restored needs
+ * to have its remoteness flipped first.
+ *
+ * @param (object) with the following properties:
+ *
+ * tabbrowser (<xul:tabbrowser>):
+ * The tabbrowser that the browser belongs to.
+ *
+ * tab (<xul:tab>):
+ * The tab being restored
+ *
+ * willRestoreImmediately (bool):
+ * true if the tab is going to have its content
+ * restored immediately by the caller.
+ *
+ */
+ _maybeUpdateBrowserRemoteness({ tabbrowser, tab,
+ willRestoreImmediately }) {
+ // If the browser we're attempting to restore happens to be
+ // remote, we need to flip it back to non-remote if it's going
+ // to go into the pending background tab state. This is to make
+ // sure that a background tab can't crash if it hasn't yet
+ // been restored.
+ //
+ // Normally, when a window is restored, the tabs that SessionStore
+ // inserts are non-remote - but the initial browser is, by default,
+ // remote, so this check and flip covers this case. The other case
+ // is when window state is overwriting the state of an existing
+ // window with some remote tabs.
+ let browser = tab.linkedBrowser;
+
+ // There are two ways that a tab might start restoring its content
+ // very soon - either the caller is going to restore the content
+ // immediately, or the TabRestoreQueue is set up so that the tab
+ // content is going to be restored in the very near future. In
+ // either case, we don't want to flip remoteness, since the browser
+ // will soon be loading content.
+ let willRestore = willRestoreImmediately ||
+ TabRestoreQueue.willRestoreSoon(tab);
+
+ if (browser.isRemoteBrowser && !willRestore) {
+ tabbrowser.updateBrowserRemoteness(browser, false);
+ }
+ },
+
+ /**
+ * Update the session start time and send a telemetry measurement
+ * for the number of days elapsed since the session was started.
+ *
+ * @param state
+ * The session state.
+ */
+ _updateSessionStartTime: function ssi_updateSessionStartTime(state) {
+ // Attempt to load the session start time from the session state
+ if (state.session && state.session.startTime) {
+ this._sessionStartTime = state.session.startTime;
+ }
+ },
+
+ /**
+ * 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 ssi_forEachBrowserWindow(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 ssi_getMostRecentBrowserWindow() {
+ return RecentWindow.getMostRecentBrowserWindow({ allowPopups: true });
+ },
+
+ /**
+ * 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 ssi_handleClosedWindows() {
+ 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 ssi_openWindowWithState(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 ssi_doResumeSession() {
+ 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 ssi_isCmdLineEmpty(aWindow, aState) {
+ var pinnedOnly = aState.windows &&
+ aState.windows.every(win =>
+ win.tabs.every(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;
+ },
+
+ /**
+ * 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 ssi_getWindowDimension(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;
+ },
+
+ /**
+ * @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 ssi_needsRestorePage(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 (this._hasSingleTabWithURL(winData, "about:sessionrestore") ||
+ this._hasSingleTabWithURL(winData, "about:welcomeback")) {
+ 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);
+ },
+
+ /**
+ * @param aWinData is the set of windows in session state
+ * @param aURL is the single URL we're looking for
+ * @returns whether the window data contains only the single URL passed
+ */
+ _hasSingleTabWithURL: function(aWinData, aURL) {
+ if (aWinData &&
+ aWinData.length == 1 &&
+ aWinData[0].tabs &&
+ aWinData[0].tabs.length == 1 &&
+ aWinData[0].tabs[0].entries &&
+ aWinData[0].tabs[0].entries.length == 1) {
+ return aURL == aWinData[0].tabs[0].entries[0].url;
+ }
+ return false;
+ },
+
+ /**
+ * 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 ssi_shouldSaveTabState(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.entries[0].url == "about:privatebrowsing") &&
+ !aTabState.userTypedValue);
+ },
+
+ /**
+ * 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 passed into
+ * LastSession 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 ssi_prepDataForDeferredRestore(state) {
+ // Make sure that we don't modify the global state as provided by
+ // nsSessionStartup.state.
+ state = Cu.cloneInto(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 ssi_splitCookiesFromWindow(aWinState, aTargetWinState) {
+ if (!aWinState.cookies || !aWinState.cookies.length)
+ return;
+
+ // Get the hosts for history entries in aTargetWinState
+ let cookieHosts = SessionCookies.getHostsForWindow(aTargetWinState);
+
+ // 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++;
+ }
+ },
+
+ _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() {
+ // 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 ssi_changeWindowStateBusyValue(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 ssi_setWindowStateReady(aWindow) {
+ let newCount = (this._windowBusyStates.get(aWindow) || 0) - 1;
+ if (newCount < 0) {
+ throw new Error("Invalid window busy state (less than zero).");
+ }
+ this._windowBusyStates.set(aWindow, newCount);
+
+ if (newCount == 0) {
+ this._setWindowStateBusyValue(aWindow, false);
+ this._sendWindowStateEvent(aWindow, "Ready");
+ }
+ },
+
+ /**
+ * Set the given window's state to 'busy'.
+ * @param aWindow the window
+ */
+ _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) {
+ let newCount = (this._windowBusyStates.get(aWindow) || 0) + 1;
+ this._windowBusyStates.set(aWindow, newCount);
+
+ if (newCount == 1) {
+ 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 ssi_sendWindowStateEvent(aWindow, aType) {
+ let event = aWindow.document.createEvent("Events");
+ event.initEvent("SSWindowState" + aType, true, false);
+ aWindow.dispatchEvent(event);
+ },
+
+ /**
+ * Dispatch the SSWindowRestored event for the given window.
+ * @param aWindow
+ * The window which has been restored
+ */
+ _sendWindowRestoredNotification(aWindow) {
+ let event = aWindow.document.createEvent("Events");
+ event.initEvent("SSWindowRestored", true, false);
+ aWindow.dispatchEvent(event);
+ },
+
+ /**
+ * Dispatch the SSTabRestored event for the given tab.
+ * @param aTab
+ * The tab which has been restored
+ * @param aIsRemotenessUpdate
+ * True if this tab was restored due to flip from running from
+ * out-of-main-process to in-main-process or vice-versa.
+ */
+ _sendTabRestoredNotification(aTab, aIsRemotenessUpdate) {
+ let event = aTab.ownerDocument.createEvent("CustomEvent");
+ event.initCustomEvent("SSTabRestored", true, false, {
+ isRemotenessUpdate: aIsRemotenessUpdate,
+ });
+ 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 ssi_isWindowLoaded(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 ssi_replaceLoadingTitle(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 ssi_capClosedWindows() {
+ if (this._closedWindows.length <= this._max_windows_undo)
+ return;
+ let spliceTo = this._max_windows_undo;
+ if (AppConstants.platform != "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;
+ }
+ this._closedWindows.splice(spliceTo, this._closedWindows.length);
+ },
+
+ /**
+ * Clears the set of windows that are "resurrected" before writing to disk to
+ * make closing windows one after the other until shutdown work as expected.
+ *
+ * This function should only be called when we are sure that there has been
+ * a user action that indicates the browser is actively being used and all
+ * windows that have been closed before are not part of a series of closing
+ * windows.
+ */
+ _clearRestoringWindows: function ssi_clearRestoringWindows() {
+ 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 ssi_initRestoringState() {
+ 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"
+ */
+ _resetLocalTabRestoringState: function (aTab) {
+ NS_ASSERT(aTab.linkedBrowser.__SS_restoreState,
+ "given tab is not restoring");
+
+ 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");
+
+ if (previousState == TAB_STATE_RESTORING) {
+ if (this._tabsRestoringCount)
+ this._tabsRestoringCount--;
+ } else if (previousState == TAB_STATE_NEEDS_RESTORE) {
+ // Make sure that the tab is removed from the list of tabs to restore.
+ // Again, this is normally done in restoreTabContent, but that isn't being called
+ // for this tab.
+ TabRestoreQueue.remove(aTab);
+ }
+ },
+
+ _resetTabRestoringState: function (tab) {
+ NS_ASSERT(tab.linkedBrowser.__SS_restoreState,
+ "given tab is not restoring");
+
+ let browser = tab.linkedBrowser;
+ browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
+ this._resetLocalTabRestoringState(tab);
+ },
+
+ /**
+ * Each fresh tab starts out with epoch=0. This function can be used to
+ * start a next epoch by incrementing the current value. It will enables us
+ * to ignore stale messages sent from previous epochs. The function returns
+ * the new epoch ID for the given |browser|.
+ */
+ startNextEpoch(browser) {
+ let next = this.getCurrentEpoch(browser) + 1;
+ this._browserEpochs.set(browser.permanentKey, next);
+ return next;
+ },
+
+ /**
+ * Returns the current epoch for the given <browser>. If we haven't assigned
+ * a new epoch this will default to zero for new tabs.
+ */
+ getCurrentEpoch(browser) {
+ return this._browserEpochs.get(browser.permanentKey) || 0;
+ },
+
+ /**
+ * Each time a <browser> element is restored, we increment its "epoch". To
+ * check if a message from content-sessionStore.js is out of date, we can
+ * compare the epoch received with the message to the <browser> element's
+ * epoch. This function does that, and returns true if |epoch| is up-to-date
+ * with respect to |browser|.
+ */
+ isCurrentEpoch: function (browser, epoch) {
+ return this.getCurrentEpoch(browser) == epoch;
+ },
+
+ /**
+ * Resets the epoch for a given <browser>. We need to this every time we
+ * receive a hint that a new docShell has been loaded into the browser as
+ * the frame script starts out with epoch=0.
+ */
+ resetEpoch(browser) {
+ this._browserEpochs.delete(browser.permanentKey);
+ },
+
+ /**
+ * Handle an error report from a content process.
+ */
+ reportInternalError(data) {
+ // For the moment, we only report errors through Telemetry.
+ if (data.telemetry) {
+ for (let key of Object.keys(data.telemetry)) {
+ let histogram = Telemetry.getHistogramById(key);
+ histogram.add(data.telemetry[key]);
+ }
+ }
+ },
+
+ /**
+ * Countdown for a given duration, skipping beats if the computer is too busy,
+ * sleeping or otherwise unavailable.
+ *
+ * @param {number} delay An approximate delay to wait in milliseconds (rounded
+ * up to the closest second).
+ *
+ * @return Promise
+ */
+ looseTimer(delay) {
+ let DELAY_BEAT = 1000;
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let beats = Math.ceil(delay / DELAY_BEAT);
+ let promise = new Promise(resolve => {
+ timer.initWithCallback(function() {
+ if (beats <= 0) {
+ resolve();
+ }
+ --beats;
+ }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
+ });
+ // Ensure that the timer is both canceled once we are done with it
+ // and not garbage-collected until then.
+ promise.then(() => timer.cancel(), () => timer.cancel());
+ return promise;
+ }
+};
+
+/**
+ * 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);
+ }
+ },
+
+ // 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);
+ }
+ },
+
+ /**
+ * Returns true if the passed tab is in one of the sets that we're
+ * restoring content in automatically.
+ *
+ * @param tab (<xul:tab>)
+ * The tab to check
+ * @returns bool
+ */
+ willRestoreSoon: function (tab) {
+ let { priority, hidden, visible } = this.tabs;
+ let { restoreOnDemand, restorePinnedTabsOnDemand,
+ restoreHiddenTabs } = this.prefs;
+ let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand);
+ let candidateSet = [];
+
+ if (restorePinned && priority.length)
+ candidateSet.push(...priority);
+
+ if (!restoreOnDemand) {
+ if (visible.length)
+ candidateSet.push(...visible);
+
+ if (restoreHiddenTabs && hidden.length)
+ candidateSet.push(...hidden);
+ }
+
+ return candidateSet.indexOf(tab) > -1;
+ },
+};
+
+// 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 weak set of dirty windows. We use it to determine which windows we need to
+// recollect data for when getCurrentState() is called.
+var DirtyWindows = {
+ _data: new WeakMap(),
+
+ has: function (window) {
+ return this._data.has(window);
+ },
+
+ add: function (window) {
+ return this._data.set(window, true);
+ },
+
+ remove: function (window) {
+ this._data.delete(window);
+ },
+
+ clear: function (window) {
+ this._data = new WeakMap();
+ }
+};
+
+// 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
+var LastSession = {
+ _state: null,
+
+ get canRestore() {
+ return !!this._state;
+ },
+
+ getState: function () {
+ return this._state;
+ },
+
+ setState: function (state) {
+ this._state = state;
+ },
+
+ clear: function () {
+ if (this._state) {
+ this._state = null;
+ Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null);
+ }
+ }
+};
diff --git a/browser/components/sessionstore/SessionWorker.js b/browser/components/sessionstore/SessionWorker.js
new file mode 100644
index 000000000..7d802a7df
--- /dev/null
+++ b/browser/components/sessionstore/SessionWorker.js
@@ -0,0 +1,381 @@
+/* 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/. */
+
+/**
+ * A worker dedicated to handle I/O for Session Store.
+ */
+
+"use strict";
+
+importScripts("resource://gre/modules/osfile.jsm");
+
+var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+
+var File = OS.File;
+var Encoder = new TextEncoder();
+var Decoder = new TextDecoder();
+
+var worker = new PromiseWorker.AbstractWorker();
+worker.dispatch = function(method, args = []) {
+ return Agent[method](...args);
+};
+worker.postMessage = function(result, ...transfers) {
+ self.postMessage(result, ...transfers);
+};
+worker.close = function() {
+ self.close();
+};
+
+self.addEventListener("message", msg => worker.handleMessage(msg));
+
+// The various possible states
+
+/**
+ * We just started (we haven't written anything to disk yet) from
+ * `Paths.clean`. The backup directory may not exist.
+ */
+const STATE_CLEAN = "clean";
+/**
+ * We know that `Paths.recovery` is good, either because we just read
+ * it (we haven't written anything to disk yet) or because have
+ * already written once to `Paths.recovery` during this session.
+ * `Paths.clean` is absent or invalid. The backup directory exists.
+ */
+const STATE_RECOVERY = "recovery";
+/**
+ * We just started from `Paths.recoverBackupy` (we haven't written
+ * anything to disk yet). Both `Paths.clean` and `Paths.recovery` are
+ * absent or invalid. The backup directory exists.
+ */
+const STATE_RECOVERY_BACKUP = "recoveryBackup";
+/**
+ * We just started from `Paths.upgradeBackup` (we haven't written
+ * anything to disk yet). Both `Paths.clean`, `Paths.recovery` and
+ * `Paths.recoveryBackup` are absent or invalid. The backup directory
+ * exists.
+ */
+const STATE_UPGRADE_BACKUP = "upgradeBackup";
+/**
+ * We just started without a valid session store file (we haven't
+ * written anything to disk yet). The backup directory may not exist.
+ */
+const STATE_EMPTY = "empty";
+
+var Agent = {
+ // Path to the files used by the SessionWorker
+ Paths: null,
+
+ /**
+ * The current state of the worker, as one of the following strings:
+ * - "permanent", once the first write has been completed;
+ * - "empty", before the first write has been completed,
+ * if we have started without any sessionstore;
+ * - one of "clean", "recovery", "recoveryBackup", "cleanBackup",
+ * "upgradeBackup", before the first write has been completed, if
+ * we have started by loading the corresponding file.
+ */
+ state: null,
+
+ /**
+ * Number of old upgrade backups that are being kept
+ */
+ maxUpgradeBackups: null,
+
+ /**
+ * Initialize (or reinitialize) the worker
+ *
+ * @param {string} origin Which of sessionstore.js or its backups
+ * was used. One of the `STATE_*` constants defined above.
+ * @param {object} paths The paths at which to find the various files.
+ * @param {object} prefs The preferences the worker needs to known.
+ */
+ init(origin, paths, prefs = {}) {
+ if (!(origin in paths || origin == STATE_EMPTY)) {
+ throw new TypeError("Invalid origin: " + origin);
+ }
+
+ // Check that all required preference values were passed.
+ for (let pref of ["maxUpgradeBackups", "maxSerializeBack", "maxSerializeForward"]) {
+ if (!prefs.hasOwnProperty(pref)) {
+ throw new TypeError(`Missing preference value for ${pref}`);
+ }
+ }
+
+ this.state = origin;
+ this.Paths = paths;
+ this.maxUpgradeBackups = prefs.maxUpgradeBackups;
+ this.maxSerializeBack = prefs.maxSerializeBack;
+ this.maxSerializeForward = prefs.maxSerializeForward;
+ this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup;
+ return {result: true};
+ },
+
+ /**
+ * Write the session to disk.
+ * Write the session to disk, performing any necessary backup
+ * along the way.
+ *
+ * @param {object} state The state to write to disk.
+ * @param {object} options
+ * - performShutdownCleanup If |true|, we should
+ * perform shutdown-time cleanup to ensure that private data
+ * is not left lying around;
+ * - isFinalWrite If |true|, write to Paths.clean instead of
+ * Paths.recovery
+ */
+ write: function (state, options = {}) {
+ let exn;
+ let telemetry = {};
+
+ // Cap the number of backward and forward shistory entries on shutdown.
+ if (options.isFinalWrite) {
+ for (let window of state.windows) {
+ for (let tab of window.tabs) {
+ let lower = 0;
+ let upper = tab.entries.length;
+
+ if (this.maxSerializeBack > -1) {
+ lower = Math.max(lower, tab.index - this.maxSerializeBack - 1);
+ }
+ if (this.maxSerializeForward > -1) {
+ upper = Math.min(upper, tab.index + this.maxSerializeForward);
+ }
+
+ tab.entries = tab.entries.slice(lower, upper);
+ tab.index -= lower;
+ }
+ }
+ }
+
+ let stateString = JSON.stringify(state);
+ let data = Encoder.encode(stateString);
+
+ try {
+
+ if (this.state == STATE_CLEAN || this.state == STATE_EMPTY) {
+ // The backups directory may not exist yet. In all other cases,
+ // we have either already read from or already written to this
+ // directory, so we are satisfied that it exists.
+ File.makeDir(this.Paths.backups);
+ }
+
+ if (this.state == STATE_CLEAN) {
+ // Move $Path.clean out of the way, to avoid any ambiguity as
+ // to which file is more recent.
+ File.move(this.Paths.clean, this.Paths.cleanBackup);
+ }
+
+ let startWriteMs = Date.now();
+
+ if (options.isFinalWrite) {
+ // We are shutting down. At this stage, we know that
+ // $Paths.clean is either absent or corrupted. If it was
+ // originally present and valid, it has been moved to
+ // $Paths.cleanBackup a long time ago. We can therefore write
+ // with the guarantees that we erase no important data.
+ File.writeAtomic(this.Paths.clean, data, {
+ tmpPath: this.Paths.clean + ".tmp"
+ });
+ } else if (this.state == STATE_RECOVERY) {
+ // At this stage, either $Paths.recovery was written >= 15
+ // seconds ago during this session or we have just started
+ // from $Paths.recovery left from the previous session. Either
+ // way, $Paths.recovery is good. We can move $Path.backup to
+ // $Path.recoveryBackup without erasing a good file with a bad
+ // file.
+ File.writeAtomic(this.Paths.recovery, data, {
+ tmpPath: this.Paths.recovery + ".tmp",
+ backupTo: this.Paths.recoveryBackup
+ });
+ } else {
+ // In other cases, either $Path.recovery is not necessary, or
+ // it doesn't exist or it has been corrupted. Regardless,
+ // don't backup $Path.recovery.
+ File.writeAtomic(this.Paths.recovery, data, {
+ tmpPath: this.Paths.recovery + ".tmp"
+ });
+ }
+
+ telemetry.FX_SESSION_RESTORE_WRITE_FILE_MS = Date.now() - startWriteMs;
+ telemetry.FX_SESSION_RESTORE_FILE_SIZE_BYTES = data.byteLength;
+
+ } catch (ex) {
+ // Don't throw immediately
+ exn = exn || ex;
+ }
+
+ // If necessary, perform an upgrade backup
+ let upgradeBackupComplete = false;
+ if (this.upgradeBackupNeeded
+ && (this.state == STATE_CLEAN || this.state == STATE_UPGRADE_BACKUP)) {
+ try {
+ // If we loaded from `clean`, the file has since then been renamed to `cleanBackup`.
+ let path = this.state == STATE_CLEAN ? this.Paths.cleanBackup : this.Paths.upgradeBackup;
+ File.copy(path, this.Paths.nextUpgradeBackup);
+ this.upgradeBackupNeeded = false;
+ upgradeBackupComplete = true;
+ } catch (ex) {
+ // Don't throw immediately
+ exn = exn || ex;
+ }
+
+ // Find all backups
+ let iterator;
+ let backups = []; // array that will contain the paths to all upgrade backup
+ let upgradeBackupPrefix = this.Paths.upgradeBackupPrefix; // access for forEach callback
+
+ try {
+ iterator = new File.DirectoryIterator(this.Paths.backups);
+ iterator.forEach(function (file) {
+ if (file.path.startsWith(upgradeBackupPrefix)) {
+ backups.push(file.path);
+ }
+ }, this);
+ } catch (ex) {
+ // Don't throw immediately
+ exn = exn || ex;
+ } finally {
+ if (iterator) {
+ iterator.close();
+ }
+ }
+
+ // If too many backups exist, delete them
+ if (backups.length > this.maxUpgradeBackups) {
+ // Use alphanumerical sort since dates are in YYYYMMDDHHMMSS format
+ backups.sort().forEach((file, i) => {
+ // remove backup file if it is among the first (n-maxUpgradeBackups) files
+ if (i < backups.length - this.maxUpgradeBackups) {
+ File.remove(file);
+ }
+ });
+ }
+ }
+
+ if (options.performShutdownCleanup && !exn) {
+
+ // During shutdown, if auto-restore is disabled, we need to
+ // remove possibly sensitive data that has been stored purely
+ // for crash recovery. Note that this slightly decreases our
+ // ability to recover from OS-level/hardware-level issue.
+
+ // If an exception was raised, we assume that we still need
+ // these files.
+ File.remove(this.Paths.recoveryBackup);
+ File.remove(this.Paths.recovery);
+ }
+
+ this.state = STATE_RECOVERY;
+
+ if (exn) {
+ throw exn;
+ }
+
+ return {
+ result: {
+ upgradeBackup: upgradeBackupComplete
+ },
+ telemetry: telemetry,
+ };
+ },
+
+ /**
+ * Wipes all files holding session data from disk.
+ */
+ wipe: function () {
+
+ // Don't stop immediately in case of error.
+ let exn = null;
+
+ // Erase main session state file
+ try {
+ File.remove(this.Paths.clean);
+ } catch (ex) {
+ // Don't stop immediately.
+ exn = exn || ex;
+ }
+
+ // Wipe the Session Restore directory
+ try {
+ this._wipeFromDir(this.Paths.backups, null);
+ } catch (ex) {
+ exn = exn || ex;
+ }
+
+ try {
+ File.removeDir(this.Paths.backups);
+ } catch (ex) {
+ exn = exn || ex;
+ }
+
+ // Wipe legacy Ression Restore files from the profile directory
+ try {
+ this._wipeFromDir(OS.Constants.Path.profileDir, "sessionstore.bak");
+ } catch (ex) {
+ exn = exn || ex;
+ }
+
+
+ this.state = STATE_EMPTY;
+ if (exn) {
+ throw exn;
+ }
+
+ return { result: true };
+ },
+
+ /**
+ * Wipe a number of files from a directory.
+ *
+ * @param {string} path The directory.
+ * @param {string|null} prefix If provided, only remove files whose
+ * name starts with a specific prefix.
+ */
+ _wipeFromDir: function(path, prefix) {
+ // Sanity check
+ if (typeof prefix == "undefined" || prefix == "") {
+ throw new TypeError();
+ }
+
+ let exn = null;
+
+ let iterator = new File.DirectoryIterator(path);
+ try {
+ if (!iterator.exists()) {
+ return;
+ }
+ for (let entry in iterator) {
+ if (entry.isDir) {
+ continue;
+ }
+ if (!prefix || entry.name.startsWith(prefix)) {
+ try {
+ File.remove(entry.path);
+ } catch (ex) {
+ // Don't stop immediately
+ exn = exn || ex;
+ }
+ }
+ }
+
+ if (exn) {
+ throw exn;
+ }
+ } finally {
+ iterator.close();
+ }
+ },
+};
+
+function isNoSuchFileEx(aReason) {
+ return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile;
+}
+
+/**
+ * Estimate the number of bytes that a data structure will use on disk
+ * once serialized.
+ */
+function getByteLength(str) {
+ return Encoder.encode(JSON.stringify(str)).byteLength;
+}
diff --git a/browser/components/sessionstore/SessionWorker.jsm b/browser/components/sessionstore/SessionWorker.jsm
new file mode 100644
index 000000000..b26e531ac
--- /dev/null
+++ b/browser/components/sessionstore/SessionWorker.jsm
@@ -0,0 +1,25 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Interface to a dedicated thread handling I/O
+ */
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+this.EXPORTED_SYMBOLS = ["SessionWorker"];
+
+this.SessionWorker = new BasePromiseWorker("resource:///modules/sessionstore/SessionWorker.js");
+// As the Session Worker performs I/O, we can receive instances of
+// OS.File.Error, so we need to install a decoder.
+this.SessionWorker.ExceptionHandlers["OS.File.Error"] = OS.File.Error.fromMsg;
+
diff --git a/browser/components/sessionstore/StartupPerformance.jsm b/browser/components/sessionstore/StartupPerformance.jsm
new file mode 100644
index 000000000..d1b77a237
--- /dev/null
+++ b/browser/components/sessionstore/StartupPerformance.jsm
@@ -0,0 +1,234 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["StartupPerformance"];
+
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+const COLLECT_RESULTS_AFTER_MS = 10000;
+
+const OBSERVED_TOPICS = ["sessionstore-restoring-on-startup", "sessionstore-initiating-manual-restore"];
+
+this.StartupPerformance = {
+ /**
+ * Once we have finished restoring initial tabs, we broadcast on this topic.
+ */
+ RESTORED_TOPIC: "sessionstore-finished-restoring-initial-tabs",
+
+ // Instant at which we have started restoration (notification "sessionstore-restoring-on-startup")
+ _startTimeStamp: null,
+
+ // Latest instant at which we have finished restoring a tab (DOM event "SSTabRestored")
+ _latestRestoredTimeStamp: null,
+
+ // A promise resolved once we have finished restoring all the startup tabs.
+ _promiseFinished: null,
+
+ // Function `resolve()` for `_promiseFinished`.
+ _resolveFinished: null,
+
+ // A timer
+ _deadlineTimer: null,
+
+ // `true` once the timer has fired
+ _hasFired: false,
+
+ // `true` once we are restored
+ _isRestored: false,
+
+ // Statistics on the session we need to restore.
+ _totalNumberOfEagerTabs: 0,
+ _totalNumberOfTabs: 0,
+ _totalNumberOfWindows: 0,
+
+ init: function() {
+ for (let topic of OBSERVED_TOPICS) {
+ Services.obs.addObserver(this, topic, false);
+ }
+ },
+
+ /**
+ * Return the timestamp at which we finished restoring the latest tab.
+ *
+ * This information is not really interesting until we have finished restoring
+ * tabs.
+ */
+ get latestRestoredTimeStamp() {
+ return this._latestRestoredTimeStamp;
+ },
+
+ /**
+ * `true` once we have finished restoring startup tabs.
+ */
+ get isRestored() {
+ return this._isRestored;
+ },
+
+ // Called when restoration starts.
+ // Record the start timestamp, setup the timer and `this._promiseFinished`.
+ // Behavior is unspecified if there was already an ongoing measure.
+ _onRestorationStarts: function(isAutoRestore) {
+ this._latestRestoredTimeStamp = this._startTimeStamp = Date.now();
+ this._totalNumberOfEagerTabs = 0;
+ this._totalNumberOfTabs = 0;
+ this._totalNumberOfWindows = 0;
+
+ // While we may restore several sessions in a single run of the browser,
+ // that's a very unusual case, and not really worth measuring, so let's
+ // stop listening for further restorations.
+
+ for (let topic of OBSERVED_TOPICS) {
+ Services.obs.removeObserver(this, topic);
+ }
+
+ Services.obs.addObserver(this, "sessionstore-single-window-restored", false);
+ this._promiseFinished = new Promise(resolve => {
+ this._resolveFinished = resolve;
+ });
+ this._promiseFinished.then(() => {
+ try {
+ this._isRestored = true;
+ Services.obs.notifyObservers(null, this.RESTORED_TOPIC, "");
+
+ if (this._latestRestoredTimeStamp == this._startTimeStamp) {
+ // Apparently, we haven't restored any tab.
+ return;
+ }
+
+ // Once we are done restoring tabs, update Telemetry.
+ let histogramName = isAutoRestore ?
+ "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS" :
+ "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS";
+ let histogram = Services.telemetry.getHistogramById(histogramName);
+ let delta = this._latestRestoredTimeStamp - this._startTimeStamp;
+ histogram.add(delta);
+
+ Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED").add(this._totalNumberOfEagerTabs);
+ Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED").add(this._totalNumberOfTabs);
+ Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED").add(this._totalNumberOfWindows);
+
+ // Reset
+ this._startTimeStamp = null;
+ } catch (ex) {
+ console.error("StartupPerformance: error after resolving promise", ex);
+ }
+ });
+ },
+
+ _startTimer: function() {
+ if (this._hasFired) {
+ return;
+ }
+ if (this._deadlineTimer) {
+ clearTimeout(this._deadlineTimer);
+ }
+ this._deadlineTimer = setTimeout(() => {
+ try {
+ this._resolveFinished();
+ } catch (ex) {
+ console.error("StartupPerformance: Error in timeout handler", ex);
+ } finally {
+ // Clean up.
+ this._deadlineTimer = null;
+ this._hasFired = true;
+ this._resolveFinished = null;
+ Services.obs.removeObserver(this, "sessionstore-single-window-restored");
+ }
+ }, COLLECT_RESULTS_AFTER_MS);
+ },
+
+ observe: function(subject, topic, details) {
+ try {
+ switch (topic) {
+ case "sessionstore-restoring-on-startup":
+ this._onRestorationStarts(true);
+ break;
+ case "sessionstore-initiating-manual-restore":
+ this._onRestorationStarts(false);
+ break;
+ case "sessionstore-single-window-restored": {
+ // Session Restore has just opened a window with (initially empty) tabs.
+ // Some of these tabs will be restored eagerly, while others will be
+ // restored on demand. The process becomes usable only when all windows
+ // have finished restored their eager tabs.
+ //
+ // While it would be possible to track the restoration of each tab
+ // from within SessionRestore to determine exactly when the process
+ // becomes usable, experience shows that this is too invasive. Rather,
+ // we employ the following heuristic:
+ // - we maintain a timer of `COLLECT_RESULTS_AFTER_MS` that we expect
+ // will be triggered only once all tabs have been restored;
+ // - whenever we restore a new window (hence a bunch of eager tabs),
+ // we postpone the timer to ensure that the new eager tabs have
+ // `COLLECT_RESULTS_AFTER_MS` to be restored;
+ // - whenever a tab is restored, we update
+ // `this._latestRestoredTimeStamp`;
+ // - after `COLLECT_RESULTS_AFTER_MS`, we collect the final version
+ // of `this._latestRestoredTimeStamp`, and use it to determine the
+ // entire duration of the collection.
+ //
+ // Note that this heuristic may be inaccurate if a user clicks
+ // immediately on a restore-on-demand tab before the end of
+ // `COLLECT_RESULTS_AFTER_MS`. We assume that this will not
+ // affect too much the results.
+ //
+ // Reset the delay, to give the tabs a little (more) time to restore.
+ this._startTimer();
+
+ this._totalNumberOfWindows += 1;
+
+ // Observe the restoration of all tabs. We assume that all tabs of this
+ // window will have been restored before `COLLECT_RESULTS_AFTER_MS`.
+ // The last call to `observer` will let us determine how long it took
+ // to reach that point.
+ let win = subject;
+
+ let observer = (event) => {
+ // We don't care about tab restorations that are due to
+ // a browser flipping from out-of-main-process to in-main-process
+ // or vice-versa. We only care about restorations that are due
+ // to the user switching to a lazily restored tab, or for tabs
+ // that are restoring eagerly.
+ if (!event.detail.isRemotenessUpdate) {
+ this._latestRestoredTimeStamp = Date.now();
+ this._totalNumberOfEagerTabs += 1;
+ }
+ };
+ win.gBrowser.tabContainer.addEventListener("SSTabRestored", observer);
+ this._totalNumberOfTabs += win.gBrowser.tabContainer.itemCount;
+
+ // Once we have finished collecting the results, clean up the observers.
+ this._promiseFinished.then(() => {
+ if (!win.gBrowser.tabContainer) {
+ // May be undefined during shutdown and/or some tests.
+ return;
+ }
+ win.gBrowser.tabContainer.removeEventListener("SSTabRestored", observer);
+ });
+ }
+ break;
+ default:
+ throw new Error(`Unexpected topic ${topic}`);
+ }
+ } catch (ex) {
+ console.error("StartupPerformance error", ex, ex.stack);
+ throw ex;
+ }
+ }
+};
diff --git a/browser/components/sessionstore/TabAttributes.jsm b/browser/components/sessionstore/TabAttributes.jsm
new file mode 100644
index 000000000..8a29680f4
--- /dev/null
+++ b/browser/components/sessionstore/TabAttributes.jsm
@@ -0,0 +1,74 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["TabAttributes"];
+
+// We never want to directly read or write these attributes.
+// 'image' should not be accessed directly but handled by using the
+// gBrowser.getIcon()/setIcon() methods.
+// 'muted' should not be accessed directly but handled by using the
+// tab.linkedBrowser.audioMuted/toggleMuteAudio methods.
+// 'pending' is used internal by sessionstore and managed accordingly.
+// 'iconLoadingPrincipal' is same as 'image' that it should be handled by
+// using the gBrowser.getIcon()/setIcon() methods.
+const ATTRIBUTES_TO_SKIP = new Set(["image", "muted", "pending", "iconLoadingPrincipal"]);
+
+// 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.
+this.TabAttributes = Object.freeze({
+ persist: function (name) {
+ return TabAttributesInternal.persist(name);
+ },
+
+ get: function (tab) {
+ return TabAttributesInternal.get(tab);
+ },
+
+ set: function (tab, data = {}) {
+ TabAttributesInternal.set(tab, data);
+ }
+});
+
+var TabAttributesInternal = {
+ _attrs: new Set(),
+
+ persist: function (name) {
+ if (this._attrs.has(name) || ATTRIBUTES_TO_SKIP.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) {
+ if (!ATTRIBUTES_TO_SKIP.has(name)) {
+ tab.setAttribute(name, data[name]);
+ }
+ }
+ }
+};
+
diff --git a/browser/components/sessionstore/TabState.jsm b/browser/components/sessionstore/TabState.jsm
new file mode 100644
index 000000000..f22c52fe3
--- /dev/null
+++ b/browser/components/sessionstore/TabState.jsm
@@ -0,0 +1,196 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["TabState"];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
+ "resource:///modules/sessionstore/PrivacyFilter.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
+ "resource:///modules/sessionstore/TabStateCache.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
+ "resource:///modules/sessionstore/TabAttributes.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+ "resource://gre/modules/sessionstore/Utils.jsm");
+
+/**
+ * Module that contains tab state collection methods.
+ */
+this.TabState = Object.freeze({
+ update: function (browser, data) {
+ TabStateInternal.update(browser, data);
+ },
+
+ collect: function (tab) {
+ return TabStateInternal.collect(tab);
+ },
+
+ clone: function (tab) {
+ return TabStateInternal.clone(tab);
+ },
+
+ copyFromCache(browser, tabData, options) {
+ TabStateInternal.copyFromCache(browser, tabData, options);
+ },
+});
+
+var TabStateInternal = {
+ /**
+ * Processes a data update sent by the content script.
+ */
+ update: function (browser, {data}) {
+ TabStateCache.update(browser, data);
+ },
+
+ /**
+ * Collect data related to a single tab, synchronously.
+ *
+ * @param tab
+ * tabbrowser tab
+ *
+ * @returns {TabData} An object with the data for this tab. If the
+ * tab has not been invalidated since the last call to
+ * collect(aTab), the same object is returned.
+ */
+ collect: function (tab) {
+ return this._collectBaseTabData(tab);
+ },
+
+ /**
+ * Collect data related to a single tab, including private data.
+ * Use with caution.
+ *
+ * @param tab
+ * tabbrowser tab
+ *
+ * @returns {object} An object with the data for this tab. This data is never
+ * cached, it will always be read from the tab and thus be
+ * up-to-date.
+ */
+ clone: function (tab) {
+ return this._collectBaseTabData(tab, {includePrivateData: true});
+ },
+
+ /**
+ * Collects basic tab data for a given tab.
+ *
+ * @param tab
+ * tabbrowser tab
+ * @param options (object)
+ * {includePrivateData: true} to always include private data
+ *
+ * @returns {object} An object with the basic data for this tab.
+ */
+ _collectBaseTabData: function (tab, options) {
+ let tabData = { entries: [], lastAccessed: tab.lastAccessed };
+ let browser = tab.linkedBrowser;
+
+ if (tab.pinned) {
+ tabData.pinned = true;
+ }
+
+ tabData.hidden = tab.hidden;
+
+ if (browser.audioMuted) {
+ tabData.muted = true;
+ tabData.muteReason = tab.muteReason;
+ }
+
+ // Save tab attributes.
+ tabData.attributes = TabAttributes.get(tab);
+
+ if (tab.__SS_extdata) {
+ tabData.extData = tab.__SS_extdata;
+ }
+
+ // Copy data from the tab state cache only if the tab has fully finished
+ // restoring. We don't want to overwrite data contained in __SS_data.
+ this.copyFromCache(browser, tabData, options);
+
+ // After copyFromCache() was called we check for properties that are kept
+ // in the cache only while the tab is pending or restoring. Once that
+ // happened those properties will be removed from the cache and will
+ // be read from the tab/browser every time we collect data.
+
+ // Store the tab icon.
+ if (!("image" in tabData)) {
+ let tabbrowser = tab.ownerGlobal.gBrowser;
+ tabData.image = tabbrowser.getIcon(tab);
+ }
+
+ // Store the serialized contentPrincipal of this tab to use for the icon.
+ if (!("iconLoadingPrincipal" in tabData)) {
+ tabData.iconLoadingPrincipal = Utils.serializePrincipal(browser.contentPrincipal);
+ }
+
+ // 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.
+ // If so, we also track whether we were still in the process of loading something.
+ if (!("userTypedValue" in tabData) && 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;
+ }
+
+ return tabData;
+ },
+
+ /**
+ * Copy data for the given |browser| from the cache to |tabData|.
+ *
+ * @param browser (xul:browser)
+ * The browser belonging to the given |tabData| object.
+ * @param tabData (object)
+ * The tab data belonging to the given |tab|.
+ * @param options (object)
+ * {includePrivateData: true} to always include private data
+ */
+ copyFromCache(browser, tabData, options = {}) {
+ let data = TabStateCache.get(browser);
+ if (!data) {
+ return;
+ }
+
+ // The caller may explicitly request to omit privacy checks.
+ let includePrivateData = options && options.includePrivateData;
+ let isPinned = !!tabData.pinned;
+
+ for (let key of Object.keys(data)) {
+ let value = data[key];
+
+ // Filter sensitive data according to the current privacy level.
+ if (!includePrivateData) {
+ if (key === "storage") {
+ value = PrivacyFilter.filterSessionStorageData(value);
+ } else if (key === "formdata") {
+ value = PrivacyFilter.filterFormData(value);
+ }
+ }
+
+ if (key === "history") {
+ tabData.entries = value.entries;
+
+ if (value.hasOwnProperty("userContextId")) {
+ tabData.userContextId = value.userContextId;
+ }
+
+ if (value.hasOwnProperty("index")) {
+ tabData.index = value.index;
+ }
+ } else {
+ tabData[key] = value;
+ }
+ }
+ }
+};
diff --git a/browser/components/sessionstore/TabStateCache.jsm b/browser/components/sessionstore/TabStateCache.jsm
new file mode 100644
index 000000000..9bed315a0
--- /dev/null
+++ b/browser/components/sessionstore/TabStateCache.jsm
@@ -0,0 +1,163 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["TabStateCache"];
+
+/**
+ * A cache for tabs data.
+ *
+ * This cache implements a weak map from tabs (as XUL elements)
+ * to tab data (as objects).
+ *
+ * Note that we should never cache private data, as:
+ * - that data is used very seldom by SessionStore;
+ * - caching private data in addition to public data is memory consuming.
+ */
+this.TabStateCache = Object.freeze({
+ /**
+ * Retrieves cached data for a given |tab| or associated |browser|.
+ *
+ * @param browserOrTab (xul:tab or xul:browser)
+ * The tab or browser to retrieve cached data for.
+ * @return (object)
+ * The cached data stored for the given |tab|
+ * or associated |browser|.
+ */
+ get: function (browserOrTab) {
+ return TabStateCacheInternal.get(browserOrTab);
+ },
+
+ /**
+ * Updates cached data for a given |tab| or associated |browser|.
+ *
+ * @param browserOrTab (xul:tab or xul:browser)
+ * The tab or browser belonging to the given tab data.
+ * @param newData (object)
+ * The new data to be stored for the given |tab|
+ * or associated |browser|.
+ */
+ update: function (browserOrTab, newData) {
+ TabStateCacheInternal.update(browserOrTab, newData);
+ }
+});
+
+var TabStateCacheInternal = {
+ _data: new WeakMap(),
+
+ /**
+ * Retrieves cached data for a given |tab| or associated |browser|.
+ *
+ * @param browserOrTab (xul:tab or xul:browser)
+ * The tab or browser to retrieve cached data for.
+ * @return (object)
+ * The cached data stored for the given |tab|
+ * or associated |browser|.
+ */
+ get: function (browserOrTab) {
+ return this._data.get(browserOrTab.permanentKey);
+ },
+
+ /**
+ * Helper function used by update (see below). For message size
+ * optimization sometimes we don't update the whole session storage
+ * only the values that have been changed.
+ *
+ * @param data (object)
+ * The cached data where we want to update the changes.
+ * @param change (object)
+ * The actual changed values per domain.
+ */
+ updatePartialStorageChange: function (data, change) {
+ if (!data.storage) {
+ data.storage = {};
+ }
+
+ let storage = data.storage;
+ for (let domain of Object.keys(change)) {
+ for (let key of Object.keys(change[domain])) {
+ let value = change[domain][key];
+ if (value === null) {
+ if (storage[domain] && storage[domain][key]) {
+ delete storage[domain][key];
+ }
+ } else {
+ if (!storage[domain]) {
+ storage[domain] = {};
+ }
+ storage[domain][key] = value;
+ }
+ }
+ }
+ },
+
+ /**
+ * Helper function used by update (see below). For message size
+ * optimization sometimes we don't update the whole browser history
+ * only the current index and the tail of the history from a certain
+ * index (specified by change.fromIdx)
+ *
+ * @param data (object)
+ * The cached data where we want to update the changes.
+ * @param change (object)
+ * Object containing the tail of the history array, and
+ * some additional metadata.
+ */
+ updatePartialHistoryChange: function (data, change) {
+ const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
+
+ if (!data.history) {
+ data.history = { entries: [] };
+ }
+
+ let history = data.history;
+ for (let key of Object.keys(change)) {
+ if (key == "entries") {
+ if (change.fromIdx != kLastIndex) {
+ history.entries.splice(change.fromIdx + 1);
+ while (change.entries.length) {
+ history.entries.push(change.entries.shift());
+ }
+ }
+ } else if (key != "fromIndex") {
+ history[key] = change[key];
+ }
+ }
+ },
+
+ /**
+ * Updates cached data for a given |tab| or associated |browser|.
+ *
+ * @param browserOrTab (xul:tab or xul:browser)
+ * The tab or browser belonging to the given tab data.
+ * @param newData (object)
+ * The new data to be stored for the given |tab|
+ * or associated |browser|.
+ */
+ update: function (browserOrTab, newData) {
+ let data = this._data.get(browserOrTab.permanentKey) || {};
+
+ for (let key of Object.keys(newData)) {
+ if (key == "storagechange") {
+ this.updatePartialStorageChange(data, newData.storagechange);
+ continue;
+ }
+
+ if (key == "historychange") {
+ this.updatePartialHistoryChange(data, newData.historychange);
+ continue;
+ }
+
+ let value = newData[key];
+ if (value === null) {
+ delete data[key];
+ } else {
+ data[key] = value;
+ }
+ }
+
+ this._data.set(browserOrTab.permanentKey, data);
+ }
+};
diff --git a/browser/components/sessionstore/TabStateFlusher.jsm b/browser/components/sessionstore/TabStateFlusher.jsm
new file mode 100644
index 000000000..6397efe9d
--- /dev/null
+++ b/browser/components/sessionstore/TabStateFlusher.jsm
@@ -0,0 +1,184 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["TabStateFlusher"];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Promise.jsm", this);
+
+/**
+ * A module that enables async flushes. Updates from frame scripts are
+ * throttled to be sent only once per second. If an action wants a tab's latest
+ * state without waiting for a second then it can request an async flush and
+ * wait until the frame scripts reported back. At this point the parent has the
+ * latest data and the action can continue.
+ */
+this.TabStateFlusher = Object.freeze({
+ /**
+ * Requests an async flush for the given browser. Returns a promise that will
+ * resolve when we heard back from the content process and the parent has
+ * all the latest data.
+ */
+ flush(browser) {
+ return TabStateFlusherInternal.flush(browser);
+ },
+
+ /**
+ * Requests an async flush for all browsers of a given window. Returns a Promise
+ * that will resolve when we've heard back from all browsers.
+ */
+ flushWindow(window) {
+ return TabStateFlusherInternal.flushWindow(window);
+ },
+
+ /**
+ * Resolves the flush request with the given flush ID.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which the flush is being resolved.
+ * @param flushID (int)
+ * The ID of the flush that was sent to the browser.
+ * @param success (bool, optional)
+ * Whether or not the flush succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that a flush failed.
+ */
+ resolve(browser, flushID, success=true, message="") {
+ TabStateFlusherInternal.resolve(browser, flushID, success, message);
+ },
+
+ /**
+ * Resolves all active flush requests for a given browser. This should be
+ * used when the content process crashed or the final update message was
+ * seen. In those cases we can't guarantee to ever hear back from the frame
+ * script so we just resolve all requests instead of discarding them.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which all flushes are being resolved.
+ * @param success (bool, optional)
+ * Whether or not the flushes succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that the flushes failed.
+ */
+ resolveAll(browser, success=true, message="") {
+ TabStateFlusherInternal.resolveAll(browser, success, message);
+ }
+});
+
+var TabStateFlusherInternal = {
+ // Stores the last request ID.
+ _lastRequestID: 0,
+
+ // A map storing all active requests per browser.
+ _requests: new WeakMap(),
+
+ /**
+ * Requests an async flush for the given browser. Returns a promise that will
+ * resolve when we heard back from the content process and the parent has
+ * all the latest data.
+ */
+ flush(browser) {
+ let id = ++this._lastRequestID;
+ let mm = browser.messageManager;
+ mm.sendAsyncMessage("SessionStore:flush", {id});
+
+ // Retrieve active requests for given browser.
+ let permanentKey = browser.permanentKey;
+ let perBrowserRequests = this._requests.get(permanentKey) || new Map();
+
+ return new Promise(resolve => {
+ // Store resolve() so that we can resolve the promise later.
+ perBrowserRequests.set(id, resolve);
+
+ // Update the flush requests stored per browser.
+ this._requests.set(permanentKey, perBrowserRequests);
+ });
+ },
+
+ /**
+ * Requests an async flush for all browsers of a given window. Returns a Promise
+ * that will resolve when we've heard back from all browsers.
+ */
+ flushWindow(window) {
+ let browsers = window.gBrowser.browsers;
+ let promises = browsers.map((browser) => this.flush(browser));
+ return Promise.all(promises);
+ },
+
+ /**
+ * Resolves the flush request with the given flush ID.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which the flush is being resolved.
+ * @param flushID (int)
+ * The ID of the flush that was sent to the browser.
+ * @param success (bool, optional)
+ * Whether or not the flush succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that a flush failed.
+ */
+ resolve(browser, flushID, success=true, message="") {
+ // Nothing to do if there are no pending flushes for the given browser.
+ if (!this._requests.has(browser.permanentKey)) {
+ return;
+ }
+
+ // Retrieve active requests for given browser.
+ let perBrowserRequests = this._requests.get(browser.permanentKey);
+ if (!perBrowserRequests.has(flushID)) {
+ return;
+ }
+
+ if (!success) {
+ Cu.reportError("Failed to flush browser: " + message);
+ }
+
+ // Resolve the request with the given id.
+ let resolve = perBrowserRequests.get(flushID);
+ perBrowserRequests.delete(flushID);
+ resolve(success);
+ },
+
+ /**
+ * Resolves all active flush requests for a given browser. This should be
+ * used when the content process crashed or the final update message was
+ * seen. In those cases we can't guarantee to ever hear back from the frame
+ * script so we just resolve all requests instead of discarding them.
+ *
+ * @param browser (<xul:browser>)
+ * The browser for which all flushes are being resolved.
+ * @param success (bool, optional)
+ * Whether or not the flushes succeeded.
+ * @param message (string, optional)
+ * An error message that will be sent to the Console in the
+ * event that the flushes failed.
+ */
+ resolveAll(browser, success=true, message="") {
+ // Nothing to do if there are no pending flushes for the given browser.
+ if (!this._requests.has(browser.permanentKey)) {
+ return;
+ }
+
+ // Retrieve active requests for given browser.
+ let perBrowserRequests = this._requests.get(browser.permanentKey);
+
+ if (!success) {
+ Cu.reportError("Failed to flush browser: " + message);
+ }
+
+ // Resolve all requests.
+ for (let resolve of perBrowserRequests.values()) {
+ resolve(success);
+ }
+
+ // Clear active requests.
+ perBrowserRequests.clear();
+ }
+};
diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js
new file mode 100644
index 000000000..cc8d2da0b
--- /dev/null
+++ b/browser/components/sessionstore/content/aboutSessionRestore.js
@@ -0,0 +1,362 @@
+/* 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/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+
+var gStateObject;
+var gTreeData;
+
+// Page initialization
+
+window.onload = function() {
+ // pages used by this script may have a link that needs to be updated to
+ // the in-product link.
+ let anchor = document.getElementById("linkMoreTroubleshooting");
+ if (anchor) {
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ anchor.setAttribute("href", baseURL + "troubleshooting");
+ }
+
+ // wire up click handlers for the radio buttons if they exist.
+ for (let radioId of ["radioRestoreAll", "radioRestoreChoose"]) {
+ let button = document.getElementById(radioId);
+ if (button) {
+ button.addEventListener("click", updateTabListVisibility);
+ }
+ }
+
+ // the crashed session state is kept inside a textbox so that SessionStore picks it up
+ // (for when the tab is closed or the session crashes right again)
+ var sessionData = document.getElementById("sessionData");
+ if (!sessionData.value) {
+ document.getElementById("errorTryAgain").disabled = true;
+ return;
+ }
+
+ gStateObject = JSON.parse(sessionData.value);
+
+ // make sure the data is tracked to be restored in case of a subsequent crash
+ var event = document.createEvent("UIEvents");
+ event.initUIEvent("input", true, true, window, 0);
+ sessionData.dispatchEvent(event);
+
+ initTreeView();
+
+ document.getElementById("errorTryAgain").focus();
+};
+
+function isTreeViewVisible() {
+ let tabList = document.querySelector(".tree-container");
+ return tabList.hasAttribute("available");
+}
+
+function initTreeView() {
+ // If we aren't visible we initialize as we are made visible (and it's OK
+ // to initialize multiple times)
+ if (!isTreeViewVisible()) {
+ return;
+ }
+ var tabList = document.getElementById("tabList");
+ var winLabel = tabList.getAttribute("_window_label");
+
+ gTreeData = [];
+ gStateObject.windows.forEach(function(aWinData, aIx) {
+ var winState = {
+ label: winLabel.replace("%S", (aIx + 1)),
+ open: true,
+ checked: true,
+ ix: aIx
+ };
+ winState.tabs = aWinData.tabs.map(function(aTabData) {
+ var entry = aTabData.entries[aTabData.index - 1] || { url: "about:blank" };
+ var iconURL = aTabData.image || null;
+ // don't initiate a connection just to fetch a favicon (see bug 462863)
+ if (/^https?:/.test(iconURL))
+ iconURL = "moz-anno:favicon:" + iconURL;
+ return {
+ label: entry.title || entry.url,
+ checked: true,
+ src: iconURL,
+ parent: winState
+ };
+ });
+ gTreeData.push(winState);
+ for (let tab of winState.tabs)
+ gTreeData.push(tab);
+ }, this);
+
+ tabList.view = treeView;
+ tabList.view.selection.select(0);
+}
+
+// User actions
+function updateTabListVisibility() {
+ let tabList = document.querySelector(".tree-container");
+ let container = document.querySelector(".container");
+ if (document.getElementById("radioRestoreChoose").checked) {
+ tabList.setAttribute("available", "true");
+ container.classList.add("restore-chosen");
+ } else {
+ tabList.removeAttribute("available");
+ container.classList.remove("restore-chosen");
+ }
+ initTreeView();
+}
+
+function restoreSession() {
+ Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore", "");
+ document.getElementById("errorTryAgain").disabled = true;
+
+ if (isTreeViewVisible()) {
+ if (!gTreeData.some(aItem => aItem.checked)) {
+ // This should only be possible when we have no "cancel" button, and thus
+ // the "Restore session" button always remains enabled. In that case and
+ // when nothing is selected, we just want a new session.
+ startNewSession();
+ return;
+ }
+
+ // remove all unselected tabs from the state before restoring it
+ var ix = gStateObject.windows.length - 1;
+ for (var t = gTreeData.length - 1; t >= 0; t--) {
+ if (treeView.isContainer(t)) {
+ if (gTreeData[t].checked === 0)
+ // this window will be restored partially
+ gStateObject.windows[ix].tabs =
+ gStateObject.windows[ix].tabs.filter((aTabData, aIx) =>
+ gTreeData[t].tabs[aIx].checked);
+ else if (!gTreeData[t].checked)
+ // this window won't be restored at all
+ gStateObject.windows.splice(ix, 1);
+ ix--;
+ }
+ }
+ }
+ var stateString = JSON.stringify(gStateObject);
+
+ var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ var top = getBrowserWindow();
+
+ // if there's only this page open, reuse the window for restoring the session
+ if (top.gBrowser.tabs.length == 1) {
+ ss.setWindowState(top, stateString, true);
+ return;
+ }
+
+ // restore the session into a new window and close the current tab
+ var newWindow = top.openDialog(top.location, "_blank", "chrome,dialog=no,all");
+
+ var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+ obs.addObserver(function observe(win, topic) {
+ if (win != newWindow) {
+ return;
+ }
+
+ obs.removeObserver(observe, topic);
+ ss.setWindowState(newWindow, stateString, true);
+
+ var tabbrowser = top.gBrowser;
+ var tabIndex = tabbrowser.getBrowserIndexForDocument(document);
+ tabbrowser.removeTab(tabbrowser.tabs[tabIndex]);
+ }, "browser-delayed-startup-finished", false);
+}
+
+function startNewSession() {
+ var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ if (prefBranch.getIntPref("browser.startup.page") == 0)
+ getBrowserWindow().gBrowser.loadURI("about:blank");
+ else
+ getBrowserWindow().BrowserHome();
+}
+
+function onListClick(aEvent) {
+ // don't react to right-clicks
+ if (aEvent.button == 2)
+ return;
+
+ var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.col) {
+ // Restore this specific tab in the same window for middle/double/accel clicking
+ // on a tab's title.
+ let accelKey = AppConstants.platform == "macosx" ?
+ aEvent.metaKey :
+ aEvent.ctrlKey;
+ if ((aEvent.button == 1 || aEvent.button == 0 && aEvent.detail == 2 || accelKey) &&
+ cell.col.id == "title" &&
+ !treeView.isContainer(cell.row)) {
+ restoreSingleTab(cell.row, aEvent.shiftKey);
+ aEvent.stopPropagation();
+ }
+ else if (cell.col.id == "restore")
+ toggleRowChecked(cell.row);
+ }
+}
+
+function onListKeyDown(aEvent) {
+ switch (aEvent.keyCode)
+ {
+ case KeyEvent.DOM_VK_SPACE:
+ toggleRowChecked(document.getElementById("tabList").currentIndex);
+ // Prevent page from scrolling on the space key.
+ aEvent.preventDefault();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ var ix = document.getElementById("tabList").currentIndex;
+ if (aEvent.ctrlKey && !treeView.isContainer(ix))
+ restoreSingleTab(ix, aEvent.shiftKey);
+ break;
+ }
+}
+
+// Helper functions
+
+function getBrowserWindow() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+}
+
+function toggleRowChecked(aIx) {
+ function isChecked(aItem) {
+ return aItem.checked;
+ }
+
+ var item = gTreeData[aIx];
+ item.checked = !item.checked;
+ treeView.treeBox.invalidateRow(aIx);
+
+ if (treeView.isContainer(aIx)) {
+ // (un)check all tabs of this window as well
+ for (let tab of item.tabs) {
+ tab.checked = item.checked;
+ treeView.treeBox.invalidateRow(gTreeData.indexOf(tab));
+ }
+ }
+ else {
+ // update the window's checkmark as well (0 means "partially checked")
+ item.parent.checked = item.parent.tabs.every(isChecked) ? true :
+ item.parent.tabs.some(isChecked) ? 0 : false;
+ treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent));
+ }
+
+ // we only disable the button when there's no cancel button.
+ if (document.getElementById("errorCancel")) {
+ document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked);
+ }
+}
+
+function restoreSingleTab(aIx, aShifted) {
+ var tabbrowser = getBrowserWindow().gBrowser;
+ var newTab = tabbrowser.addTab();
+ var item = gTreeData[aIx];
+
+ var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ var tabState = gStateObject.windows[item.parent.ix]
+ .tabs[aIx - gTreeData.indexOf(item.parent) - 1];
+ // ensure tab would be visible on the tabstrip.
+ tabState.hidden = false;
+ ss.setTabState(newTab, JSON.stringify(tabState));
+
+ // respect the preference as to whether to select the tab (the Shift key inverses)
+ var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ if (prefBranch.getBoolPref("browser.tabs.loadInBackground") != !aShifted)
+ tabbrowser.selectedTab = newTab;
+}
+
+// Tree controller
+
+var treeView = {
+ treeBox: null,
+ selection: null,
+
+ get rowCount() { return gTreeData.length; },
+ setTree: function(treeBox) { this.treeBox = treeBox; },
+ getCellText: function(idx, column) { return gTreeData[idx].label; },
+ isContainer: function(idx) { return "open" in gTreeData[idx]; },
+ getCellValue: function(idx, column){ return gTreeData[idx].checked; },
+ isContainerOpen: function(idx) { return gTreeData[idx].open; },
+ isContainerEmpty: function(idx) { return false; },
+ isSeparator: function(idx) { return false; },
+ isSorted: function() { return false; },
+ isEditable: function(idx, column) { return false; },
+ canDrop: function(idx, orientation, dt) { return false; },
+ getLevel: function(idx) { return this.isContainer(idx) ? 0 : 1; },
+
+ getParentIndex: function(idx) {
+ if (!this.isContainer(idx))
+ for (var t = idx - 1; t >= 0 ; t--)
+ if (this.isContainer(t))
+ return t;
+ return -1;
+ },
+
+ hasNextSibling: function(idx, after) {
+ var thisLevel = this.getLevel(idx);
+ for (var t = after + 1; t < gTreeData.length; t++)
+ if (this.getLevel(t) <= thisLevel)
+ return this.getLevel(t) == thisLevel;
+ return false;
+ },
+
+ toggleOpenState: function(idx) {
+ if (!this.isContainer(idx))
+ return;
+ var item = gTreeData[idx];
+ if (item.open) {
+ // remove this window's tab rows from the view
+ var thisLevel = this.getLevel(idx);
+ for (var t = idx + 1; t < gTreeData.length && this.getLevel(t) > thisLevel; t++);
+ var deletecount = t - idx - 1;
+ gTreeData.splice(idx + 1, deletecount);
+ this.treeBox.rowCountChanged(idx + 1, -deletecount);
+ }
+ else {
+ // add this window's tab rows to the view
+ var toinsert = gTreeData[idx].tabs;
+ for (var i = 0; i < toinsert.length; i++)
+ gTreeData.splice(idx + i + 1, 0, toinsert[i]);
+ this.treeBox.rowCountChanged(idx + 1, toinsert.length);
+ }
+ item.open = !item.open;
+ this.treeBox.invalidateRow(idx);
+ },
+
+ getCellProperties: function(idx, column) {
+ if (column.id == "restore" && this.isContainer(idx) && gTreeData[idx].checked === 0)
+ return "partial";
+ if (column.id == "title")
+ return this.getImageSrc(idx, column) ? "icon" : "noicon";
+
+ return "";
+ },
+
+ getRowProperties: function(idx) {
+ var winState = gTreeData[idx].parent || gTreeData[idx];
+ if (winState.ix % 2 != 0)
+ return "alternate";
+
+ return "";
+ },
+
+ getImageSrc: function(idx, column) {
+ if (column.id == "title")
+ return gTreeData[idx].src || null;
+ return null;
+ },
+
+ getProgressMode : function(idx, column) { },
+ cycleHeader: function(column) { },
+ cycleCell: function(idx, column) { },
+ selectionChanged: function() { },
+ performAction: function(action) { },
+ performActionOnCell: function(action, index, column) { },
+ getColumnProperties: function(column) { return ""; }
+};
diff --git a/browser/components/sessionstore/content/aboutSessionRestore.xhtml b/browser/components/sessionstore/content/aboutSessionRestore.xhtml
new file mode 100644
index 000000000..bcd9084e7
--- /dev/null
+++ b/browser/components/sessionstore/content/aboutSessionRestore.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# 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/.
+-->
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % restorepageDTD SYSTEM "chrome://browser/locale/aboutSessionRestore.dtd">
+ %restorepageDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <title>&restorepage.tabtitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css" media="all"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutSessionRestore.css" type="text/css" media="all"/>
+ <link rel="icon" type="image/png" href="chrome://global/skin/icons/warning-16.png"/>
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutSessionRestore.js"/>
+ </head>
+
+ <body dir="&locale.dir;">
+
+ <div class="container restore-chosen">
+
+ <div class="title">
+ <h1 class="title-text">&restorepage.errorTitle;</h1>
+ </div>
+ <div class="description">
+ <p>&restorepage.problemDesc;</p>
+
+ <div id="errorLongDesc">
+ <p>&restorepage.tryThis;</p>
+ <ul>
+ <li>&restorepage.restoreSome;</li>
+ <li>&restorepage.startNew;</li>
+ </ul>
+ </div>
+ </div>
+ <div class="tree-container" available="true">
+ <xul:tree id="tabList" seltype="single" hidecolumnpicker="true"
+ onclick="onListClick(event);" onkeydown="onListKeyDown(event);"
+ _window_label="&restorepage.windowLabel;">
+ <xul:treecols>
+ <xul:treecol cycler="true" id="restore" type="checkbox" label="&restorepage.restoreHeader;"/>
+ <xul:splitter class="tree-splitter"/>
+ <xul:treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/>
+ </xul:treecols>
+ <xul:treechildren flex="1"/>
+ </xul:tree>
+ </div>
+ <div class="button-container">
+#ifdef XP_UNIX
+ <xul:button id="errorCancel"
+ label="&restorepage.closeButton;"
+ accesskey="&restorepage.close.access;"
+ oncommand="startNewSession();"/>
+ <xul:button class="primary"
+ id="errorTryAgain"
+ label="&restorepage.tryagainButton;"
+ accesskey="&restorepage.restore.access;"
+ oncommand="restoreSession();"/>
+#else
+ <xul:button class="primary"
+ id="errorTryAgain"
+ label="&restorepage.tryagainButton;"
+ accesskey="&restorepage.restore.access;"
+ oncommand="restoreSession();"/>
+ <xul:button id="errorCancel"
+ label="&restorepage.closeButton;"
+ accesskey="&restorepage.close.access;"
+ oncommand="startNewSession();"/>
+#endif
+ </div>
+ <!-- holds the session data for when the tab is closed -->
+ <input type="text" id="sessionData" style="display: none;"/>
+ </div>
+
+ </body>
+</html>
diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js
new file mode 100644
index 000000000..858e35750
--- /dev/null
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -0,0 +1,897 @@
+/* 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/. */
+
+"use strict";
+
+function debug(msg) {
+ Services.console.logStringMessage("SessionStoreContent: " + msg);
+}
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormData",
+ "resource://gre/modules/FormData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
+ "resource:///modules/sessionstore/DocShellCapabilities.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
+ "resource:///modules/sessionstore/PageStyle.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
+ "resource://gre/modules/ScrollPosition.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
+ "resource:///modules/sessionstore/SessionHistory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
+ "resource:///modules/sessionstore/SessionStorage.jsm");
+
+Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
+var gFrameTree = new FrameTree(this);
+
+Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
+XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
+ () => { return new ContentRestore(this) });
+
+// The current epoch.
+var gCurrentEpoch = 0;
+
+// A bound to the size of data to store for DOM Storage.
+const DOM_STORAGE_MAX_CHARS = 10000000; // 10M characters
+
+// This pref controls whether or not we send updates to the parent on a timeout
+// or not, and should only be used for tests or debugging.
+const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates";
+
+const kNoIndex = Number.MAX_SAFE_INTEGER;
+const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
+
+/**
+ * Returns a lazy function that will evaluate the given
+ * function |fn| only once and cache its return value.
+ */
+function createLazy(fn) {
+ let cached = false;
+ let cachedValue = null;
+
+ return function lazy() {
+ if (!cached) {
+ cachedValue = fn();
+ cached = true;
+ }
+
+ return cachedValue;
+ };
+}
+
+/**
+ * Listens for and handles content events that we need for the
+ * session store service to be notified of state changes in content.
+ */
+var EventListener = {
+
+ init: function () {
+ addEventListener("load", this, true);
+ },
+
+ handleEvent: function (event) {
+ // Ignore load events from subframes.
+ if (event.target != content.document) {
+ return;
+ }
+
+ if (content.document.documentURI.startsWith("about:reader")) {
+ if (event.type == "load" &&
+ !content.document.body.classList.contains("loaded")) {
+ // Don't restore the scroll position of an about:reader page at this
+ // point; listen for the custom event dispatched from AboutReader.jsm.
+ content.addEventListener("AboutReaderContentReady", this);
+ return;
+ }
+
+ content.removeEventListener("AboutReaderContentReady", this);
+ }
+
+ // Restore the form data and scroll position. If we're not currently
+ // restoring a tab state then this call will simply be a noop.
+ gContentRestore.restoreDocument();
+ }
+};
+
+/**
+ * Listens for and handles messages sent by the session store service.
+ */
+var MessageListener = {
+
+ MESSAGES: [
+ "SessionStore:restoreHistory",
+ "SessionStore:restoreTabContent",
+ "SessionStore:resetRestore",
+ "SessionStore:flush",
+ ],
+
+ init: function () {
+ this.MESSAGES.forEach(m => addMessageListener(m, this));
+ },
+
+ receiveMessage: function ({name, data}) {
+ // The docShell might be gone. Don't process messages,
+ // that will just lead to errors anyway.
+ if (!docShell) {
+ return;
+ }
+
+ // A fresh tab always starts with epoch=0. The parent has the ability to
+ // override that to signal a new era in this tab's life. This enables it
+ // to ignore async messages that were already sent but not yet received
+ // and would otherwise confuse the internal tab state.
+ if (data.epoch && data.epoch != gCurrentEpoch) {
+ gCurrentEpoch = data.epoch;
+ }
+
+ switch (name) {
+ case "SessionStore:restoreHistory":
+ this.restoreHistory(data);
+ break;
+ case "SessionStore:restoreTabContent":
+ this.restoreTabContent(data);
+ break;
+ case "SessionStore:resetRestore":
+ gContentRestore.resetRestore();
+ break;
+ case "SessionStore:flush":
+ this.flush(data);
+ break;
+ default:
+ debug("received unknown message '" + name + "'");
+ break;
+ }
+ },
+
+ restoreHistory({epoch, tabData, loadArguments, isRemotenessUpdate}) {
+ gContentRestore.restoreHistory(tabData, loadArguments, {
+ // Note: The callbacks passed here will only be used when a load starts
+ // that was not initiated by sessionstore itself. This can happen when
+ // some code calls browser.loadURI() or browser.reload() on a pending
+ // browser/tab.
+
+ onLoadStarted() {
+ // Notify the parent that the tab is no longer pending.
+ sendSyncMessage("SessionStore:restoreTabContentStarted", {epoch});
+ },
+
+ onLoadFinished() {
+ // Tell SessionStore.jsm that it may want to restore some more tabs,
+ // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
+ sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch});
+ }
+ });
+
+ // When restoreHistory finishes, we send a synchronous message to
+ // SessionStore.jsm so that it can run SSTabRestoring. Users of
+ // SSTabRestoring seem to get confused if chrome and content are out of
+ // sync about the state of the restore (particularly regarding
+ // docShell.currentURI). Using a synchronous message is the easiest way
+ // to temporarily synchronize them.
+ sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate});
+ },
+
+ restoreTabContent({loadArguments, isRemotenessUpdate}) {
+ let epoch = gCurrentEpoch;
+
+ // We need to pass the value of didStartLoad back to SessionStore.jsm.
+ let didStartLoad = gContentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => {
+ // Tell SessionStore.jsm that it may want to restore some more tabs,
+ // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
+ sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
+ });
+
+ sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch, isRemotenessUpdate});
+
+ if (!didStartLoad) {
+ // Pretend that the load succeeded so that event handlers fire correctly.
+ sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
+ }
+ },
+
+ flush({id}) {
+ // Flush the message queue, send the latest updates.
+ MessageQueue.send({flushID: id});
+ }
+};
+
+/**
+ * Listens for changes to the session history. Whenever the user navigates
+ * we will collect URLs and everything belonging to session history.
+ *
+ * Causes a SessionStore:update message to be sent that contains the current
+ * session history.
+ *
+ * Example:
+ * {entries: [{url: "about:mozilla", ...}, ...], index: 1}
+ */
+var SessionHistoryListener = {
+ init: function () {
+ // The frame tree observer is needed to handle initial subframe loads.
+ // It will redundantly invalidate with the SHistoryListener in some cases
+ // but these invalidations are very cheap.
+ gFrameTree.addObserver(this);
+
+ // By adding the SHistoryListener immediately, we will unfortunately be
+ // notified of every history entry as the tab is restored. We don't bother
+ // waiting to add the listener later because these notifications are cheap.
+ // We will likely only collect once since we are batching collection on
+ // a delay.
+ docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.
+ addSHistoryListener(this);
+
+ // Collect data if we start with a non-empty shistory.
+ if (!SessionHistory.isEmpty(docShell)) {
+ this.collect();
+ // When a tab is detached from the window, for the new window there is a
+ // new SessionHistoryListener created. Normally it is empty at this point
+ // but in a test env. the initial about:blank might have a children in which
+ // case we fire off a history message here with about:blank in it. If we
+ // don't do it ASAP then there is going to be a browser swap and the parent
+ // will be all confused by that message.
+ MessageQueue.send();
+ }
+
+ // Listen for page title changes.
+ addEventListener("DOMTitleChanged", this);
+ },
+
+ uninit: function () {
+ let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
+ if (sessionHistory) {
+ sessionHistory.removeSHistoryListener(this);
+ }
+ },
+
+ collect: function () {
+ this._fromIdx = kNoIndex;
+ if (docShell) {
+ MessageQueue.push("history", () => SessionHistory.collect(docShell));
+ }
+ },
+
+ _fromIdx: kNoIndex,
+
+ // History can grow relatively big with the nested elements, so if we don't have to, we
+ // don't want to send the entire history all the time. For a simple optimization
+ // we keep track of the smallest index from after any change has occured and we just send
+ // the elements from that index. If something more complicated happens we just clear it
+ // and send the entire history. We always send the additional info like the current selected
+ // index (so for going back and forth between history entries we set the index to kLastIndex
+ // if nothing else changed send an empty array and the additonal info like the selected index)
+ collectFrom: function (idx) {
+ if (this._fromIdx <= idx) {
+ // If we already know that we need to update history fromn index N we can ignore any changes
+ // tha happened with an element with index larger than N.
+ // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything
+ // here, and in case of navigation in the history back and forth we use kLastIndex which ignores
+ // only the subsequent navigations, but not any new elements added.
+ return;
+ }
+
+ this._fromIdx = idx;
+ MessageQueue.push("historychange", () => {
+ if (this._fromIdx === kNoIndex) {
+ return null;
+ }
+
+ let history = SessionHistory.collect(docShell);
+ if (kLastIndex == idx) {
+ history.entries = [];
+ } else {
+ history.entries.splice(0, this._fromIdx + 1);
+ }
+
+ history.fromIdx = this._fromIdx;
+
+ this._fromIdx = kNoIndex;
+ return history;
+ });
+ },
+
+ handleEvent(event) {
+ this.collect();
+ },
+
+ onFrameTreeCollected: function () {
+ this.collect();
+ },
+
+ onFrameTreeReset: function () {
+ this.collect();
+ },
+
+ OnHistoryNewEntry: function (newURI, oldIndex) {
+ this.collectFrom(oldIndex);
+ },
+
+ OnHistoryGoBack: function (backURI) {
+ this.collectFrom(kLastIndex);
+ return true;
+ },
+
+ OnHistoryGoForward: function (forwardURI) {
+ this.collectFrom(kLastIndex);
+ return true;
+ },
+
+ OnHistoryGotoIndex: function (index, gotoURI) {
+ this.collectFrom(kLastIndex);
+ return true;
+ },
+
+ OnHistoryPurge: function (numEntries) {
+ this.collect();
+ return true;
+ },
+
+ OnHistoryReload: function (reloadURI, reloadFlags) {
+ this.collect();
+ return true;
+ },
+
+ OnHistoryReplaceEntry: function (index) {
+ this.collect();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsISHistoryListener,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+/**
+ * Listens for scroll position changes. Whenever the user scrolls the top-most
+ * frame we update the scroll position and will restore it when requested.
+ *
+ * Causes a SessionStore:update message to be sent that contains the current
+ * scroll positions as a tree of strings. If no frame of the whole frame tree
+ * is scrolled this will return null so that we don't tack a property onto
+ * the tabData object in the parent process.
+ *
+ * Example:
+ * {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
+ */
+var ScrollPositionListener = {
+ init: function () {
+ addEventListener("scroll", this);
+ gFrameTree.addObserver(this);
+ },
+
+ handleEvent: function (event) {
+ let frame = event.target.defaultView;
+
+ // Don't collect scroll data for frames created at or after the load event
+ // as SessionStore can't restore scroll data for those.
+ if (gFrameTree.contains(frame)) {
+ MessageQueue.push("scroll", () => this.collect());
+ }
+ },
+
+ onFrameTreeCollected: function () {
+ MessageQueue.push("scroll", () => this.collect());
+ },
+
+ onFrameTreeReset: function () {
+ MessageQueue.push("scroll", () => null);
+ },
+
+ collect: function () {
+ return gFrameTree.map(ScrollPosition.collect);
+ }
+};
+
+/**
+ * Listens for changes to input elements. Whenever the value of an input
+ * element changes we will re-collect data for the current frame tree and send
+ * a message to the parent process.
+ *
+ * Causes a SessionStore:update message to be sent that contains the form data
+ * for all reachable frames.
+ *
+ * Example:
+ * {
+ * formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}},
+ * children: [
+ * null,
+ * {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
+ * ]
+ * }
+ */
+var FormDataListener = {
+ init: function () {
+ addEventListener("input", this, true);
+ addEventListener("change", this, true);
+ gFrameTree.addObserver(this);
+ },
+
+ handleEvent: function (event) {
+ let frame = event.target.ownerGlobal;
+
+ // Don't collect form data for frames created at or after the load event
+ // as SessionStore can't restore form data for those.
+ if (gFrameTree.contains(frame)) {
+ MessageQueue.push("formdata", () => this.collect());
+ }
+ },
+
+ onFrameTreeReset: function () {
+ MessageQueue.push("formdata", () => null);
+ },
+
+ collect: function () {
+ return gFrameTree.map(FormData.collect);
+ }
+};
+
+/**
+ * Listens for changes to the page style. Whenever a different page style is
+ * selected or author styles are enabled/disabled we send a message with the
+ * currently applied style to the chrome process.
+ *
+ * Causes a SessionStore:update message to be sent that contains the currently
+ * selected pageStyle for all reachable frames.
+ *
+ * Example:
+ * {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]}
+ */
+var PageStyleListener = {
+ init: function () {
+ Services.obs.addObserver(this, "author-style-disabled-changed", false);
+ Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false);
+ gFrameTree.addObserver(this);
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "author-style-disabled-changed");
+ Services.obs.removeObserver(this, "style-sheet-applicable-state-changed");
+ },
+
+ observe: function (subject, topic) {
+ let frame = subject.defaultView;
+
+ if (frame && gFrameTree.contains(frame)) {
+ MessageQueue.push("pageStyle", () => this.collect());
+ }
+ },
+
+ collect: function () {
+ return PageStyle.collect(docShell, gFrameTree);
+ },
+
+ onFrameTreeCollected: function () {
+ MessageQueue.push("pageStyle", () => this.collect());
+ },
+
+ onFrameTreeReset: function () {
+ MessageQueue.push("pageStyle", () => null);
+ }
+};
+
+/**
+ * Listens for changes to docShell capabilities. Whenever a new load is started
+ * we need to re-check the list of capabilities and send message when it has
+ * changed.
+ *
+ * Causes a SessionStore:update message to be sent that contains the currently
+ * disabled docShell capabilities (all nsIDocShell.allow* properties set to
+ * false) as a string - i.e. capability names separate by commas.
+ */
+var DocShellCapabilitiesListener = {
+ /**
+ * This field is used to compare the last docShell capabilities to the ones
+ * that have just been collected. If nothing changed we won't send a message.
+ */
+ _latestCapabilities: "",
+
+ init: function () {
+ gFrameTree.addObserver(this);
+ },
+
+ /**
+ * onFrameTreeReset() is called as soon as we start loading a page.
+ */
+ onFrameTreeReset: function() {
+ // The order of docShell capabilities cannot change while we're running
+ // so calling join() without sorting before is totally sufficient.
+ let caps = DocShellCapabilities.collect(docShell).join(",");
+
+ // Send new data only when the capability list changes.
+ if (caps != this._latestCapabilities) {
+ this._latestCapabilities = caps;
+ MessageQueue.push("disallow", () => caps || null);
+ }
+ }
+};
+
+/**
+ * Listens for changes to the DOMSessionStorage. Whenever new keys are added,
+ * existing ones removed or changed, or the storage is cleared we will send a
+ * message to the parent process containing up-to-date sessionStorage data.
+ *
+ * Causes a SessionStore:update message to be sent that contains the current
+ * DOMSessionStorage contents. The data is a nested object using host names
+ * as keys and per-host DOMSessionStorage data as values.
+ */
+var SessionStorageListener = {
+ init: function () {
+ addEventListener("MozSessionStorageChanged", this, true);
+ Services.obs.addObserver(this, "browser:purge-domain-data", false);
+ gFrameTree.addObserver(this);
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "browser:purge-domain-data");
+ },
+
+ handleEvent: function (event) {
+ if (gFrameTree.contains(event.target)) {
+ this.collectFromEvent(event);
+ }
+ },
+
+ observe: function () {
+ // Collect data on the next tick so that any other observer
+ // that needs to purge data can do its work first.
+ setTimeout(() => this.collect(), 0);
+ },
+
+ // Before DOM Storage can be written to disk, it needs to be serialized
+ // for sending across frames/processes, then again to be sent across
+ // threads, then again to be put in a buffer for the disk. Each of these
+ // serializations is an opportunity to OOM and (depending on the site of
+ // the OOM), either crash, lose all data for the frame or lose all data
+ // for the application.
+ //
+ // In order to avoid this, compute an estimate of the size of the
+ // object, and block SessionStorage items that are too large. As
+ // we also don't want to cause an OOM here, we use a quick and memory-
+ // efficient approximation: we compute the total sum of string lengths
+ // involved in this object.
+ estimateStorageSize: function(collected) {
+ if (!collected) {
+ return 0;
+ }
+
+ let size = 0;
+ for (let host of Object.keys(collected)) {
+ size += host.length;
+ let perHost = collected[host];
+ for (let key of Object.keys(perHost)) {
+ size += key.length;
+ let perKey = perHost[key];
+ size += perKey.length;
+ }
+ }
+
+ return size;
+ },
+
+ // We don't want to send all the session storage data for all the frames
+ // for every change. So if only a few value changed we send them over as
+ // a "storagechange" event. If however for some reason before we send these
+ // changes we have to send over the entire sessions storage data, we just
+ // reset these changes.
+ _changes: undefined,
+
+ resetChanges: function () {
+ this._changes = undefined;
+ },
+
+ collectFromEvent: function (event) {
+ // TODO: we should take browser.sessionstore.dom_storage_limit into an account here.
+ if (docShell) {
+ let {url, key, newValue} = event;
+ let uri = Services.io.newURI(url, null, null);
+ let domain = uri.prePath;
+ if (!this._changes) {
+ this._changes = {};
+ }
+ if (!this._changes[domain]) {
+ this._changes[domain] = {};
+ }
+ this._changes[domain][key] = newValue;
+
+ MessageQueue.push("storagechange", () => {
+ let tmp = this._changes;
+ // If there were multiple changes we send them merged.
+ // First one will collect all the changes the rest of
+ // these messages will be ignored.
+ this.resetChanges();
+ return tmp;
+ });
+ }
+ },
+
+ collect: function () {
+ if (docShell) {
+ // We need the entire session storage, let's reset the pending individual change
+ // messages.
+ this.resetChanges();
+ MessageQueue.push("storage", () => {
+ let collected = SessionStorage.collect(docShell, gFrameTree);
+
+ if (collected == null) {
+ return collected;
+ }
+
+ let size = this.estimateStorageSize(collected);
+
+ MessageQueue.push("telemetry", () => ({ FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS: size }));
+ if (size > Preferences.get("browser.sessionstore.dom_storage_limit", DOM_STORAGE_MAX_CHARS)) {
+ // Rather than keeping the old storage, which wouldn't match the rest
+ // of the state of the page, empty the storage. DOM storage will be
+ // recollected the next time and stored if it is now small enough.
+ return {};
+ }
+
+ return collected;
+ });
+ }
+ },
+
+ onFrameTreeCollected: function () {
+ this.collect();
+ },
+
+ onFrameTreeReset: function () {
+ this.collect();
+ }
+};
+
+/**
+ * Listen for changes to the privacy status of the tab.
+ * By definition, tabs start in non-private mode.
+ *
+ * Causes a SessionStore:update message to be sent for
+ * field "isPrivate". This message contains
+ * |true| if the tab is now private
+ * |null| if the tab is now public - the field is therefore
+ * not saved.
+ */
+var PrivacyListener = {
+ init: function() {
+ docShell.addWeakPrivacyTransitionObserver(this);
+
+ // Check that value at startup as it might have
+ // been set before the frame script was loaded.
+ if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) {
+ MessageQueue.push("isPrivate", () => true);
+ }
+ },
+
+ // Ci.nsIPrivacyTransitionObserver
+ privateModeChanged: function(enabled) {
+ MessageQueue.push("isPrivate", () => enabled || null);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+/**
+ * A message queue that takes collected data and will take care of sending it
+ * to the chrome process. It allows flushing using synchronous messages and
+ * takes care of any race conditions that might occur because of that. Changes
+ * will be batched if they're pushed in quick succession to avoid a message
+ * flood.
+ */
+var MessageQueue = {
+ /**
+ * A map (string -> lazy fn) holding lazy closures of all queued data
+ * collection routines. These functions will return data collected from the
+ * docShell.
+ */
+ _data: new Map(),
+
+ /**
+ * The delay (in ms) used to delay sending changes after data has been
+ * invalidated.
+ */
+ BATCH_DELAY_MS: 1000,
+
+ /**
+ * The current timeout ID, null if there is no queue data. We use timeouts
+ * to damp a flood of data changes and send lots of changes as one batch.
+ */
+ _timeout: null,
+
+ /**
+ * Whether or not sending batched messages on a timer is disabled. This should
+ * only be used for debugging or testing. If you need to access this value,
+ * you should probably use the timeoutDisabled getter.
+ */
+ _timeoutDisabled: false,
+
+ /**
+ * True if batched messages are not being fired on a timer. This should only
+ * ever be true when debugging or during tests.
+ */
+ get timeoutDisabled() {
+ return this._timeoutDisabled;
+ },
+
+ /**
+ * Disables sending batched messages on a timer. Also cancels any pending
+ * timers.
+ */
+ set timeoutDisabled(val) {
+ this._timeoutDisabled = val;
+
+ if (val && this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+
+ return val;
+ },
+
+ init() {
+ this.timeoutDisabled =
+ Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
+
+ Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this, false);
+ },
+
+ uninit() {
+ Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this);
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed" && data == TIMEOUT_DISABLED_PREF) {
+ this.timeoutDisabled =
+ Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
+ }
+ },
+
+ /**
+ * Pushes a given |value| onto the queue. The given |key| represents the type
+ * of data that is stored and can override data that has been queued before
+ * but has not been sent to the parent process, yet.
+ *
+ * @param key (string)
+ * A unique identifier specific to the type of data this is passed.
+ * @param fn (function)
+ * A function that returns the value that will be sent to the parent
+ * process.
+ */
+ push: function (key, fn) {
+ this._data.set(key, createLazy(fn));
+
+ if (!this._timeout && !this._timeoutDisabled) {
+ // Wait a little before sending the message to batch multiple changes.
+ this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS);
+ }
+ },
+
+ /**
+ * Sends queued data to the chrome process.
+ *
+ * @param options (object)
+ * {flushID: 123} to specify that this is a flush
+ * {isFinal: true} to signal this is the final message sent on unload
+ */
+ send: function (options = {}) {
+ // Looks like we have been called off a timeout after the tab has been
+ // closed. The docShell is gone now and we can just return here as there
+ // is nothing to do.
+ if (!docShell) {
+ return;
+ }
+
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+
+ let flushID = (options && options.flushID) || 0;
+
+ let durationMs = Date.now();
+
+ let data = {};
+ let telemetry = {};
+ for (let [key, func] of this._data) {
+ let value = func();
+ if (key == "telemetry") {
+ for (let histogramId of Object.keys(value)) {
+ telemetry[histogramId] = value[histogramId];
+ }
+ } else if (value || (key != "storagechange" && key != "historychange")) {
+ data[key] = value;
+ }
+ }
+
+ this._data.clear();
+
+ durationMs = Date.now() - durationMs;
+ telemetry.FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS = durationMs;
+
+ try {
+ // Send all data to the parent process.
+ sendAsyncMessage("SessionStore:update", {
+ data, telemetry, flushID,
+ isFinal: options.isFinal || false,
+ epoch: gCurrentEpoch
+ });
+ } catch (ex if ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) {
+ let telemetry = {
+ FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM: 1
+ };
+ sendAsyncMessage("SessionStore:error", {
+ telemetry
+ });
+ }
+ },
+};
+
+EventListener.init();
+MessageListener.init();
+FormDataListener.init();
+PageStyleListener.init();
+SessionHistoryListener.init();
+SessionStorageListener.init();
+ScrollPositionListener.init();
+DocShellCapabilitiesListener.init();
+PrivacyListener.init();
+MessageQueue.init();
+
+function handleRevivedTab() {
+ if (!content) {
+ removeEventListener("pagehide", handleRevivedTab);
+ return;
+ }
+
+ if (content.document.documentURI.startsWith("about:tabcrashed")) {
+ if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ // Sanity check - we'd better be loading this in a non-remote browser.
+ throw new Error("We seem to be navigating away from about:tabcrashed in " +
+ "a non-remote browser. This should really never happen.");
+ }
+
+ removeEventListener("pagehide", handleRevivedTab);
+
+ // Notify the parent.
+ sendAsyncMessage("SessionStore:crashedTabRevived");
+ }
+}
+
+// If we're browsing from the tab crashed UI to a blacklisted URI that keeps
+// this browser non-remote, we'll handle that in a pagehide event.
+addEventListener("pagehide", handleRevivedTab);
+
+addEventListener("unload", () => {
+ // Upon frameLoader destruction, send a final update message to
+ // the parent and flush all data currently held in the child.
+ MessageQueue.send({isFinal: true});
+
+ // If we're browsing from the tab crashed UI to a URI that causes the tab
+ // to go remote again, we catch this in the unload event handler, because
+ // swapping out the non-remote browser for a remote one in
+ // tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide
+ // event to be fired.
+ handleRevivedTab();
+
+ // Remove all registered nsIObservers.
+ PageStyleListener.uninit();
+ SessionStorageListener.uninit();
+ SessionHistoryListener.uninit();
+ MessageQueue.uninit();
+
+ // Remove progress listeners.
+ gContentRestore.resetRestore();
+
+ // We don't need to take care of any gFrameTree observers as the gFrameTree
+ // will die with the content script. The same goes for the privacy transition
+ // observer that will die with the docShell when the tab is closed.
+});
diff --git a/browser/components/sessionstore/jar.mn b/browser/components/sessionstore/jar.mn
new file mode 100644
index 000000000..7e5bc07dc
--- /dev/null
+++ b/browser/components/sessionstore/jar.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+browser.jar:
+* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
+ content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js)
+ content/browser/content-sessionStore.js (content/content-sessionStore.js)
diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build
new file mode 100644
index 000000000..8a8221c9f
--- /dev/null
+++ b/browser/components/sessionstore/moz.build
@@ -0,0 +1,52 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+XPIDL_SOURCES += [
+ 'nsISessionStartup.idl',
+ 'nsISessionStore.idl',
+]
+
+XPIDL_MODULE = 'sessionstore'
+
+EXTRA_COMPONENTS += [
+ 'nsSessionStartup.js',
+ 'nsSessionStore.js',
+ 'nsSessionStore.manifest',
+]
+
+EXTRA_JS_MODULES.sessionstore = [
+ 'ContentRestore.jsm',
+ 'DocShellCapabilities.jsm',
+ 'FrameTree.jsm',
+ 'GlobalState.jsm',
+ 'PageStyle.jsm',
+ 'PrivacyFilter.jsm',
+ 'PrivacyLevel.jsm',
+ 'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
+ 'RunState.jsm',
+ 'SessionCookies.jsm',
+ 'SessionFile.jsm',
+ 'SessionHistory.jsm',
+ 'SessionMigration.jsm',
+ 'SessionSaver.jsm',
+ 'SessionStorage.jsm',
+ 'SessionStore.jsm',
+ 'SessionWorker.js',
+ 'SessionWorker.jsm',
+ 'StartupPerformance.jsm',
+ 'TabAttributes.jsm',
+ 'TabState.jsm',
+ 'TabStateCache.jsm',
+ 'TabStateFlusher.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Session Restore')
diff --git a/browser/components/sessionstore/nsISessionStartup.idl b/browser/components/sessionstore/nsISessionStartup.idl
new file mode 100644
index 000000000..2321ac310
--- /dev/null
+++ b/browser/components/sessionstore/nsISessionStartup.idl
@@ -0,0 +1,66 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * nsISessionStore keeps track of the current browsing state - i.e.
+ * tab history, cookies, scroll state, form data, and window features
+ * - and allows to restore everything into one window.
+ */
+
+[scriptable, uuid(934697e4-3807-47f8-b6c9-6caa8d83ccd1)]
+interface nsISessionStartup: nsISupports
+{
+ /**
+ * Return a promise that is resolved once initialization
+ * is complete.
+ */
+ readonly attribute jsval onceInitialized;
+
+ // Get session state
+ readonly attribute jsval state;
+
+ /**
+ * Determines whether there is a pending session restore. Should only be
+ * called after initialization has completed.
+ */
+ boolean doRestore();
+
+ /**
+ * Determines whether automatic session restoration is enabled for this
+ * launch of the browser. This does not include crash restoration, and will
+ * return false if restoration will only be caused by a crash.
+ */
+ boolean isAutomaticRestoreEnabled();
+
+ /**
+ * Returns whether we will restore a session that ends up replacing the
+ * homepage. The browser uses this to not start loading the homepage if
+ * we're going to stop its load anyway shortly after.
+ *
+ * This is meant to be an optimization for the average case that loading the
+ * session file finishes before we may want to start loading the default
+ * homepage. Should this be called before the session file has been read it
+ * will just return false.
+ */
+ readonly attribute bool willOverrideHomepage;
+
+ /**
+ * What type of session we're restoring.
+ * NO_SESSION There is no data available from the previous session
+ * RECOVER_SESSION The last session crashed. It will either be restored or
+ * about:sessionrestore will be shown.
+ * RESUME_SESSION The previous session should be restored at startup
+ * DEFER_SESSION The previous session is fine, but it shouldn't be restored
+ * without explicit action (with the exception of pinned tabs)
+ */
+ const unsigned long NO_SESSION = 0;
+ const unsigned long RECOVER_SESSION = 1;
+ const unsigned long RESUME_SESSION = 2;
+ const unsigned long DEFER_SESSION = 3;
+
+ readonly attribute unsigned long sessionType;
+ readonly attribute bool previousSessionCrashed;
+};
diff --git a/browser/components/sessionstore/nsISessionStore.idl b/browser/components/sessionstore/nsISessionStore.idl
new file mode 100644
index 000000000..0d2500ef7
--- /dev/null
+++ b/browser/components/sessionstore/nsISessionStore.idl
@@ -0,0 +1,220 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+interface nsIDOMNode;
+
+/**
+ * nsISessionStore keeps track of the current browsing state - i.e.
+ * tab history, cookies, scroll state, form data, and window features
+ * - and allows to restore everything into one browser window.
+ *
+ * The nsISessionStore API operates mostly on browser windows and the tabbrowser
+ * tabs contained in them:
+ *
+ * * "Browser windows" are those DOM windows having loaded
+ * chrome://browser/content/browser.xul . From overlays you can just pass the
+ * global |window| object to the API, though (or |top| from a sidebar).
+ * From elsewhere you can get browser windows through the nsIWindowMediator
+ * by looking for "navigator:browser" windows.
+ *
+ * * "Tabbrowser tabs" are all the child nodes of a browser window's
+ * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|.
+ */
+
+[scriptable, uuid(4580f5eb-693d-423d-b0ce-2cb20a962e4d)]
+interface nsISessionStore : nsISupports
+{
+ /**
+ * Is it possible to restore the previous session. Will always be false when
+ * in Private Browsing mode.
+ */
+ attribute boolean canRestoreLastSession;
+
+ /**
+ * Restore the previous session if possible. This will not overwrite the
+ * current session. Instead the previous session will be merged into the
+ * current session. Current windows will be reused if they were windows that
+ * pinned tabs were previously restored into. New windows will be opened as
+ * needed.
+ *
+ * Note: This will throw if there is no previous state to restore. Check with
+ * canRestoreLastSession first to avoid thrown errors.
+ */
+ void restoreLastSession();
+
+ /**
+ * Get the current browsing state.
+ * @returns a JSON string representing the session state.
+ */
+ AString getBrowserState();
+
+ /**
+ * Set the browsing state.
+ * This will immediately restore the state of the whole application to the state
+ * passed in, *replacing* the current session.
+ *
+ * @param aState is a JSON string representing the session state.
+ */
+ void setBrowserState(in AString aState);
+
+ /**
+ * @param aWindow is the browser window whose state is to be returned.
+ *
+ * @returns a JSON string representing a session state with only one window.
+ */
+ AString getWindowState(in nsIDOMWindow aWindow);
+
+ /**
+ * @param aWindow is the browser window whose state is to be set.
+ * @param aState is a JSON string representing a session state.
+ * @param aOverwrite boolean overwrite existing tabs
+ */
+ void setWindowState(in nsIDOMWindow aWindow, in AString aState, in boolean aOverwrite);
+
+ /**
+ * @param aTab is the tabbrowser tab whose state is to be returned.
+ *
+ * @returns a JSON string representing the state of the tab
+ * (note: doesn't contain cookies - if you need them, use getWindowState instead).
+ */
+ AString getTabState(in nsIDOMNode aTab);
+
+ /**
+ * @param aTab is the tabbrowser tab whose state is to be set.
+ * @param aState is a JSON string representing a session state.
+ */
+ void setTabState(in nsIDOMNode aTab, in AString aState);
+
+ /**
+ * Duplicates a given tab as thoroughly as possible.
+ *
+ * @param aWindow is the browser window into which the tab will be duplicated.
+ * @param aTab is the tabbrowser tab to duplicate (can be from a different window).
+ * @param aDelta is the offset to the history entry to load in the duplicated tab.
+ * @returns a reference to the newly created tab.
+ */
+ nsIDOMNode duplicateTab(in nsIDOMWindow aWindow, in nsIDOMNode aTab,
+ [optional] in long aDelta);
+
+ /**
+ * Get the number of restore-able tabs for a browser window
+ */
+ unsigned long getClosedTabCount(in nsIDOMWindow aWindow);
+
+ /**
+ * Get closed tab data
+ *
+ * @param aWindow is the browser window for which to get closed tab data
+ * @returns a JSON string representing the list of closed tabs.
+ */
+ AString getClosedTabData(in nsIDOMWindow aWindow);
+
+ /**
+ * @param aWindow is the browser window to reopen a closed tab in.
+ * @param aIndex is the index of the tab to be restored (FIFO ordered).
+ * @returns a reference to the reopened tab.
+ */
+ nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * @param aWindow is the browser window associated with the closed tab.
+ * @param aIndex is the index of the closed tab to be removed (FIFO ordered).
+ */
+ nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * Get the number of restore-able windows
+ */
+ unsigned long getClosedWindowCount();
+
+ /**
+ * Get closed windows data
+ *
+ * @returns a JSON string representing the list of closed windows.
+ */
+ AString getClosedWindowData();
+
+ /**
+ * @param aIndex is the index of the windows to be restored (FIFO ordered).
+ * @returns the nsIDOMWindow object of the reopened window
+ */
+ nsIDOMWindow undoCloseWindow(in unsigned long aIndex);
+
+ /**
+ * @param aIndex is the index of the closed window to be removed (FIFO ordered).
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * when aIndex does not map to a closed window
+ */
+ nsIDOMNode forgetClosedWindow(in unsigned long aIndex);
+
+ /**
+ * @param aWindow is the window to get the value for.
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getWindowValue(in nsIDOMWindow aWindow, in AString aKey);
+
+ /**
+ * @param aWindow is the browser window to set the value for.
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
+ */
+ void setWindowValue(in nsIDOMWindow aWindow, in AString aKey, in jsval aStringValue);
+
+ /**
+ * @param aWindow is the browser window to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteWindowValue(in nsIDOMWindow aWindow, in AString aKey);
+
+ /**
+ * @param aTab is the tabbrowser tab to get the value for.
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getTabValue(in nsIDOMNode aTab, in AString aKey);
+
+ /**
+ * @param aTab is the tabbrowser tab to set the value for.
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
+ */
+ void setTabValue(in nsIDOMNode aTab, in AString aKey, in jsval aStringValue);
+
+ /**
+ * @param aTab is the tabbrowser tab to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteTabValue(in nsIDOMNode aTab, in AString aKey);
+
+ /**
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getGlobalValue(in AString aKey);
+
+ /**
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
+ */
+ void setGlobalValue(in AString aKey, in jsval aStringValue);
+
+ /**
+ * @param aTab is the browser tab to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteGlobalValue(in AString aKey);
+
+ /**
+ * @param aName is the name of the attribute to save/restore for all tabbrowser tabs.
+ */
+ void persistTabAttribute(in AString aName);
+};
diff --git a/browser/components/sessionstore/nsSessionStartup.js b/browser/components/sessionstore/nsSessionStartup.js
new file mode 100644
index 000000000..7593c48ec
--- /dev/null
+++ b/browser/components/sessionstore/nsSessionStartup.js
@@ -0,0 +1,353 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Session Storage and Restoration
+ *
+ * Overview
+ * This service reads user's session file at startup, and makes a determination
+ * as to whether the session should be restored. It will restore the session
+ * under the circumstances described below. If the auto-start Private Browsing
+ * mode is active, however, the session is never restored.
+ *
+ * Crash Detection
+ * The CrashMonitor is used to check if the final session state was successfully
+ * written at shutdown of the last session. If we did not reach
+ * 'sessionstore-final-state-write-complete', then it's assumed that the browser
+ * has previously crashed and we should restore the session.
+ *
+ * Forced Restarts
+ * In the event that a restart is required due to application update or extension
+ * installation, set the browser.sessionstore.resume_session_once pref to true,
+ * and the session will be restored the next time the browser starts.
+ *
+ * Always Resume
+ * This service will always resume the session if the integer pref
+ * browser.startup.page is set to 3.
+ */
+
+/* :::::::: Constants and Helpers ::::::::::::::: */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
+ "resource:///modules/sessionstore/SessionFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "StartupPerformance",
+ "resource:///modules/sessionstore/StartupPerformance.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
+ "resource://gre/modules/CrashMonitor.jsm");
+
+const STATE_RUNNING_STR = "running";
+
+// 'browser.startup.page' preference value to resume the previous session.
+const BROWSER_STARTUP_RESUME_SESSION = 3;
+
+function debug(aMsg) {
+ aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n");
+ Services.console.logStringMessage(aMsg);
+}
+function warning(aMsg, aException) {
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+consoleMsg.init(aMsg, aException.fileName, null, aException.lineNumber, 0, Ci.nsIScriptError.warningFlag, "component javascript");
+ Services.console.logMessage(consoleMsg);
+}
+
+var gOnceInitializedDeferred = (function () {
+ let deferred = {};
+
+ deferred.promise = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+
+ return deferred;
+})();
+
+/* :::::::: The Service ::::::::::::::: */
+
+function SessionStartup() {
+}
+
+SessionStartup.prototype = {
+
+ // the state to restore at startup
+ _initialState: null,
+ _sessionType: Ci.nsISessionStartup.NO_SESSION,
+ _initialized: false,
+
+ // Stores whether the previous session crashed.
+ _previousSessionCrashed: null,
+
+/* ........ Global Event Handlers .............. */
+
+ /**
+ * Initialize the component
+ */
+ init: function sss_init() {
+ Services.obs.notifyObservers(null, "sessionstore-init-started", null);
+ StartupPerformance.init();
+
+ // do not need to initialize anything in auto-started private browsing sessions
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ this._initialized = true;
+ gOnceInitializedDeferred.resolve();
+ return;
+ }
+
+ SessionFile.read().then(
+ this._onSessionFileRead.bind(this),
+ console.error
+ );
+ },
+
+ // Wrap a string as a nsISupports
+ _createSupportsString: function ssfi_createSupportsString(aData) {
+ let string = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ string.data = aData;
+ return string;
+ },
+
+ /**
+ * Complete initialization once the Session File has been read
+ *
+ * @param source The Session State string read from disk.
+ * @param parsed The object obtained by parsing |source| as JSON.
+ */
+ _onSessionFileRead: function ({source, parsed, noFilesFound}) {
+ this._initialized = true;
+
+ // Let observers modify the state before it is used
+ let supportsStateString = this._createSupportsString(source);
+ Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
+ let stateString = supportsStateString.data;
+
+ if (stateString != source) {
+ // The session has been modified by an add-on, reparse.
+ try {
+ this._initialState = JSON.parse(stateString);
+ } catch (ex) {
+ // That's not very good, an add-on has rewritten the initial
+ // state to something that won't parse.
+ warning("Observer rewrote the state to something that won't parse", ex);
+ }
+ } else {
+ // No need to reparse
+ this._initialState = parsed;
+ }
+
+ if (this._initialState == null) {
+ // No valid session found.
+ this._sessionType = Ci.nsISessionStartup.NO_SESSION;
+ Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
+ gOnceInitializedDeferred.resolve();
+ return;
+ }
+
+ let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
+ let shouldResumeSession = shouldResumeSessionOnce ||
+ Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
+
+ // If this is a normal restore then throw away any previous session
+ if (!shouldResumeSessionOnce && this._initialState) {
+ delete this._initialState.lastSessionState;
+ }
+
+ let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
+
+ CrashMonitor.previousCheckpoints.then(checkpoints => {
+ if (checkpoints) {
+ // If the previous session finished writing the final state, we'll
+ // assume there was no crash.
+ this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"];
+
+ } else {
+ // If the Crash Monitor could not load a checkpoints file it will
+ // provide null. This could occur on the first run after updating to
+ // a version including the Crash Monitor, or if the checkpoints file
+ // was removed, or on first startup with this profile, or after Firefox Reset.
+
+ if (noFilesFound) {
+ // There was no checkpoints file and no sessionstore.js or its backups
+ // so we will assume that this was a fresh profile.
+ this._previousSessionCrashed = false;
+
+ } else {
+ // If this is the first run after an update, sessionstore.js should
+ // still contain the session.state flag to indicate if the session
+ // crashed. If it is not present, we will assume this was not the first
+ // run after update and the checkpoints file was somehow corrupted or
+ // removed by a crash.
+ //
+ // If the session.state flag is present, we will fallback to using it
+ // for crash detection - If the last write of sessionstore.js had it
+ // set to "running", we crashed.
+ let stateFlagPresent = (this._initialState.session &&
+ this._initialState.session.state);
+
+
+ this._previousSessionCrashed = !stateFlagPresent ||
+ (this._initialState.session.state == STATE_RUNNING_STR);
+ }
+ }
+
+ // Report shutdown success via telemetry. Shortcoming here are
+ // being-killed-by-OS-shutdown-logic, shutdown freezing after
+ // session restore was written, etc.
+ Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed);
+
+ // set the startup type
+ if (this._previousSessionCrashed && resumeFromCrash)
+ this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
+ else if (!this._previousSessionCrashed && shouldResumeSession)
+ this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
+ else if (this._initialState)
+ this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
+ else
+ this._initialState = null; // reset the state
+
+ Services.obs.addObserver(this, "sessionstore-windows-restored", true);
+
+ if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
+ Services.obs.addObserver(this, "browser:purge-session-history", true);
+
+ // We're ready. Notify everyone else.
+ Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
+ gOnceInitializedDeferred.resolve();
+ });
+ },
+
+ /**
+ * Handle notifications
+ */
+ observe: function sss_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "app-startup":
+ Services.obs.addObserver(this, "final-ui-startup", true);
+ Services.obs.addObserver(this, "quit-application", true);
+ break;
+ case "final-ui-startup":
+ Services.obs.removeObserver(this, "final-ui-startup");
+ Services.obs.removeObserver(this, "quit-application");
+ this.init();
+ break;
+ case "quit-application":
+ // no reason for initializing at this point (cf. bug 409115)
+ Services.obs.removeObserver(this, "final-ui-startup");
+ Services.obs.removeObserver(this, "quit-application");
+ if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
+ Services.obs.removeObserver(this, "browser:purge-session-history");
+ break;
+ case "sessionstore-windows-restored":
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ // free _initialState after nsSessionStore is done with it
+ this._initialState = null;
+ break;
+ case "browser:purge-session-history":
+ Services.obs.removeObserver(this, "browser:purge-session-history");
+ // reset all state on sanitization
+ this._sessionType = Ci.nsISessionStartup.NO_SESSION;
+ break;
+ }
+ },
+
+/* ........ Public API ................*/
+
+ get onceInitialized() {
+ return gOnceInitializedDeferred.promise;
+ },
+
+ /**
+ * Get the session state as a jsval
+ */
+ get state() {
+ return this._initialState;
+ },
+
+ /**
+ * Determines whether there is a pending session restore. Should only be
+ * called after initialization has completed.
+ * @returns bool
+ */
+ doRestore: function sss_doRestore() {
+ return this._willRestore();
+ },
+
+ /**
+ * Determines whether automatic session restoration is enabled for this
+ * launch of the browser. This does not include crash restoration. In
+ * particular, if session restore is configured to restore only in case of
+ * crash, this method returns false.
+ * @returns bool
+ */
+ isAutomaticRestoreEnabled: function () {
+ return Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") ||
+ Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
+ },
+
+ /**
+ * Determines whether there is a pending session restore.
+ * @returns bool
+ */
+ _willRestore: function () {
+ return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
+ this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
+ },
+
+ /**
+ * Returns whether we will restore a session that ends up replacing the
+ * homepage. The browser uses this to not start loading the homepage if
+ * we're going to stop its load anyway shortly after.
+ *
+ * This is meant to be an optimization for the average case that loading the
+ * session file finishes before we may want to start loading the default
+ * homepage. Should this be called before the session file has been read it
+ * will just return false.
+ *
+ * @returns bool
+ */
+ get willOverrideHomepage() {
+ if (this._initialState && this._willRestore()) {
+ let windows = this._initialState.windows || null;
+ // If there are valid windows with not only pinned tabs, signal that we
+ // will override the default homepage by restoring a session.
+ return windows && windows.some(w => w.tabs.some(t => !t.pinned));
+ }
+ return false;
+ },
+
+ /**
+ * Get the type of pending session store, if any.
+ */
+ get sessionType() {
+ return this._sessionType;
+ },
+
+ /**
+ * Get whether the previous session crashed.
+ */
+ get previousSessionCrashed() {
+ return this._previousSessionCrashed;
+ },
+
+ /* ........ QueryInterface .............. */
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISessionStartup]),
+ classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}")
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]);
diff --git a/browser/components/sessionstore/nsSessionStore.js b/browser/components/sessionstore/nsSessionStore.js
new file mode 100644
index 000000000..8d96178ce
--- /dev/null
+++ b/browser/components/sessionstore/nsSessionStore.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Session Storage and Restoration
+ *
+ * Overview
+ * This service keeps track of a user's session, storing the various bits
+ * required to return the browser to its current state. The relevant data is
+ * stored in memory, and is periodically saved to disk in a file in the
+ * profile directory. The service is started at first window load, in
+ * delayedStartup, and will restore the session from the data received from
+ * the nsSessionStartup service.
+ */
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
+
+function SessionStoreService() {}
+
+// The SessionStore module's object is frozen. We need to modify our prototype
+// and add some properties so let's just copy the SessionStore object.
+Object.keys(SessionStore).forEach(function (aName) {
+ let desc = Object.getOwnPropertyDescriptor(SessionStore, aName);
+ Object.defineProperty(SessionStoreService.prototype, aName, desc);
+});
+
+SessionStoreService.prototype.classID =
+ Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}");
+SessionStoreService.prototype.QueryInterface =
+ XPCOMUtils.generateQI([Ci.nsISessionStore]);
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStoreService]);
diff --git a/browser/components/sessionstore/nsSessionStore.manifest b/browser/components/sessionstore/nsSessionStore.manifest
new file mode 100644
index 000000000..9b5819c6a
--- /dev/null
+++ b/browser/components/sessionstore/nsSessionStore.manifest
@@ -0,0 +1,15 @@
+# This component must restrict its registration for the app-startup category
+# to the specific list of apps that use it so it doesn't get loaded in xpcshell.
+# Thus we restrict it to these apps:
+#
+# b2g: {3c2e2abc-06d4-11e1-ac3b-374f68613e61}
+# browser: {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+# mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110}
+# mobile/xul: {a23983c0-fd0e-11dc-95ff-0800200c9a66}
+# graphene: {d1bfe7d9-c01e-4237-998b-7b5f960a4314}
+
+component {5280606b-2510-4fe0-97ef-9b5a22eafe6b} nsSessionStore.js
+contract @mozilla.org/browser/sessionstore;1 {5280606b-2510-4fe0-97ef-9b5a22eafe6b}
+component {ec7a6c20-e081-11da-8ad9-0800200c9a66} nsSessionStartup.js
+contract @mozilla.org/browser/sessionstartup;1 {ec7a6c20-e081-11da-8ad9-0800200c9a66}
+category app-startup nsSessionStartup service,@mozilla.org/browser/sessionstartup;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314}
diff --git a/browser/components/sessionstore/test/.eslintrc.js b/browser/components/sessionstore/test/.eslintrc.js
new file mode 100644
index 000000000..c764b133d
--- /dev/null
+++ b/browser/components/sessionstore/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/components/sessionstore/test/browser.ini b/browser/components/sessionstore/test/browser.ini
new file mode 100644
index 000000000..37154a0cc
--- /dev/null
+++ b/browser/components/sessionstore/test/browser.ini
@@ -0,0 +1,242 @@
+# 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/.
+
+# browser_506482.js is disabled because of frequent failures (bug 538672)
+# browser_526613.js is disabled because of frequent failures (bug 534489)
+# browser_589246.js is disabled for leaking browser windows (bug 752467)
+# browser_580512.js is disabled for leaking browser windows (bug 752467)
+
+[DEFAULT]
+support-files =
+ head.js
+ content.js
+ content-forms.js
+ browser_cookies.sjs
+ browser_formdata_sample.html
+ browser_formdata_xpath_sample.html
+ browser_frametree_sample.html
+ browser_frametree_sample_frameset.html
+ browser_frame_history_index.html
+ browser_frame_history_index2.html
+ browser_frame_history_index_blank.html
+ browser_frame_history_a.html
+ browser_frame_history_b.html
+ browser_frame_history_c.html
+ browser_frame_history_c1.html
+ browser_frame_history_c2.html
+ browser_form_restore_events_sample.html
+ browser_formdata_format_sample.html
+ browser_pageStyle_sample.html
+ browser_pageStyle_sample_nested.html
+ browser_sessionHistory_slow.sjs
+ browser_scrollPositions_sample.html
+ browser_scrollPositions_sample_frameset.html
+ browser_scrollPositions_readerModeArticle.html
+ browser_sessionStorage.html
+ browser_248970_b_sample.html
+ browser_339445_sample.html
+ browser_423132_sample.html
+ browser_447951_sample.html
+ browser_454908_sample.html
+ browser_456342_sample.xhtml
+ browser_463205_sample.html
+ browser_463206_sample.html
+ browser_466937_sample.html
+ browser_485482_sample.html
+ browser_637020_slow.sjs
+ browser_662743_sample.html
+ browser_739531_sample.html
+ browser_911547_sample.html
+ browser_911547_sample.html^headers^
+ restore_redirect_http.html
+ restore_redirect_http.html^headers^
+ restore_redirect_js.html
+ restore_redirect_target.html
+ browser_1234021_page.html
+
+#NB: the following are disabled
+# browser_464620_a.html
+# browser_464620_b.html
+# browser_464620_xd.html
+
+
+#disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html
+#disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html
+#disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html
+
+[browser_aboutPrivateBrowsing.js]
+[browser_aboutSessionRestore.js]
+[browser_async_duplicate_tab.js]
+[browser_async_flushes.js]
+run-if = e10s && crashreporter
+skip-if = debug # bug 1167933
+[browser_async_remove_tab.js]
+run-if = e10s
+skip-if = debug # bug 1211084
+[browser_attributes.js]
+[browser_backup_recovery.js]
+[browser_broadcast.js]
+[browser_capabilities.js]
+[browser_cleaner.js]
+[browser_cookies.js]
+[browser_crashedTabs.js]
+skip-if = !e10s || !crashreporter
+[browser_unrestored_crashedTabs.js]
+skip-if = !e10s || !crashreporter
+[browser_revive_crashed_bg_tabs.js]
+skip-if = !e10s || !crashreporter
+[browser_dying_cache.js]
+[browser_dynamic_frames.js]
+[browser_form_restore_events.js]
+[browser_formdata.js]
+[browser_formdata_cc.js]
+[browser_formdata_format.js]
+[browser_formdata_xpath.js]
+[browser_frametree.js]
+[browser_frame_history.js]
+[browser_global_store.js]
+[browser_history_persist.js]
+[browser_label_and_icon.js]
+[browser_merge_closed_tabs.js]
+[browser_page_title.js]
+[browser_pageStyle.js]
+[browser_pending_tabs.js]
+[browser_privatetabs.js]
+[browser_purge_shistory.js]
+skip-if = e10s # Bug 1271024
+[browser_replace_load.js]
+[browser_restore_redirect.js]
+[browser_restore_cookies_noOriginAttributes.js]
+[browser_scrollPositions.js]
+[browser_scrollPositionsReaderMode.js]
+[browser_sessionHistory.js]
+[browser_sessionStorage.js]
+[browser_sessionStorage_size.js]
+[browser_swapDocShells.js]
+[browser_switch_remoteness.js]
+run-if = e10s
+[browser_upgrade_backup.js]
+[browser_windowRestore_perwindowpb.js]
+[browser_248970_b_perwindowpb.js]
+# Disabled because of leaks.
+# Re-enabling and rewriting this test is tracked in bug 936919.
+skip-if = true
+[browser_339445.js]
+[browser_345898.js]
+[browser_350525.js]
+[browser_354894_perwindowpb.js]
+[browser_367052.js]
+[browser_393716.js]
+[browser_394759_basic.js]
+# Disabled for intermittent failures, bug 944372.
+skip-if = true
+[browser_394759_behavior.js]
+[browser_394759_perwindowpb.js]
+[browser_394759_purge.js]
+[browser_423132.js]
+[browser_447951.js]
+[browser_454908.js]
+[browser_456342.js]
+[browser_461634.js]
+[browser_463205.js]
+[browser_463206.js]
+[browser_464199.js]
+[browser_465215.js]
+[browser_465223.js]
+[browser_466937.js]
+[browser_467409-backslashplosion.js]
+[browser_477657.js]
+[browser_480893.js]
+[browser_485482.js]
+[browser_485563.js]
+[browser_490040.js]
+[browser_491168.js]
+[browser_491577.js]
+[browser_495495.js]
+[browser_500328.js]
+[browser_514751.js]
+[browser_522375.js]
+[browser_522545.js]
+[browser_524745.js]
+[browser_528776.js]
+[browser_579868.js]
+[browser_579879.js]
+skip-if = (os == 'linux' && e10s && (debug||asan)) # Bug 1234404
+[browser_581937.js]
+[browser_586147.js]
+[browser_586068-apptabs.js]
+[browser_586068-apptabs_ondemand.js]
+[browser_586068-browser_state_interrupted.js]
+[browser_586068-cascade.js]
+[browser_586068-multi_window.js]
+[browser_586068-reload.js]
+[browser_586068-select.js]
+[browser_586068-window_state.js]
+[browser_586068-window_state_override.js]
+[browser_588426.js]
+[browser_590268.js]
+[browser_590563.js]
+[browser_595601-restore_hidden.js]
+[browser_597071.js]
+skip-if = true # Needs to be rewritten as Marionette test, bug 995916
+[browser_599909.js]
+[browser_600545.js]
+[browser_601955.js]
+[browser_607016.js]
+[browser_615394-SSWindowState_events.js]
+[browser_618151.js]
+[browser_623779.js]
+[browser_624727.js]
+[browser_628270.js]
+[browser_635418.js]
+[browser_636279.js]
+[browser_637020.js]
+[browser_644409-scratchpads.js]
+[browser_645428.js]
+[browser_659591.js]
+[browser_662743.js]
+[browser_662812.js]
+[browser_665702-state_session.js]
+[browser_682507.js]
+[browser_687710.js]
+[browser_687710_2.js]
+[browser_694378.js]
+[browser_701377.js]
+[browser_705597.js]
+[browser_707862.js]
+[browser_739531.js]
+[browser_739805.js]
+[browser_819510_perwindowpb.js]
+skip-if = (os == 'win' && bits == 64) # Bug 1284312
+
+# Disabled for frequent intermittent failures
+[browser_464620_a.js]
+skip-if = true
+[browser_464620_b.js]
+skip-if = true
+
+# Disabled on OS X:
+[browser_625016.js]
+skip-if = os == "mac"
+
+[browser_911547.js]
+[browser_send_async_message_oom.js]
+[browser_multiple_navigateAndRestore.js]
+run-if = e10s
+[browser_async_window_flushing.js]
+[browser_forget_async_closings.js]
+[browser_newtab_userTypedValue.js]
+[browser_parentProcessRestoreHash.js]
+run-if = e10s
+[browser_sessionStoreContainer.js]
+[browser_windowStateContainer.js]
+[browser_1234021.js]
+[browser_remoteness_flip_on_restore.js]
+run-if = e10s
+[browser_background_tab_crash.js]
+run-if = e10s && crashreporter
+
+# Disabled on debug for frequent intermittent failures:
+[browser_undoCloseById.js]
+skip-if = debug
diff --git a/browser/components/sessionstore/test/browser_1234021.js b/browser/components/sessionstore/test/browser_1234021.js
new file mode 100644
index 000000000..a307d1e01
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_1234021.js
@@ -0,0 +1,18 @@
+"use strict";
+
+const PREF = 'network.cookie.cookieBehavior';
+const PAGE_URL = 'http://mochi.test:8888/browser/' +
+ 'browser/components/sessionstore/test/browser_1234021_page.html';
+const BEHAVIOR_REJECT = 2;
+
+add_task(function* test() {
+ yield pushPrefs([PREF, BEHAVIOR_REJECT]);
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: PAGE_URL
+ }, function* handler(aBrowser) {
+ yield TabStateFlusher.flush(aBrowser);
+ ok(true, "Flush didn't time out");
+ });
+});
diff --git a/browser/components/sessionstore/test/browser_1234021_page.html b/browser/components/sessionstore/test/browser_1234021_page.html
new file mode 100644
index 000000000..4a74fbc02
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_1234021_page.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+ <script>
+ sessionStorage
+ </script>
+</html>
diff --git a/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
new file mode 100644
index 000000000..f5775cd5b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
@@ -0,0 +1,166 @@
+/* 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/. */
+
+function test() {
+ /** Test (B) for Bug 248970 **/
+ waitForExplicitFinish();
+
+ let windowsToClose = [];
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ let filePath = file.path;
+ let fieldList = {
+ "//input[@name='input']": Date.now().toString(),
+ "//input[@name='spaced 1']": Math.random().toString(),
+ "//input[3]": "three",
+ "//input[@type='checkbox']": true,
+ "//input[@name='uncheck']": false,
+ "//input[@type='radio'][1]": false,
+ "//input[@type='radio'][2]": true,
+ "//input[@type='radio'][3]": false,
+ "//select": 2,
+ "//select[@multiple]": [1, 3],
+ "//textarea[1]": "",
+ "//textarea[2]": "Some text... " + Math.random(),
+ "//textarea[3]": "Some more text\n" + new Date(),
+ "//input[@type='file']": filePath
+ };
+
+ registerCleanupFunction(function* () {
+ for (let win of windowsToClose) {
+ yield BrowserTestUtils.closeWindow(win);
+ }
+ });
+
+ function test(aLambda) {
+ try {
+ return aLambda() || true;
+ } catch(ex) { }
+ return false;
+ }
+
+ function getElementByXPath(aTab, aQuery) {
+ let doc = aTab.linkedBrowser.contentDocument;
+ let xptype = Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE;
+ return doc.evaluate(aQuery, doc, null, xptype, null).singleNodeValue;
+ }
+
+ function setFormValue(aTab, aQuery, aValue) {
+ let node = getElementByXPath(aTab, aQuery);
+ if (typeof aValue == "string")
+ node.value = aValue;
+ else if (typeof aValue == "boolean")
+ node.checked = aValue;
+ else if (typeof aValue == "number")
+ node.selectedIndex = aValue;
+ else
+ Array.forEach(node.options, (aOpt, aIx) =>
+ (aOpt.selected = aValue.indexOf(aIx) > -1));
+ }
+
+ function compareFormValue(aTab, aQuery, aValue) {
+ let node = getElementByXPath(aTab, aQuery);
+ if (!node)
+ return false;
+ if (node instanceof Ci.nsIDOMHTMLInputElement)
+ return aValue == (node.type == "checkbox" || node.type == "radio" ?
+ node.checked : node.value);
+ if (node instanceof Ci.nsIDOMHTMLTextAreaElement)
+ return aValue == node.value;
+ if (!node.multiple)
+ return aValue == node.selectedIndex;
+ return Array.every(node.options, (aOpt, aIx) =>
+ (aValue.indexOf(aIx) > -1) == aOpt.selected);
+ }
+
+ //////////////////////////////////////////////////////////////////
+ // Test (B) : Session data restoration between windows //
+ //////////////////////////////////////////////////////////////////
+
+ let rootDir = getRootDirectory(gTestPath);
+ const testURL = rootDir + "browser_248970_b_sample.html";
+ const testURL2 = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_248970_b_sample.html";
+
+ whenNewWindowLoaded({ private: false }, function(aWin) {
+ windowsToClose.push(aWin);
+
+ // get closed tab count
+ let count = ss.getClosedTabCount(aWin);
+ let max_tabs_undo =
+ Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+ ok(0 <= count && count <= max_tabs_undo,
+ "getClosedTabCount should return zero or at most max_tabs_undo");
+
+ // setup a state for tab (A) so we can check later that is restored
+ let key = "key";
+ let value = "Value " + Math.random();
+ let state = { entries: [{ url: testURL }], extData: { key: value } };
+
+ // public session, add new tab: (A)
+ let tab_A = aWin.gBrowser.addTab(testURL);
+ ss.setTabState(tab_A, JSON.stringify(state));
+ promiseBrowserLoaded(tab_A.linkedBrowser).then(() => {
+ // make sure that the next closed tab will increase getClosedTabCount
+ Services.prefs.setIntPref(
+ "browser.sessionstore.max_tabs_undo", max_tabs_undo + 1)
+
+ // populate tab_A with form data
+ for (let i in fieldList)
+ setFormValue(tab_A, i, fieldList[i]);
+
+ // public session, close tab: (A)
+ aWin.gBrowser.removeTab(tab_A);
+
+ // verify that closedTabCount increased
+ ok(ss.getClosedTabCount(aWin) > count,
+ "getClosedTabCount has increased after closing a tab");
+
+ // verify tab: (A), in undo list
+ let tab_A_restored = test(() => ss.undoCloseTab(aWin, 0));
+ ok(tab_A_restored, "a tab is in undo list");
+ promiseTabRestored(tab_A_restored).then(() => {
+ is(testURL, tab_A_restored.linkedBrowser.currentURI.spec,
+ "it's the same tab that we expect");
+ aWin.gBrowser.removeTab(tab_A_restored);
+
+ whenNewWindowLoaded({ private: true }, function(aWin) {
+ windowsToClose.push(aWin);
+
+ // setup a state for tab (B) so we can check that its duplicated
+ // properly
+ let key1 = "key1";
+ let value1 = "Value " + Math.random();
+ let state1 = {
+ entries: [{ url: testURL2 }], extData: { key1: value1 }
+ };
+
+ let tab_B = aWin.gBrowser.addTab(testURL2);
+ promiseTabState(tab_B, state1).then(() => {
+ // populate tab: (B) with different form data
+ for (let item in fieldList)
+ setFormValue(tab_B, item, fieldList[item]);
+
+ // duplicate tab: (B)
+ let tab_C = aWin.gBrowser.duplicateTab(tab_B);
+ promiseTabRestored(tab_C).then(() => {
+ // verify the correctness of the duplicated tab
+ is(ss.getTabValue(tab_C, key1), value1,
+ "tab successfully duplicated - correct state");
+
+ for (let item in fieldList)
+ ok(compareFormValue(tab_C, item, fieldList[item]),
+ "The value for \"" + item + "\" was correctly duplicated");
+
+ // private browsing session, close tab: (C) and (B)
+ aWin.gBrowser.removeTab(tab_C);
+ aWin.gBrowser.removeTab(tab_B);
+
+ finish();
+ });
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_248970_b_sample.html b/browser/components/sessionstore/test/browser_248970_b_sample.html
new file mode 100644
index 000000000..76c3ae1aa
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_248970_b_sample.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<meta charset="utf-8">
+<title>Test for bug 248970</title>
+
+<h3>Text Fields</h3>
+<input type="text" name="input">
+<input type="text" name="spaced 1">
+<input>
+
+<h3>Checkboxes and Radio buttons</h3>
+<input type="checkbox" name="check"> Check 1
+<input type="checkbox" name="uncheck" checked> Check 2
+<p>
+<input type="radio" name="group" value="1"> Radio 1
+<input type="radio" name="group" value="some"> Radio 2
+<input type="radio" name="group" checked> Radio 3
+
+<h3>Selects</h3>
+<select name="any">
+ <option value="1"> Select 1
+ <option value="some"> Select 2
+ <option>Select 3
+</select>
+<select multiple="multiple">
+ <option value=1> Multi-select 1
+ <option value=2> Multi-select 2
+ <option value=3> Multi-select 3
+ <option value=4> Multi-select 4
+</select>
+
+<h3>Text Areas</h3>
+<textarea name="testarea"></textarea>
+<textarea name="sized one" rows="5" cols="25"></textarea>
+<textarea></textarea>
+
+<h3>File Selector</h3>
+<input type="file">
diff --git a/browser/components/sessionstore/test/browser_339445.js b/browser/components/sessionstore/test/browser_339445.js
new file mode 100644
index 000000000..c38a6cb18
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_339445.js
@@ -0,0 +1,32 @@
+/* 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/. */
+
+add_task(function* test() {
+ /** Test for Bug 339445 **/
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_339445_sample.html";
+
+ let tab = gBrowser.addTab(testURL);
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ yield ContentTask.spawn(tab.linkedBrowser, null, function() {
+ let doc = content.document;
+ is(doc.getElementById("storageTestItem").textContent, "PENDING",
+ "sessionStorage value has been set");
+ });
+
+ let tab2 = gBrowser.duplicateTab(tab);
+ yield promiseTabRestored(tab2);
+
+ yield ContentTask.spawn(tab2.linkedBrowser, null, function() {
+ let doc2 = content.document;
+ is(doc2.getElementById("storageTestItem").textContent, "SUCCESS",
+ "sessionStorage value has been duplicated");
+ });
+
+ // clean up
+ yield Promise.all([ BrowserTestUtils.removeTab(tab2),
+ BrowserTestUtils.removeTab(tab) ]);
+});
diff --git a/browser/components/sessionstore/test/browser_339445_sample.html b/browser/components/sessionstore/test/browser_339445_sample.html
new file mode 100644
index 000000000..32656a8d9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_339445_sample.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<meta charset="utf-8">
+<title>Test for bug 339445</title>
+
+storageTestItem = <span id="storageTestItem">FAIL</span>
+
+<!--
+ storageTestItem's textContent will be one of the following:
+ * FAIL : sessionStorage wasn't available
+ * PENDING : the test value has been initialized on first load
+ * SUCCESS : the test value was correctly retrieved
+-->
+
+<script type="application/javascript">
+ document.getElementById("storageTestItem").textContent =
+ sessionStorage["storageTestItem"] || "PENDING";
+ sessionStorage["storageTestItem"] = "SUCCESS";
+</script>
diff --git a/browser/components/sessionstore/test/browser_345898.js b/browser/components/sessionstore/test/browser_345898.js
new file mode 100644
index 000000000..bd4a46e69
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_345898.js
@@ -0,0 +1,44 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 345898 **/
+
+ function test(aLambda) {
+ try {
+ aLambda();
+ return false;
+ }
+ catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE" ||
+ ex.name == "NS_ERROR_FAILURE";
+ }
+ }
+
+ // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
+ ok(test(() => ss.getWindowState({})),
+ "Invalid window for getWindowState throws");
+ ok(test(() => ss.setWindowState({}, "", false)),
+ "Invalid window for setWindowState throws");
+ ok(test(() => ss.getTabState({})),
+ "Invalid tab for getTabState throws");
+ ok(test(() => ss.setTabState({}, "{}")),
+ "Invalid tab state for setTabState throws");
+ ok(test(() => ss.setTabState({}, JSON.stringify({ entries: [] }))),
+ "Invalid tab for setTabState throws");
+ ok(test(() => ss.duplicateTab({}, {})),
+ "Invalid tab for duplicateTab throws");
+ ok(test(() => ss.duplicateTab({}, gBrowser.selectedTab)),
+ "Invalid window for duplicateTab throws");
+ ok(test(() => ss.getClosedTabData({})),
+ "Invalid window for getClosedTabData throws");
+ ok(test(() => ss.undoCloseTab({}, 0)),
+ "Invalid window for undoCloseTab throws");
+ ok(test(() => ss.undoCloseTab(window, -1)),
+ "Invalid index for undoCloseTab throws");
+ ok(test(() => ss.getWindowValue({}, "")),
+ "Invalid window for getWindowValue throws");
+ ok(test(() => ss.setWindowValue({}, "", "")),
+ "Invalid window for setWindowValue throws");
+}
diff --git a/browser/components/sessionstore/test/browser_350525.js b/browser/components/sessionstore/test/browser_350525.js
new file mode 100644
index 000000000..1d87b3754
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_350525.js
@@ -0,0 +1,102 @@
+"use strict";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+})
+
+add_task(function* () {
+ /** Test for Bug 350525 **/
+
+ function test(aLambda) {
+ try {
+ return aLambda() || true;
+ }
+ catch (ex) { }
+ return false;
+ }
+
+ ////////////////////////////
+ // setWindowValue, et al. //
+ ////////////////////////////
+ let key = "Unique name: " + Date.now();
+ let value = "Unique value: " + Math.random();
+
+ // test adding
+ ok(test(() => ss.setWindowValue(window, key, value)), "set a window value");
+
+ // test retrieving
+ is(ss.getWindowValue(window, key), value, "stored window value matches original");
+
+ // test deleting
+ ok(test(() => ss.deleteWindowValue(window, key)), "delete the window value");
+
+ // value should not exist post-delete
+ is(ss.getWindowValue(window, key), "", "window value was deleted");
+
+ // test deleting a non-existent value
+ ok(test(() => ss.deleteWindowValue(window, key)), "delete non-existent window value");
+
+ /////////////////////////
+ // setTabValue, et al. //
+ /////////////////////////
+ key = "Unique name: " + Math.random();
+ value = "Unique value: " + Date.now();
+ let tab = gBrowser.addTab();
+ tab.linkedBrowser.stop();
+
+ // test adding
+ ok(test(() => ss.setTabValue(tab, key, value)), "store a tab value");
+
+ // test retrieving
+ is(ss.getTabValue(tab, key), value, "stored tab value match original");
+
+ // test deleting
+ ok(test(() => ss.deleteTabValue(tab, key)), "delete the tab value");
+
+ // value should not exist post-delete
+ is(ss.getTabValue(tab, key), "", "tab value was deleted");
+
+ // test deleting a non-existent value
+ ok(test(() => ss.deleteTabValue(tab, key)), "delete non-existent tab value");
+
+ // clean up
+ yield promiseRemoveTab(tab);
+
+ /////////////////////////////////////
+ // getClosedTabCount, undoCloseTab //
+ /////////////////////////////////////
+
+ // get closed tab count
+ let count = ss.getClosedTabCount(window);
+ let max_tabs_undo = gPrefService.getIntPref("browser.sessionstore.max_tabs_undo");
+ ok(0 <= count && count <= max_tabs_undo,
+ "getClosedTabCount returns zero or at most max_tabs_undo");
+
+ // create a new tab
+ let testURL = "about:";
+ tab = gBrowser.addTab(testURL);
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ // make sure that the next closed tab will increase getClosedTabCount
+ gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
+ registerCleanupFunction(() => gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo"));
+
+ // remove tab
+ yield promiseRemoveTab(tab);
+
+ // getClosedTabCount
+ let newcount = ss.getClosedTabCount(window);
+ ok(newcount > count, "after closing a tab, getClosedTabCount has been incremented");
+
+ // undoCloseTab
+ tab = test(() => ss.undoCloseTab(window, 0));
+ ok(tab, "undoCloseTab doesn't throw")
+
+ yield promiseTabRestored(tab);
+ is(tab.linkedBrowser.currentURI.spec, testURL, "correct tab was reopened");
+
+ // clean up
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_354894_perwindowpb.js b/browser/components/sessionstore/test/browser_354894_perwindowpb.js
new file mode 100644
index 000000000..bf80cd710
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_354894_perwindowpb.js
@@ -0,0 +1,474 @@
+/* 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/. */
+
+/**
+ * Checks that restoring the last browser window in session is actually
+ * working.
+ *
+ * @see https://bugzilla.mozilla.org/show_bug.cgi?id=354894
+ * @note It is implicitly tested that restoring the last window works when
+ * non-browser windows are around. The "Run Tests" window as well as the main
+ * browser window (wherein the test code gets executed) won't be considered
+ * browser windows. To achiveve this said main browser window has it's windowtype
+ * attribute modified so that it's not considered a browser window any longer.
+ * This is crucial, because otherwise there would be two browser windows around,
+ * said main test window and the one opened by the tests, and hence the new
+ * logic wouldn't be executed at all.
+ * @note Mac only tests the new notifications, as restoring the last window is
+ * not enabled on that platform (platform shim; the application is kept running
+ * although there are no windows left)
+ * @note There is a difference when closing a browser window with
+ * BrowserTryToCloseWindow() as opposed to close(). The former will make
+ * nsSessionStore restore a window next time it gets a chance and will post
+ * notifications. The latter won't.
+ */
+
+// Some urls that might be opened in tabs and/or popups
+// Do not use about:blank:
+// That one is reserved for special purposes in the tests
+const TEST_URLS = ["about:mozilla", "about:buildconfig"];
+
+// Number of -request notifications to except
+// remember to adjust when adding new tests
+const NOTIFICATIONS_EXPECTED = 6;
+
+// Window features of popup windows
+const POPUP_FEATURES = "toolbar=no,resizable=no,status=no";
+
+// Window features of browser windows
+const CHROME_FEATURES = "chrome,all,dialog=no";
+
+const IS_MAC = navigator.platform.match(/Mac/);
+
+/**
+ * Returns an Object with two properties:
+ * open (int):
+ * A count of how many non-closed navigator:browser windows there are.
+ * winstates (int):
+ * A count of how many windows there are in the SessionStore state.
+ */
+function getBrowserWindowsCount() {
+ let open = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++open;
+ }
+
+ let winstates = JSON.parse(ss.getBrowserState()).windows.length;
+
+ return { open, winstates };
+}
+
+add_task(function* setup() {
+ // Make sure we've only got one browser window to start with
+ let { open, winstates } = getBrowserWindowsCount();
+ is(open, 1, "Should only be one open window");
+ is(winstates, 1, "Should only be one window state in SessionStore");
+
+ // This test takes some time to run, and it could timeout randomly.
+ // So we require a longer timeout. See bug 528219.
+ requestLongerTimeout(3);
+
+ // Make the main test window not count as a browser window any longer
+ let oldWinType = document.documentElement.getAttribute("windowtype");
+ document.documentElement.setAttribute("windowtype", "navigator:testrunner");
+
+ registerCleanupFunction(() => {
+ document.documentElement.setAttribute("windowtype", "navigator:browser");
+ });
+});
+
+/**
+ * Sets up one of our tests by setting the right preferences, and
+ * then opening up a browser window preloaded with some tabs.
+ *
+ * @param options (Object)
+ * An object that can contain the following properties:
+ *
+ * private:
+ * Whether or not the opened window should be private.
+ *
+ * denyFirst:
+ * Whether or not the first window that attempts to close
+ * via closeWindowForRestoration should be denied.
+ *
+ * @param testFunction (Function*)
+ * A generator function that yields Promises to be run
+ * once the test has been set up.
+ *
+ * @returns Promise
+ * Resolves once the test has been cleaned up.
+ */
+let setupTest = Task.async(function*(options, testFunction) {
+ yield pushPrefs(["browser.startup.page", 3],
+ ["browser.tabs.warnOnClose", false]);
+
+ // Observe these, and also use to count the number of hits
+ let observing = {
+ "browser-lastwindow-close-requested": 0,
+ "browser-lastwindow-close-granted": 0
+ };
+
+ /**
+ * Helper: Will observe and handle the notifications for us
+ */
+ let hitCount = 0;
+ function observer(aCancel, aTopic, aData) {
+ // count so that we later may compare
+ observing[aTopic]++;
+
+ // handle some tests
+ if (options.denyFirst && ++hitCount == 1) {
+ aCancel.QueryInterface(Ci.nsISupportsPRBool).data = true;
+ }
+ }
+
+ for (let o in observing) {
+ Services.obs.addObserver(observer, o, false);
+ }
+
+ let private = options.private || false;
+ let newWin = yield promiseNewWindowLoaded({ private });
+
+ injectTestTabs(newWin);
+
+ yield testFunction(newWin, observing);
+
+ let count = getBrowserWindowsCount();
+ is(count.open, 0, "Got right number of open windows");
+ is(count.winstates, 1, "Got right number of stored window states");
+
+ for (let o in observing) {
+ Services.obs.removeObserver(observer, o);
+ }
+
+ yield popPrefs();
+});
+
+/**
+ * Loads a TEST_URLS into a browser window.
+ *
+ * @param win (Window)
+ * The browser window to load the tabs in
+ */
+function injectTestTabs(win) {
+ TEST_URLS.forEach(function (url) {
+ win.gBrowser.addTab(url);
+ });
+}
+
+/**
+ * Attempts to close a window via BrowserTryToCloseWindow so that
+ * we get the browser-lastwindow-close-requested and
+ * browser-lastwindow-close-granted observer notifications.
+ *
+ * @param win (Window)
+ * The window to try to close
+ * @returns Promise
+ * Resolves to true if the window closed, or false if the window
+ * was denied the ability to close.
+ */
+function closeWindowForRestoration(win) {
+ return new Promise((resolve) => {
+ let closePromise = BrowserTestUtils.windowClosed(win);
+ win.BrowserTryToCloseWindow();
+ if (!win.closed) {
+ resolve(false);
+ return;
+ }
+
+ closePromise.then(() => {
+ resolve(true);
+ });
+ });
+}
+
+/**
+ * Normal in-session restore
+ *
+ * @note: Non-Mac only
+ *
+ * Should do the following:
+ * 1. Open a new browser window
+ * 2. Add some tabs
+ * 3. Close that window
+ * 4. Opening another window
+ * 5. Checks that state is restored
+ */
+add_task(function* test_open_close_normal() {
+ if (IS_MAC) {
+ return;
+ }
+
+ yield setupTest({ denyFirst: true }, function*(newWin, obs) {
+ let closed = yield closeWindowForRestoration(newWin);
+ ok(!closed, "First close request should have been denied");
+
+ closed = yield closeWindowForRestoration(newWin);
+ ok(closed, "Second close request should be accepted");
+
+ newWin = yield promiseNewWindowLoaded();
+ is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
+ "Restored window in-session with otherpopup windows around");
+
+ // Note that this will not result in the the browser-lastwindow-close
+ // notifications firing for this other newWin.
+ yield BrowserTestUtils.closeWindow(newWin);
+
+ // setupTest gave us a window which was denied for closing once, and then
+ // closed.
+ is(obs["browser-lastwindow-close-requested"], 2,
+ "Got expected browser-lastwindow-close-requested notifications");
+ is(obs["browser-lastwindow-close-granted"], 1,
+ "Got expected browser-lastwindow-close-granted notifications");
+ });
+});
+
+/**
+ * PrivateBrowsing in-session restore
+ *
+ * @note: Non-Mac only
+ *
+ * Should do the following:
+ * 1. Open a new browser window A
+ * 2. Add some tabs
+ * 3. Close the window A as the last window
+ * 4. Open a private browsing window B
+ * 5. Make sure that B didn't restore the tabs from A
+ * 6. Close private browsing window B
+ * 7. Open a new window C
+ * 8. Make sure that new window C has restored tabs from A
+ */
+add_task(function* test_open_close_private_browsing() {
+ if (IS_MAC) {
+ return;
+ }
+
+ yield setupTest({}, function*(newWin, obs) {
+ let closed = yield closeWindowForRestoration(newWin);
+ ok(closed, "Should be able to close the window");
+
+ newWin = yield promiseNewWindowLoaded({private: true});
+ is(newWin.gBrowser.browsers.length, 1,
+ "Did not restore in private browing mode");
+
+ closed = yield closeWindowForRestoration(newWin);
+ ok(closed, "Should be able to close the window");
+
+ newWin = yield promiseNewWindowLoaded();
+ is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
+ "Restored tabs in a new non-private window");
+
+ // Note that this will not result in the the browser-lastwindow-close
+ // notifications firing for this other newWin.
+ yield BrowserTestUtils.closeWindow(newWin);
+
+ // We closed two windows with closeWindowForRestoration, and both
+ // should have been successful.
+ is(obs["browser-lastwindow-close-requested"], 2,
+ "Got expected browser-lastwindow-close-requested notifications");
+ is(obs["browser-lastwindow-close-granted"], 2,
+ "Got expected browser-lastwindow-close-granted notifications");
+ });
+});
+
+/**
+ * Open some popup windows to check those aren't restored, but the browser
+ * window is.
+ *
+ * @note: Non-Mac only
+ *
+ * Should do the following:
+ * 1. Open a new browser window
+ * 2. Add some tabs
+ * 3. Open some popups
+ * 4. Add another tab to one popup (so that it gets stored) and close it again
+ * 5. Close the browser window
+ * 6. Open another browser window
+ * 7. Make sure that the tabs of the closed browser window, but not the popup,
+ * are restored
+ */
+add_task(function* test_open_close_window_and_popup() {
+ if (IS_MAC) {
+ return;
+ }
+
+ yield setupTest({}, function*(newWin, obs) {
+ let popupPromise = BrowserTestUtils.waitForNewWindow();
+ openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[0]);
+ let popup = yield popupPromise;
+
+ let popup2Promise = BrowserTestUtils.waitForNewWindow();
+ openDialog(location, "popup2", POPUP_FEATURES, TEST_URLS[1]);
+ let popup2 = yield popup2Promise;
+
+ popup2.gBrowser.addTab(TEST_URLS[0]);
+
+ let closed = yield closeWindowForRestoration(newWin);
+ ok(closed, "Should be able to close the window");
+
+ yield BrowserTestUtils.closeWindow(popup2);
+
+ newWin = yield promiseNewWindowLoaded();
+
+ is(newWin.gBrowser.browsers.length, TEST_URLS.length + 2,
+ "Restored window and associated tabs in session");
+
+ yield BrowserTestUtils.closeWindow(popup);
+ yield BrowserTestUtils.closeWindow(newWin);
+
+ // We closed one window with closeWindowForRestoration, and it should
+ // have been successful.
+ is(obs["browser-lastwindow-close-requested"], 1,
+ "Got expected browser-lastwindow-close-requested notifications");
+ is(obs["browser-lastwindow-close-granted"], 1,
+ "Got expected browser-lastwindow-close-granted notifications");
+ });
+});
+
+/**
+ * Open some popup window to check it isn't restored. Instead nothing at all
+ * should be restored
+ *
+ * @note: Non-Mac only
+ *
+ * Should do the following:
+ * 1. Open a popup
+ * 2. Add another tab to the popup (so that it gets stored) and close it again
+ * 3. Open a window
+ * 4. Check that nothing at all is restored
+ * 5. Open two browser windows and close them again
+ * 6. undoCloseWindow() one
+ * 7. Open another browser window
+ * 8. Check that nothing at all is restored
+ */
+add_task(function* test_open_close_only_popup() {
+ if (IS_MAC) {
+ return;
+ }
+
+ yield setupTest({}, function*(newWin, obs) {
+ // We actually don't care about the initial window in this test.
+ yield BrowserTestUtils.closeWindow(newWin);
+
+ // This will cause nsSessionStore to restore a window the next time it
+ // gets a chance.
+ let popupPromise = BrowserTestUtils.waitForNewWindow();
+ openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
+ let popup = yield popupPromise;
+
+ is(popup.gBrowser.browsers.length, 1,
+ "Did not restore the popup window (1)");
+
+ let closed = yield closeWindowForRestoration(popup);
+ ok(closed, "Should be able to close the window");
+
+ popupPromise = BrowserTestUtils.waitForNewWindow();
+ openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]);
+ popup = yield popupPromise;
+
+ popup.gBrowser.addTab(TEST_URLS[0]);
+ is(popup.gBrowser.browsers.length, 2,
+ "Did not restore to the popup window (2)");
+
+ yield BrowserTestUtils.closeWindow(popup);
+
+ newWin = yield promiseNewWindowLoaded();
+ isnot(newWin.gBrowser.browsers.length, 2,
+ "Did not restore the popup window");
+ is(TEST_URLS.indexOf(newWin.gBrowser.browsers[0].currentURI.spec), -1,
+ "Did not restore the popup window (2)");
+ yield BrowserTestUtils.closeWindow(newWin);
+
+ // We closed one popup window with closeWindowForRestoration, and popup
+ // windows should never fire the browser-lastwindow notifications.
+ is(obs["browser-lastwindow-close-requested"], 0,
+ "Got expected browser-lastwindow-close-requested notifications");
+ is(obs["browser-lastwindow-close-granted"], 0,
+ "Got expected browser-lastwindow-close-granted notifications");
+ });
+});
+
+/**
+ * Open some windows and do undoCloseWindow. This should prevent any
+ * restoring later in the test
+ *
+ * @note: Non-Mac only
+ *
+ * Should do the following:
+ * 1. Open two browser windows and close them again
+ * 2. undoCloseWindow() one
+ * 3. Open another browser window
+ * 4. Make sure nothing at all is restored
+ */
+add_task(function* test_open_close_restore_from_popup() {
+ if (IS_MAC) {
+ return;
+ }
+
+ yield setupTest({}, function*(newWin, obs) {
+ let newWin2 = yield promiseNewWindowLoaded();
+ yield injectTestTabs(newWin2);
+
+ let closed = yield closeWindowForRestoration(newWin);
+ ok(closed, "Should be able to close the window");
+ closed = yield closeWindowForRestoration(newWin2);
+ ok(closed, "Should be able to close the window");
+
+ let counts = getBrowserWindowsCount();
+ is(counts.open, 0, "Got right number of open windows");
+ is(counts.winstates, 1, "Got right number of window states");
+
+ newWin = undoCloseWindow(0);
+ yield BrowserTestUtils.waitForEvent(newWin, "load");
+
+ // Make sure we wait until this window is restored.
+ yield BrowserTestUtils.waitForEvent(newWin.gBrowser.tabContainer,
+ "SSTabRestored");
+
+ newWin2 = yield promiseNewWindowLoaded();
+
+ is(newWin2.gBrowser.browsers.length, 1,
+ "Did not restore, as undoCloseWindow() was last called");
+ is(TEST_URLS.indexOf(newWin2.gBrowser.browsers[0].currentURI.spec), -1,
+ "Did not restore, as undoCloseWindow() was last called (2)");
+
+ counts = getBrowserWindowsCount();
+ is(counts.open, 2, "Got right number of open windows");
+ is(counts.winstates, 3, "Got right number of window states");
+
+ yield BrowserTestUtils.closeWindow(newWin);
+ yield BrowserTestUtils.closeWindow(newWin2);
+
+ counts = getBrowserWindowsCount();
+ is(counts.open, 0, "Got right number of open windows");
+ is(counts.winstates, 1, "Got right number of window states");
+ });
+});
+
+/**
+ * Test if closing can be denied on Mac.
+ * @note: Mac only
+ */
+add_task(function* test_mac_notifications() {
+ if (!IS_MAC) {
+ return;
+ }
+
+ yield setupTest({ denyFirst: true }, function*(newWin, obs) {
+ let closed = yield closeWindowForRestoration(newWin);
+ ok(!closed, "First close attempt should be denied");
+ closed = yield closeWindowForRestoration(newWin);
+ ok(closed, "Second close attempt should be granted");
+
+ // We tried closing once, and got denied. Then we tried again and
+ // succeeded. That means 2 close requests, and 1 close granted.
+ is(obs["browser-lastwindow-close-requested"], 2,
+ "Got expected browser-lastwindow-close-requested notifications");
+ is(obs["browser-lastwindow-close-granted"], 1,
+ "Got expected browser-lastwindow-close-granted notifications");
+ });
+});
+
diff --git a/browser/components/sessionstore/test/browser_367052.js b/browser/components/sessionstore/test/browser_367052.js
new file mode 100644
index 000000000..3cc89a66c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_367052.js
@@ -0,0 +1,41 @@
+/* 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/. */
+
+"use strict";
+
+add_task(function* () {
+ // make sure that the next closed tab will increase getClosedTabCount
+ let max_tabs_undo = gPrefService.getIntPref("browser.sessionstore.max_tabs_undo");
+ gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
+ registerCleanupFunction(() => gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo"));
+
+ // Empty the list of closed tabs.
+ while (ss.getClosedTabCount(window)) {
+ ss.forgetClosedTab(window, 0);
+ }
+
+ // restore a blank tab
+ let tab = gBrowser.addTab("about:");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ let count = yield promiseSHistoryCount(tab.linkedBrowser);
+ ok(count >= 1, "the new tab does have at least one history entry");
+
+ yield promiseTabState(tab, {entries: []});
+
+ // We may have a different sessionHistory object if the tab
+ // switched from non-remote to remote.
+ count = yield promiseSHistoryCount(tab.linkedBrowser);
+ is(count, 0, "the tab was restored without any history whatsoever");
+
+ yield promiseRemoveTab(tab);
+ is(ss.getClosedTabCount(window), 0,
+ "The closed blank tab wasn't added to Recently Closed Tabs");
+});
+
+function promiseSHistoryCount(browser) {
+ return ContentTask.spawn(browser, null, function* () {
+ return docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.count;
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_393716.js b/browser/components/sessionstore/test/browser_393716.js
new file mode 100644
index 000000000..c59bdcc8b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_393716.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = "about:config";
+
+/**
+ * Bug 393716 - Basic tests for getTabState(), setTabState(), and duplicateTab().
+ */
+add_task(function test_set_tabstate() {
+ let key = "Unique key: " + Date.now();
+ let value = "Unique value: " + Math.random();
+
+ // create a new tab
+ let tab = gBrowser.addTab(URL);
+ ss.setTabValue(tab, key, value);
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ // get the tab's state
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ let state = ss.getTabState(tab);
+ ok(state, "get the tab's state");
+
+ // verify the tab state's integrity
+ state = JSON.parse(state);
+ ok(state instanceof Object && state.entries instanceof Array && state.entries.length > 0,
+ "state object seems valid");
+ ok(state.entries.length == 1 && state.entries[0].url == URL,
+ "Got the expected state object (test URL)");
+ ok(state.extData && state.extData[key] == value,
+ "Got the expected state object (test manually set tab value)");
+
+ // clean up
+ gBrowser.removeTab(tab);
+});
+
+add_task(function test_set_tabstate_and_duplicate() {
+ let key2 = "key2";
+ let value2 = "Value " + Math.random();
+ let value3 = "Another value: " + Date.now();
+ let state = { entries: [{ url: URL }], extData: { key2: value2 } };
+
+ // create a new tab
+ let tab = gBrowser.addTab();
+ // set the tab's state
+ ss.setTabState(tab, JSON.stringify(state));
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ // verify the correctness of the restored tab
+ ok(ss.getTabValue(tab, key2) == value2 && tab.linkedBrowser.currentURI.spec == URL,
+ "the tab's state was correctly restored");
+
+ // add text data
+ yield setInputValue(tab.linkedBrowser, {id: "textbox", value: value3});
+
+ // duplicate the tab
+ let tab2 = ss.duplicateTab(window, tab);
+ yield promiseTabRestored(tab2);
+
+ // verify the correctness of the duplicated tab
+ ok(ss.getTabValue(tab2, key2) == value2 &&
+ tab2.linkedBrowser.currentURI.spec == URL,
+ "correctly duplicated the tab's state");
+ let textbox = yield getInputValue(tab2.linkedBrowser, {id: "textbox"});
+ is(textbox, value3, "also duplicated text data");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_394759_basic.js b/browser/components/sessionstore/test/browser_394759_basic.js
new file mode 100644
index 000000000..1b1650e27
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_394759_basic.js
@@ -0,0 +1,92 @@
+/* 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/. */
+
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf-8,<input%20id=txt>" +
+ "<input%20type=checkbox%20id=chk>";
+
+Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
+
+/**
+ * This test ensures that closing a window is a reversible action. We will
+ * close the the window, restore it and check that all data has been restored.
+ * This includes window-specific data as well as form data for tabs.
+ */
+function test() {
+ waitForExplicitFinish();
+
+ let uniqueKey = "bug 394759";
+ let uniqueValue = "unik" + Date.now();
+ let uniqueText = "pi != " + Math.random();
+
+ // Clear the list of closed windows.
+ forgetClosedWindows();
+
+ provideWindow(function onTestURLLoaded(newWin) {
+ newWin.gBrowser.addTab().linkedBrowser.stop();
+
+ // Mark the window with some unique data to be restored later on.
+ ss.setWindowValue(newWin, uniqueKey, uniqueValue);
+ let [txt, chk] = newWin.content.document.querySelectorAll("#txt, #chk");
+ txt.value = uniqueText;
+
+ let browser = newWin.gBrowser.selectedBrowser;
+ setInputChecked(browser, {id: "chk", checked: true}).then(() => {
+ BrowserTestUtils.closeWindow(newWin).then(() => {
+ is(ss.getClosedWindowCount(), 1,
+ "The closed window was added to Recently Closed Windows");
+
+ let data = SessionStore.getClosedWindowData(false);
+
+ // Verify that non JSON serialized data is the same as JSON serialized data.
+ is(JSON.stringify(data), ss.getClosedWindowData(),
+ "Non-serialized data is the same as serialized data")
+
+ ok(data[0].title == TEST_URL && JSON.stringify(data[0]).indexOf(uniqueText) > -1,
+ "The closed window data was stored correctly");
+
+ // Reopen the closed window and ensure its integrity.
+ let newWin2 = ss.undoCloseWindow(0);
+
+ ok(newWin2 instanceof ChromeWindow,
+ "undoCloseWindow actually returned a window");
+ is(ss.getClosedWindowCount(), 0,
+ "The reopened window was removed from Recently Closed Windows");
+
+ // SSTabRestored will fire more than once, so we need to make sure we count them.
+ let restoredTabs = 0;
+ let expectedTabs = data[0].tabs.length;
+ newWin2.addEventListener("SSTabRestored", function sstabrestoredListener(aEvent) {
+ ++restoredTabs;
+ info("Restored tab " + restoredTabs + "/" + expectedTabs);
+ if (restoredTabs < expectedTabs) {
+ return;
+ }
+
+ is(restoredTabs, expectedTabs, "Correct number of tabs restored");
+ newWin2.removeEventListener("SSTabRestored", sstabrestoredListener, true);
+
+ is(newWin2.gBrowser.tabs.length, 2,
+ "The window correctly restored 2 tabs");
+ is(newWin2.gBrowser.currentURI.spec, TEST_URL,
+ "The window correctly restored the URL");
+
+ let [txt, chk] = newWin2.content.document.querySelectorAll("#txt, #chk");
+ ok(txt.value == uniqueText && chk.checked,
+ "The window correctly restored the form");
+ is(ss.getWindowValue(newWin2, uniqueKey), uniqueValue,
+ "The window correctly restored the data associated with it");
+
+ // Clean up.
+ BrowserTestUtils.closeWindow(newWin2).then(finish);
+ }, true);
+ });
+ });
+ }, TEST_URL);
+}
+
+function setInputChecked(browser, data) {
+ return sendMessage(browser, "ss-test:setInputChecked", data);
+}
diff --git a/browser/components/sessionstore/test/browser_394759_behavior.js b/browser/components/sessionstore/test/browser_394759_behavior.js
new file mode 100644
index 000000000..aa74dc061
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_394759_behavior.js
@@ -0,0 +1,76 @@
+/**
+ * Test helper function that opens a series of windows, closes them
+ * and then checks the closed window data from SessionStore against
+ * expected results.
+ *
+ * @param windowsToOpen (Array)
+ * An array of Objects, where each object must define a single
+ * property "isPopup" for whether or not the opened window should
+ * be a popup.
+ * @param expectedResults (Array)
+ * An Object with two properies: mac and other, where each points
+ * at yet another Object, with the following properties:
+ *
+ * popup (int):
+ * The number of popup windows we expect to be in the closed window
+ * data.
+ * normal (int):
+ * The number of normal windows we expect to be in the closed window
+ * data.
+ * @returns Promise
+ */
+function testWindows(windowsToOpen, expectedResults) {
+ return Task.spawn(function*() {
+ for (let winData of windowsToOpen) {
+ let features = "chrome,dialog=no," +
+ (winData.isPopup ? "all=no" : "all");
+ let url = "http://example.com/?window=" + windowsToOpen.length;
+
+ let openWindowPromise = BrowserTestUtils.waitForNewWindow(true, url);
+ openDialog(getBrowserURL(), "", features, url);
+ let win = yield openWindowPromise;
+ yield BrowserTestUtils.closeWindow(win);
+ }
+
+ let closedWindowData = JSON.parse(ss.getClosedWindowData());
+ let numPopups = closedWindowData.filter(function(el, i, arr) {
+ return el.isPopup;
+ }).length;
+ let numNormal = ss.getClosedWindowCount() - numPopups;
+ // #ifdef doesn't work in browser-chrome tests, so do a simple regex on platform
+ let oResults = navigator.platform.match(/Mac/) ? expectedResults.mac
+ : expectedResults.other;
+ is(numPopups, oResults.popup,
+ "There were " + oResults.popup + " popup windows to reopen");
+ is(numNormal, oResults.normal,
+ "There were " + oResults.normal + " normal windows to repoen");
+ });
+}
+
+add_task(function* test_closed_window_states() {
+ // This test takes quite some time, and timeouts frequently, so we require
+ // more time to run.
+ // See Bug 518970.
+ requestLongerTimeout(2);
+
+ let windowsToOpen = [{isPopup: false},
+ {isPopup: false},
+ {isPopup: true},
+ {isPopup: true},
+ {isPopup: true}];
+ let expectedResults = {mac: {popup: 3, normal: 0},
+ other: {popup: 3, normal: 1}};
+
+ yield testWindows(windowsToOpen, expectedResults);
+
+
+ let windowsToOpen2 = [{isPopup: false},
+ {isPopup: false},
+ {isPopup: false},
+ {isPopup: false},
+ {isPopup: false}];
+ let expectedResults2 = {mac: {popup: 0, normal: 3},
+ other: {popup: 0, normal: 3}};
+
+ yield testWindows(windowsToOpen2, expectedResults2);
+}); \ No newline at end of file
diff --git a/browser/components/sessionstore/test/browser_394759_perwindowpb.js b/browser/components/sessionstore/test/browser_394759_perwindowpb.js
new file mode 100644
index 000000000..83eec3070
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_394759_perwindowpb.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+"use strict";
+
+const TESTS = [
+ { url: "about:config",
+ key: "bug 394759 Non-PB",
+ value: "uniq" + r() },
+ { url: "about:mozilla",
+ key: "bug 394759 PB",
+ value: "uniq" + r() },
+];
+
+function promiseTestOpenCloseWindow(aIsPrivate, aTest) {
+ return Task.spawn(function*() {
+ let win = yield BrowserTestUtils.openNewBrowserWindow({ "private": aIsPrivate });
+ win.gBrowser.selectedBrowser.loadURI(aTest.url);
+ yield promiseBrowserLoaded(win.gBrowser.selectedBrowser);
+ yield Promise.resolve();
+ // Mark the window with some unique data to be restored later on.
+ ss.setWindowValue(win, aTest.key, aTest.value);
+ yield TabStateFlusher.flushWindow(win);
+ // Close.
+ yield BrowserTestUtils.closeWindow(win);
+ });
+}
+
+function promiseTestOnWindow(aIsPrivate, aValue) {
+ return Task.spawn(function*() {
+ let win = yield BrowserTestUtils.openNewBrowserWindow({ "private": aIsPrivate });
+ yield TabStateFlusher.flushWindow(win);
+ let data = JSON.parse(ss.getClosedWindowData())[0];
+ is(ss.getClosedWindowCount(), 1, "Check that the closed window count hasn't changed");
+ ok(JSON.stringify(data).indexOf(aValue) > -1,
+ "Check the closed window data was stored correctly");
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+ });
+}
+
+add_task(function* init() {
+ forgetClosedWindows();
+ while (ss.getClosedTabCount(window) > 0) {
+ ss.forgetClosedTab(window, 0);
+ }
+});
+
+add_task(function* main() {
+ yield promiseTestOpenCloseWindow(false, TESTS[0]);
+ yield promiseTestOpenCloseWindow(true, TESTS[1]);
+ yield promiseTestOnWindow(false, TESTS[0].value);
+ yield promiseTestOnWindow(true, TESTS[0].value);
+});
+
diff --git a/browser/components/sessionstore/test/browser_394759_purge.js b/browser/components/sessionstore/test/browser_394759_purge.js
new file mode 100644
index 000000000..75144aba1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_394759_purge.js
@@ -0,0 +1,130 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm");
+
+function waitForClearHistory(aCallback) {
+ let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(this, "browser:purge-domain-data");
+ setTimeout(aCallback, 0);
+ }
+ };
+ Services.obs.addObserver(observer, "browser:purge-domain-data", false);
+}
+
+function test() {
+ waitForExplicitFinish();
+ // utility functions
+ function countClosedTabsByTitle(aClosedTabList, aTitle) {
+ return aClosedTabList.filter(aData => aData.title == aTitle).length;
+ }
+
+ function countOpenTabsByTitle(aOpenTabList, aTitle) {
+ return aOpenTabList.filter(aData => aData.entries.some(aEntry => aEntry.title == aTitle)).length;
+ }
+
+ // backup old state
+ let oldState = ss.getBrowserState();
+ let oldState_wins = JSON.parse(oldState).windows.length;
+ if (oldState_wins != 1)
+ ok(false, "oldState in test_purge has " + oldState_wins + " windows instead of 1");
+
+ // create a new state for testing
+ const REMEMBER = Date.now(), FORGET = Math.random();
+ let testState = {
+ windows: [ { tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 } ],
+ _closedWindows : [
+ // _closedWindows[0]
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com/", title: REMEMBER }] },
+ { entries: [{ url: "http://mozilla.org/", title: FORGET }] }
+ ],
+ selected: 2,
+ title: "mozilla.org",
+ _closedTabs: []
+ },
+ // _closedWindows[1]
+ {
+ tabs: [
+ { entries: [{ url: "http://mozilla.org/", title: FORGET }] },
+ { entries: [{ url: "http://example.com/", title: REMEMBER }] },
+ { entries: [{ url: "http://example.com/", title: REMEMBER }] },
+ { entries: [{ url: "http://mozilla.org/", title: FORGET }] },
+ { entries: [{ url: "http://example.com/", title: REMEMBER }] }
+ ],
+ selected: 5,
+ _closedTabs: []
+ },
+ // _closedWindows[2]
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com/", title: REMEMBER }] }
+ ],
+ selected: 1,
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ { url: "http://mozilla.org/", title: FORGET },
+ { url: "http://mozilla.org/again", title: "doesn't matter" }
+ ]
+ },
+ pos: 1,
+ title: FORGET
+ },
+ {
+ state: {
+ entries: [
+ { url: "http://example.com", title: REMEMBER }
+ ]
+ },
+ title: REMEMBER
+ }
+ ]
+ }
+ ]
+ };
+
+ // set browser to test state
+ ss.setBrowserState(JSON.stringify(testState));
+
+ // purge domain & check that we purged correctly for closed windows
+ ForgetAboutSite.removeDataFromDomain("mozilla.org");
+ waitForClearHistory(function() {
+ let closedWindowData = JSON.parse(ss.getClosedWindowData());
+
+ // First set of tests for _closedWindows[0] - tests basics
+ let win = closedWindowData[0];
+ is(win.tabs.length, 1, "1 tab was removed");
+ is(countOpenTabsByTitle(win.tabs, FORGET), 0,
+ "The correct tab was removed");
+ is(countOpenTabsByTitle(win.tabs, REMEMBER), 1,
+ "The correct tab was remembered");
+ is(win.selected, 1, "Selected tab has changed");
+ is(win.title, REMEMBER, "The window title was correctly updated");
+
+ // Test more complicated case
+ win = closedWindowData[1];
+ is(win.tabs.length, 3, "2 tabs were removed");
+ is(countOpenTabsByTitle(win.tabs, FORGET), 0,
+ "The correct tabs were removed");
+ is(countOpenTabsByTitle(win.tabs, REMEMBER), 3,
+ "The correct tabs were remembered");
+ is(win.selected, 3, "Selected tab has changed");
+ is(win.title, REMEMBER, "The window title was correctly updated");
+
+ // Tests handling of _closedTabs
+ win = closedWindowData[2];
+ is(countClosedTabsByTitle(win._closedTabs, REMEMBER), 1,
+ "The correct number of tabs were removed, and the correct ones");
+ is(countClosedTabsByTitle(win._closedTabs, FORGET), 0,
+ "All tabs to be forgotten were indeed removed");
+
+ // restore pre-test state
+ ss.setBrowserState(oldState);
+ finish();
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_423132.js b/browser/components/sessionstore/test/browser_423132.js
new file mode 100644
index 000000000..584002cff
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_423132.js
@@ -0,0 +1,59 @@
+"use strict";
+
+/**
+ * Tests that cookies are stored and restored correctly
+ * by sessionstore (bug 423132).
+ */
+add_task(function*() {
+ const testURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_423132_sample.html";
+
+ Services.cookies.removeAll();
+ // make sure that sessionstore.js can be forced to be created by setting
+ // the interval pref to 0
+ yield SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.interval", 0]]
+ });
+
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+ let browser = win.gBrowser.selectedBrowser;
+ browser.loadURI(testURL);
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+
+ // get the sessionstore state for the window
+ let state = ss.getWindowState(win);
+
+ // verify our cookie got set during pageload
+ let enumerator = Services.cookies.enumerator;
+ let cookie;
+ let i = 0;
+ while (enumerator.hasMoreElements()) {
+ cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
+ i++;
+ }
+ Assert.equal(i, 1, "expected one cookie");
+
+ // remove the cookie
+ Services.cookies.removeAll();
+
+ // restore the window state
+ ss.setWindowState(win, state, true);
+
+ // at this point, the cookie should be restored...
+ enumerator = Services.cookies.enumerator;
+ let cookie2;
+ while (enumerator.hasMoreElements()) {
+ cookie2 = enumerator.getNext().QueryInterface(Ci.nsICookie);
+ if (cookie.name == cookie2.name)
+ break;
+ }
+ is(cookie.name, cookie2.name, "cookie name successfully restored");
+ is(cookie.value, cookie2.value, "cookie value successfully restored");
+ is(cookie.path, cookie2.path, "cookie path successfully restored");
+
+ // clean up
+ Services.cookies.removeAll();
+ yield BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/sessionstore/test/browser_423132_sample.html b/browser/components/sessionstore/test/browser_423132_sample.html
new file mode 100644
index 000000000..6ff7e7aa3
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_423132_sample.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script type="text/javascript">
+ // generate an enormous random number...
+ var r = Math.floor(Math.random() * Math.pow(2, 62)).toString();
+
+ // ... and use it to set a randomly named cookie
+ document.cookie = r + "=value; path=/ohai";
+ </script>
+<body>
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_447951.js b/browser/components/sessionstore/test/browser_447951.js
new file mode 100644
index 000000000..a7b6a5ee8
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_447951.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 447951 **/
+
+ waitForExplicitFinish();
+ const baseURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_447951_sample.html#";
+
+ // Make sure the functionality added in bug 943339 doesn't affect the results
+ gPrefService.setIntPref("browser.sessionstore.max_serialize_back", -1);
+ gPrefService.setIntPref("browser.sessionstore.max_serialize_forward", -1);
+ registerCleanupFunction(function () {
+ gPrefService.clearUserPref("browser.sessionstore.max_serialize_back");
+ gPrefService.clearUserPref("browser.sessionstore.max_serialize_forward");
+ });
+
+ let tab = gBrowser.addTab();
+ promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+ let tabState = { entries: [] };
+ let max_entries = gPrefService.getIntPref("browser.sessionhistory.max_entries");
+ for (let i = 0; i < max_entries; i++)
+ tabState.entries.push({ url: baseURL + i });
+
+ promiseTabState(tab, tabState).then(() => {
+ return TabStateFlusher.flush(tab.linkedBrowser);
+ }).then(() => {
+ tabState = JSON.parse(ss.getTabState(tab));
+ is(tabState.entries.length, max_entries, "session history filled to the limit");
+ is(tabState.entries[0].url, baseURL + 0, "... but not more");
+
+ // visit yet another anchor (appending it to session history)
+ ContentTask.spawn(tab.linkedBrowser, null, function() {
+ content.window.document.querySelector("a").click();
+ }).then(flushAndCheck);
+
+ function flushAndCheck() {
+ TabStateFlusher.flush(tab.linkedBrowser).then(check);
+ }
+
+ function check() {
+ tabState = JSON.parse(ss.getTabState(tab));
+ if (tab.linkedBrowser.currentURI.spec != baseURL + "end") {
+ // It may take a few passes through the event loop before we
+ // get the right URL.
+ executeSoon(flushAndCheck);
+ return;
+ }
+
+ is(tab.linkedBrowser.currentURI.spec, baseURL + "end",
+ "the new anchor was loaded");
+ is(tabState.entries[tabState.entries.length - 1].url, baseURL + "end",
+ "... and ignored");
+ is(tabState.entries[0].url, baseURL + 1,
+ "... and the first item was removed");
+
+ // clean up
+ gBrowser.removeTab(tab);
+ finish();
+ }
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_447951_sample.html b/browser/components/sessionstore/test/browser_447951_sample.html
new file mode 100644
index 000000000..00282f25e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_447951_sample.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Testcase for bug 447951</title>
+
+<a href="#end">click me</a>
diff --git a/browser/components/sessionstore/test/browser_454908.js b/browser/components/sessionstore/test/browser_454908.js
new file mode 100644
index 000000000..fb8206e2f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_454908.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_454908_sample.html";
+const PASS = "pwd-" + Math.random();
+
+/**
+ * Bug 454908 - Don't save/restore values of password fields.
+ */
+add_task(function* test_dont_save_passwords() {
+ // Make sure we do save form data.
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+
+ // Add a tab with a password field.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Fill in some values.
+ let usernameValue = "User " + Math.random();
+ yield setInputValue(browser, {id: "username", value: usernameValue});
+ yield setInputValue(browser, {id: "passwd", value: PASS});
+
+ // Close and restore the tab.
+ yield promiseRemoveTab(tab);
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ yield promiseTabRestored(tab);
+
+ // Check that password fields aren't saved/restored.
+ let username = yield getInputValue(browser, {id: "username"});
+ is(username, usernameValue, "username was saved/restored");
+ let passwd = yield getInputValue(browser, {id: "passwd"});
+ is(passwd, "", "password wasn't saved/restored");
+
+ // Write to disk and read our file.
+ yield forceSaveState();
+ yield promiseForEachSessionRestoreFile((state, key) =>
+ // Ensure that we have not saved our password.
+ ok(!state.includes(PASS), "password has not been written to file " + key)
+ );
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_454908_sample.html b/browser/components/sessionstore/test/browser_454908_sample.html
new file mode 100644
index 000000000..02f40bf20
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_454908_sample.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<title>Test for bug 454908</title>
+
+<h3>Dummy Login</h3>
+<form>
+<p>Username: <input type="text" id="username">
+<p>Password: <input type="password" id="passwd">
+</form>
diff --git a/browser/components/sessionstore/test/browser_456342.js b/browser/components/sessionstore/test/browser_456342.js
new file mode 100644
index 000000000..d7ed33ee5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_456342.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_456342_sample.xhtml";
+
+/**
+ * Bug 456342 - Restore values from non-standard input field types.
+ */
+add_task(function test_restore_nonstandard_input_values() {
+ // Add tab with various non-standard input field types.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Fill in form values.
+ let expectedValue = Math.random();
+ yield setFormElementValues(browser, {value: expectedValue});
+
+ // Remove tab and check collected form data.
+ yield promiseRemoveTab(tab);
+ let undoItems = JSON.parse(ss.getClosedTabData(window));
+ let savedFormData = undoItems[0].state.formdata;
+
+ let countGood = 0, countBad = 0;
+ for (let id of Object.keys(savedFormData.id)) {
+ if (savedFormData.id[id] == expectedValue) {
+ countGood++;
+ } else {
+ countBad++;
+ }
+ }
+
+ for (let exp of Object.keys(savedFormData.xpath)) {
+ if (savedFormData.xpath[exp] == expectedValue) {
+ countGood++;
+ } else {
+ countBad++;
+ }
+ }
+
+ is(countGood, 4, "Saved text for non-standard input fields");
+ is(countBad, 0, "Didn't save text for ignored field types");
+});
+
+function setFormElementValues(browser, data) {
+ return sendMessage(browser, "ss-test:setFormElementValues", data);
+}
diff --git a/browser/components/sessionstore/test/browser_456342_sample.xhtml b/browser/components/sessionstore/test/browser_456342_sample.xhtml
new file mode 100644
index 000000000..a08777a8d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_456342_sample.xhtml
@@ -0,0 +1,36 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head><title>Test for bug 456342</title></head>
+
+<body>
+<form>
+<h3>Non-standard &lt;input&gt;s</h3>
+<p>Search <input type="search" id="searchTerm"/></p>
+<p>Image Search: <input type="image search" /></p>
+<p>Autocomplete: <input type="autocomplete" name="fill-in"/></p>
+<p>Mistyped: <input type="txet" name="mistyped"/></p>
+
+<h3>Ignored types</h3>
+<input type="hidden" name="hideme"/>
+<input type="HIDDEN" name="hideme2"/>
+<input type="submit" name="submit"/>
+<input type="reset" name="reset"/>
+<input type="image" name="image"/>
+<input type="button" name="button"/>
+<input type="password" name="password"/>
+<input type="PassWord" name="password2"/>
+<input type="PASSWORD" name="password3"/>
+<input autocomplete="off" name="auto1"/>
+<input type="text" autocomplete="OFF" name="auto2"/>
+<textarea autocomplete="off" name="auto3"/>
+<select autocomplete="off" name="auto4">
+ <option value="1" selected="true"/>
+ <option value="2"/>
+ <option value="3"/>
+</select>
+</form>
+
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_459906.js b/browser/components/sessionstore/test/browser_459906.js
new file mode 100644
index 000000000..cadab3e5c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_459906.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 459906 **/
+
+ waitForExplicitFinish();
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_459906_sample.html";
+ let uniqueValue = "<b>Unique:</b> " + Date.now();
+
+ var frameCount = 0;
+ let tab = gBrowser.addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 2)
+ return;
+ tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
+
+ let iframes = tab.linkedBrowser.contentWindow.frames;
+ iframes[1].document.body.innerHTML = uniqueValue;
+
+ frameCount = 0;
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener("load", function(aEvent) {
+ // wait for all frames to load (and reload!) completely
+ if (frameCount++ < 2)
+ return;
+ tab2.linkedBrowser.removeEventListener("load", arguments.callee, true);
+
+ executeSoon(function() {
+ let iframes = tab2.linkedBrowser.contentWindow.frames;
+ if (iframes[1].document.body.innerHTML !== uniqueValue) {
+ // Poll again the value, since we can't ensure to run
+ // after SessionStore has injected innerHTML value.
+ // See bug 521802.
+ info("Polling for innerHTML value");
+ setTimeout(arguments.callee, 100);
+ return;
+ }
+
+ is(iframes[1].document.body.innerHTML, uniqueValue,
+ "rich textarea's content correctly duplicated");
+
+ let innerDomain = null;
+ try {
+ innerDomain = iframes[0].document.domain;
+ }
+ catch (ex) { /* throws for chrome: documents */ }
+ is(innerDomain, "mochi.test", "XSS exploit prevented!");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ });
+ }, true);
+ }, true);
+}
diff --git a/browser/components/sessionstore/test/browser_459906_empty.html b/browser/components/sessionstore/test/browser_459906_empty.html
new file mode 100644
index 000000000..e01aaa339
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_459906_empty.html
@@ -0,0 +1,3 @@
+<title>Cross Domain File for bug 459906</title>
+
+cheers from localhost
diff --git a/browser/components/sessionstore/test/browser_459906_sample.html b/browser/components/sessionstore/test/browser_459906_sample.html
new file mode 100644
index 000000000..39b789776
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_459906_sample.html
@@ -0,0 +1,41 @@
+<!-- Testcase originally by David Bloom <bloom@google.com> -->
+
+<!DOCTYPE html>
+<title>Test for bug 459906</title>
+
+<body>
+<iframe src="data:text/html;charset=utf-8,not_on_localhost"></iframe>
+<iframe></iframe>
+
+<script type="application/javascript">
+ var loadCount = 0;
+ frames[0].addEventListener("DOMContentLoaded", handleLoad, false);
+ frames[1].addEventListener("DOMContentLoaded", handleLoad, false);
+ function handleLoad() {
+ if (++loadCount < 2)
+ return;
+ frames[0].removeEventListener("DOMContentLoaded", handleLoad, false);
+ frames[1].removeEventListener("DOMContentLoaded", handleLoad, false);
+ frames[0].document.designMode = "on";
+ frames[0].document.__defineGetter__("designMode", function() {
+ // inject a cross domain file ...
+ var documentInjected = false;
+ document.getElementsByTagName("iframe")[0].onload =
+ function() { documentInjected = true; };
+ frames[0].location = "browser_459906_empty.html";
+
+ // ... and ensure that it has time to load
+ for (var c = 0; !documentInjected && c < 20; c++) {
+ var r = new XMLHttpRequest();
+ r.open("GET", location.href, false);
+ r.overrideMimeType("text/plain");
+ r.send(null);
+ }
+
+ return "on";
+ });
+
+ frames[1].document.designMode = "on";
+ };
+</script>
+</body>
diff --git a/browser/components/sessionstore/test/browser_461634.js b/browser/components/sessionstore/test/browser_461634.js
new file mode 100644
index 000000000..01d3a4b0d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_461634.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
+
+function test() {
+ /** Test for Bug 461634 **/
+
+ waitForExplicitFinish();
+
+ const REMEMBER = Date.now(), FORGET = Math.random();
+ let test_state = { windows: [{ "tabs": [{ "entries": [] }], _closedTabs: [
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: REMEMBER },
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: REMEMBER },
+ ] }] };
+ let remember_count = 2;
+
+ function countByTitle(aClosedTabList, aTitle) {
+ return aClosedTabList.filter(aData => aData.title == aTitle).length;
+ }
+
+ function testForError(aFunction) {
+ try {
+ aFunction();
+ return false;
+ }
+ catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE";
+ }
+ }
+
+ // Open a window and add the above closed tab list.
+ let newWin = openDialog(location, "", "chrome,all,dialog=no");
+ promiseWindowLoaded(newWin).then(() => {
+ gPrefService.setIntPref("browser.sessionstore.max_tabs_undo",
+ test_state.windows[0]._closedTabs.length);
+ ss.setWindowState(newWin, JSON.stringify(test_state), true);
+
+ let closedTabs = SessionStore.getClosedTabData(newWin, false);
+
+ // Verify that non JSON serialized data is the same as JSON serialized data.
+ is(JSON.stringify(closedTabs), SessionStore.getClosedTabData(newWin),
+ "Non-serialized data is the same as serialized data")
+
+ is(closedTabs.length, test_state.windows[0]._closedTabs.length,
+ "Closed tab list has the expected length");
+ is(countByTitle(closedTabs, FORGET),
+ test_state.windows[0]._closedTabs.length - remember_count,
+ "The correct amout of tabs are to be forgotten");
+ is(countByTitle(closedTabs, REMEMBER), remember_count,
+ "Everything is set up");
+
+ // All of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE.
+ ok(testForError(() => ss.forgetClosedTab({}, 0)),
+ "Invalid window for forgetClosedTab throws");
+ ok(testForError(() => ss.forgetClosedTab(newWin, -1)),
+ "Invalid tab for forgetClosedTab throws");
+ ok(testForError(() => ss.forgetClosedTab(newWin, test_state.windows[0]._closedTabs.length + 1)),
+ "Invalid tab for forgetClosedTab throws");
+
+ // Remove third tab, then first tab.
+ ss.forgetClosedTab(newWin, 2);
+ ss.forgetClosedTab(newWin, null);
+
+ closedTabs = SessionStore.getClosedTabData(newWin, false);
+
+ // Verify that non JSON serialized data is the same as JSON serialized data.
+ is(JSON.stringify(closedTabs), SessionStore.getClosedTabData(newWin),
+ "Non-serialized data is the same as serialized data")
+
+ is(closedTabs.length, remember_count,
+ "The correct amout of tabs was removed");
+ is(countByTitle(closedTabs, FORGET), 0,
+ "All tabs specifically forgotten were indeed removed");
+ is(countByTitle(closedTabs, REMEMBER), remember_count,
+ "... and tabs not specifically forgetten weren't");
+
+ // Clean up.
+ gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
+ BrowserTestUtils.closeWindow(newWin).then(finish);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_461743.js b/browser/components/sessionstore/test/browser_461743.js
new file mode 100644
index 000000000..263fff5f2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_461743.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 461743 **/
+
+ waitForExplicitFinish();
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_461743_sample.html";
+
+ let frameCount = 0;
+ let tab = gBrowser.addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function(aEvent) {
+ // Wait for all frames to load completely.
+ if (frameCount++ < 2)
+ return;
+ tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener("461743", function(aEvent) {
+ tab2.linkedBrowser.removeEventListener("461743", arguments.callee, true);
+ is(aEvent.data, "done", "XSS injection was attempted");
+
+ executeSoon(function() {
+ let iframes = tab2.linkedBrowser.contentWindow.frames;
+ let innerHTML = iframes[1].document.body.innerHTML;
+ isnot(innerHTML, Components.utils.reportError.toString(),
+ "chrome access denied!");
+
+ // Clean up.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ });
+ }, true, true);
+ }, true);
+}
diff --git a/browser/components/sessionstore/test/browser_461743_sample.html b/browser/components/sessionstore/test/browser_461743_sample.html
new file mode 100644
index 000000000..80c9e612a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_461743_sample.html
@@ -0,0 +1,56 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<title>Test for bug 461743</title>
+
+<body>
+<iframe src="data:text/html;charset=utf-8,empty"></iframe>
+<iframe></iframe>
+
+<script type="application/javascript">
+ var chromeUrl = "chrome://global/content/mozilla.xhtml";
+ var exploitUrl = "javascript:try { document.body.innerHTML = Components.utils.reportError; } catch (ex) { }";
+
+ var loadCount = 0;
+ frames[0].addEventListener("DOMContentLoaded", handleLoad, false);
+ frames[1].addEventListener("DOMContentLoaded", handleLoad, false);
+ function handleLoad() {
+ if (++loadCount < 2)
+ return;
+ frames[0].removeEventListener("DOMContentLoaded", handleLoad, false);
+ frames[1].removeEventListener("DOMContentLoaded", handleLoad, false);
+
+ var flip = 0;
+ MutationEvent.prototype.toString = function() {
+ return flip++ == 0 ? chromeUrl : exploitUrl;
+ };
+
+ var href = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(frames[1].location), "href").get;
+ var loadChrome = { handleEvent: href };
+ var loadExploit = { handleEvent: href };
+
+ function delay() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", location.href, false);
+ xhr.send(null);
+ }
+ function done() {
+ var event = new MessageEvent('461743', { bubbles: true, cancelable: false,
+ data: "done", origin: location.href,
+ source: window });
+ document.dispatchEvent(event);
+ frames[0].document.removeEventListener("DOMNodeInserted", loadChrome, true);
+ frames[0].document.removeEventListener("DOMNodeInserted", delay, true);
+ frames[0].document.removeEventListener("DOMNodeInserted", loadExploit, true);
+ frames[0].document.removeEventListener("DOMNodeInserted", done, true);
+ }
+
+ frames[0].document.addEventListener("DOMNodeInserted", loadChrome, true);
+ frames[0].document.addEventListener("DOMNodeInserted", delay, true);
+ frames[0].document.addEventListener("DOMNodeInserted", loadExploit, true);
+ frames[0].document.addEventListener("DOMNodeInserted", done, true);
+
+ frames[0].document.designMode = "on";
+ };
+</script>
+</body>
diff --git a/browser/components/sessionstore/test/browser_463205.js b/browser/components/sessionstore/test/browser_463205.js
new file mode 100644
index 000000000..ad3f22794
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463205.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_463205_sample.html";
+
+/**
+ * Bug 463205 - Check URLs before restoring form data to make sure a malicious
+ * website can't modify frame URLs and make us inject form data into the wrong
+ * web pages.
+ */
+add_task(function test_check_urls_before_restoring() {
+ // Add a blank tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Restore form data with a valid URL.
+ yield promiseTabState(tab, getState(URL));
+
+ let value = yield getInputValue(browser, {id: "text"});
+ is(value, "foobar", "value was restored");
+
+ // Restore form data with an invalid URL.
+ yield promiseTabState(tab, getState("http://example.com/"));
+
+ value = yield getInputValue(browser, {id: "text"});
+ is(value, "", "value was not restored");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+function getState(url) {
+ return JSON.stringify({
+ entries: [{url: URL}],
+ formdata: {url: url, id: {text: "foobar"}}
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_463205_sample.html b/browser/components/sessionstore/test/browser_463205_sample.html
new file mode 100644
index 000000000..6591401b6
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463205_sample.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>bug 463205</title>
+
+<body>
+ <input type="text" id="text" />
+</body>
diff --git a/browser/components/sessionstore/test/browser_463206.js b/browser/components/sessionstore/test/browser_463206.js
new file mode 100644
index 000000000..99ee8373e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463206.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+"use strict";
+
+const TEST_URL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_463206_sample.html";
+
+add_task(function* () {
+ // Add a new tab.
+ let tab = gBrowser.addTab(TEST_URL);
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // "Type in" some random values.
+ yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
+ function typeText(aTextField, aValue) {
+ aTextField.value = aValue;
+
+ let event = aTextField.ownerDocument.createEvent("UIEvents");
+ event.initUIEvent("input", true, true, aTextField.ownerGlobal, 0);
+ aTextField.dispatchEvent(event);
+ }
+
+ typeText(content.document.getElementById("out1"), Date.now());
+ typeText(content.document.getElementsByName("1|#out2")[0], Math.random());
+ typeText(content.frames[0].frames[1].document.getElementById("in1"), new Date());
+ });
+
+ // Duplicate the tab.
+ let tab2 = gBrowser.duplicateTab(tab);
+ yield promiseTabRestored(tab2);
+
+ // Query a few values from the top and its child frames.
+ yield ContentTask.spawn(tab2.linkedBrowser, null, function* () {
+ Assert.notEqual(content.document.getElementById("out1").value,
+ content.frames[1].document.getElementById("out1").value,
+ "text isn't reused for frames");
+ Assert.notEqual(content.document.getElementsByName("1|#out2")[0].value,
+ "", "text containing | and # is correctly restored");
+ Assert.equal(content.frames[1].document.getElementById("out2").value,
+ "", "id prefixes can't be faked");
+ // Disabled for now, Bug 588077
+ //Assert.equal(content.frames[0].frames[1].document.getElementById("in1").value,
+ // "", "id prefixes aren't mixed up");
+ Assert.equal(content.frames[1].frames[0].document.getElementById("in1").value,
+ "", "id prefixes aren't mixed up");
+ });
+
+ // Cleanup.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_463206_sample.html b/browser/components/sessionstore/test/browser_463206_sample.html
new file mode 100644
index 000000000..0d31f2906
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463206_sample.html
@@ -0,0 +1,11 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for bug 463206</title>
+
+<iframe src="data:text/html;charset=utf-8,<iframe></iframe><iframe%20src='data:text/html;charset=utf-8,<input%2520id=%2522in1%2522>'></iframe>"></iframe>
+<iframe src="data:text/html;charset=utf-8,<input%20id='out1'><input%20id='out2'><iframe%20src='data:text/html;charset=utf-8,<input%2520id=%2522in1%2522>'>"></iframe>
+
+<input id="out1">
+<input name="1|#out2">
diff --git a/browser/components/sessionstore/test/browser_464199.js b/browser/components/sessionstore/test/browser_464199.js
new file mode 100644
index 000000000..36f07832c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464199.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm");
+
+function waitForClearHistory(aCallback) {
+ let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(this, "browser:purge-domain-data");
+ setTimeout(aCallback, 0);
+ }
+ };
+ Services.obs.addObserver(observer, "browser:purge-domain-data", false);
+}
+
+function test() {
+ /** Test for Bug 464199 **/
+
+ waitForExplicitFinish();
+
+ const REMEMBER = Date.now(), FORGET = Math.random();
+ let test_state = { windows: [{ "tabs": [{ "entries": [] }], _closedTabs: [
+ { state: { entries: [{ url: "http://www.example.net/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://www.example.org/" }] }, title: REMEMBER },
+ { state: { entries: [{ url: "http://www.example.net/" },
+ { url: "http://www.example.org/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://example.net/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://sub.example.net/" }] }, title: FORGET },
+ { state: { entries: [{ url: "http://www.example.net:8080/" }] }, title: FORGET },
+ { state: { entries: [{ url: "about:license" }] }, title: REMEMBER },
+ { state: { entries: [{ url: "http://www.example.org/frameset",
+ children: [
+ { url: "http://www.example.org/frame" },
+ { url: "http://www.example.org:8080/frame2" }
+ ] }] }, title: REMEMBER },
+ { state: { entries: [{ url: "http://www.example.org/frameset",
+ children: [
+ { url: "http://www.example.org/frame" },
+ { url: "http://www.example.net/frame" }
+ ] }] }, title: FORGET },
+ { state: { entries: [{ url: "http://www.example.org/form",
+ formdata: { id: { "url": "http://www.example.net/" } }
+ }] }, title: REMEMBER },
+ { state: { entries: [{ url: "http://www.example.org/form" }],
+ extData: { "setTabValue": "http://example.net:80" } }, title: REMEMBER }
+ ] }] };
+ let remember_count = 5;
+
+ function countByTitle(aClosedTabList, aTitle) {
+ return aClosedTabList.filter(aData => aData.title == aTitle).length;
+ }
+
+ // open a window and add the above closed tab list
+ let newWin = openDialog(location, "", "chrome,all,dialog=no");
+ promiseWindowLoaded(newWin).then(() => {
+ gPrefService.setIntPref("browser.sessionstore.max_tabs_undo",
+ test_state.windows[0]._closedTabs.length);
+ ss.setWindowState(newWin, JSON.stringify(test_state), true);
+
+ let closedTabs = JSON.parse(ss.getClosedTabData(newWin));
+ is(closedTabs.length, test_state.windows[0]._closedTabs.length,
+ "Closed tab list has the expected length");
+ is(countByTitle(closedTabs, FORGET),
+ test_state.windows[0]._closedTabs.length - remember_count,
+ "The correct amout of tabs are to be forgotten");
+ is(countByTitle(closedTabs, REMEMBER), remember_count,
+ "Everything is set up.");
+
+ ForgetAboutSite.removeDataFromDomain("example.net");
+ waitForClearHistory(function() {
+ closedTabs = JSON.parse(ss.getClosedTabData(newWin));
+ is(closedTabs.length, remember_count,
+ "The correct amout of tabs was removed");
+ is(countByTitle(closedTabs, FORGET), 0,
+ "All tabs to be forgotten were indeed removed");
+ is(countByTitle(closedTabs, REMEMBER), remember_count,
+ "... and tabs to be remembered weren't.");
+
+ // clean up
+ gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
+ BrowserTestUtils.closeWindow(newWin).then(finish);
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_464620_a.html b/browser/components/sessionstore/test/browser_464620_a.html
new file mode 100644
index 000000000..1f03c92c7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_a.html
@@ -0,0 +1,54 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<title>Test for bug 464620 (injection on input)</title>
+
+<iframe></iframe>
+<iframe onload="setup()"></iframe>
+
+<script>
+ var targetUrl = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_xd.html";
+ var firstPass;
+
+ function setup() {
+ if (firstPass !== undefined)
+ return;
+ firstPass = frames[1].location.href == "about:blank";
+ if (firstPass) {
+ frames[0].location = 'data:text/html;charset=utf-8,<body onload="if (parent.firstPass) parent.step();"><input id="x" oninput="parent.xss()">XXX</body>';
+ }
+ frames[1].location = targetUrl;
+ }
+
+ function step() {
+ var x = frames[0].document.getElementById("x");
+ if (x.value == "")
+ x.value = "ready";
+ x.style.display = "none";
+ frames[0].document.designMode = "on";
+ }
+
+ function xss() {
+ step();
+
+ var documentInjected = false;
+ document.getElementsByTagName("iframe")[0].onload =
+ function() { documentInjected = true; };
+ frames[0].location = targetUrl;
+
+ for (var c = 0; !documentInjected && c < 20; c++) {
+ var r = new XMLHttpRequest();
+ r.open("GET", location.href, false);
+ r.overrideMimeType("text/plain");
+ r.send(null);
+ }
+ document.getElementById("state").textContent = "done";
+
+ var event = new MessageEvent('464620_a', { bubbles: true, cancelable: false,
+ data: "done", origin: location.href,
+ source: window });
+ document.dispatchEvent(event);
+ }
+</script>
+
+<p id="state">pending</p>
diff --git a/browser/components/sessionstore/test/browser_464620_a.js b/browser/components/sessionstore/test/browser_464620_a.js
new file mode 100644
index 000000000..9756fa703
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_a.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 464620 (injection on input) **/
+
+ waitForExplicitFinish();
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_a.html";
+
+ var frameCount = 0;
+ let tab = gBrowser.addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 4)
+ return;
+ this.removeEventListener("load", arguments.callee, true);
+
+ executeSoon(function() {
+ frameCount = 0;
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener("464620_a", function(aEvent) {
+ tab2.linkedBrowser.removeEventListener("464620_a", arguments.callee, true);
+ is(aEvent.data, "done", "XSS injection was attempted");
+
+ // let form restoration complete and take into account the
+ // setTimeout(..., 0) in sss_restoreDocument_proxy
+ executeSoon(function() {
+ setTimeout(function() {
+ let win = tab2.linkedBrowser.contentWindow;
+ isnot(win.frames[0].document.location, testURL,
+ "cross domain document was loaded");
+ ok(!/XXX/.test(win.frames[0].document.body.innerHTML),
+ "no content was injected");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ }, 0);
+ });
+ }, true, true);
+ });
+ }, true);
+}
diff --git a/browser/components/sessionstore/test/browser_464620_b.html b/browser/components/sessionstore/test/browser_464620_b.html
new file mode 100644
index 000000000..8c86d2152
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_b.html
@@ -0,0 +1,58 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<title>Test for bug 464620 (injection on DOM node insertion)</title>
+
+<iframe></iframe>
+<iframe></iframe>
+<iframe onload="setup()"></iframe>
+
+<script>
+ var targetUrl = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_xd.html";
+ var firstPass;
+
+ function setup() {
+ if (firstPass !== undefined)
+ return;
+ firstPass = frames[2].location.href == "about:blank";
+ if (firstPass) {
+ frames[0].location = 'data:text/html;charset=utf-8,<body onload="parent.step()">a</body>';
+ frames[1].location = 'data:text/html;charset=utf-8,<body onload="document.designMode=\'on\';">XXX</body>';
+ }
+ frames[2].location = targetUrl;
+ }
+
+ function step() {
+ frames[0].document.designMode = "on";
+ if (firstPass)
+ return;
+
+ var body = frames[0].document.body;
+ body.addEventListener("DOMNodeInserted", function() {
+ body.removeEventListener("DOMNodeInserted", arguments.callee, true);
+ xss();
+ }, true);
+ }
+
+ function xss() {
+ var documentInjected = false;
+ document.getElementsByTagName("iframe")[1].onload =
+ function() { documentInjected = true; };
+ frames[1].location = targetUrl;
+
+ for (var c = 0; !documentInjected && c < 20; c++) {
+ var r = new XMLHttpRequest();
+ r.open("GET", location.href, false);
+ r.overrideMimeType("text/plain");
+ r.send(null);
+ }
+ document.getElementById("state").textContent = "done";
+
+ var event = new MessageEvent('464620_b', { bubbles: true, cancelable: false,
+ data: "done", origin: location.href,
+ source: window });
+ document.dispatchEvent(event);
+ }
+</script>
+
+<p id="state">pending</p>
diff --git a/browser/components/sessionstore/test/browser_464620_b.js b/browser/components/sessionstore/test/browser_464620_b.js
new file mode 100644
index 000000000..cf23aa460
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_b.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 464620 (injection on DOM node insertion) **/
+
+ waitForExplicitFinish();
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_b.html";
+
+ var frameCount = 0;
+ let tab = gBrowser.addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 6)
+ return;
+ this.removeEventListener("load", arguments.callee, true);
+
+ executeSoon(function() {
+ frameCount = 0;
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener("464620_b", function(aEvent) {
+ tab2.linkedBrowser.removeEventListener("464620_b", arguments.callee, true);
+ is(aEvent.data, "done", "XSS injection was attempted");
+
+ // let form restoration complete and take into account the
+ // setTimeout(..., 0) in sss_restoreDocument_proxy
+ executeSoon(function() {
+ setTimeout(function() {
+ let win = tab2.linkedBrowser.contentWindow;
+ isnot(win.frames[1].document.location, testURL,
+ "cross domain document was loaded");
+ ok(!/XXX/.test(win.frames[1].document.body.innerHTML),
+ "no content was injected");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ }, 0);
+ });
+ }, true, true);
+ });
+ }, true);
+}
diff --git a/browser/components/sessionstore/test/browser_464620_xd.html b/browser/components/sessionstore/test/browser_464620_xd.html
new file mode 100644
index 000000000..9ec51c4c7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_xd.html
@@ -0,0 +1,5 @@
+<title>Cross Document File for bug 464620</title>
+
+<body onload="document.designMode='on';" bgcolor="red">
+ This document is editable.
+</body>
diff --git a/browser/components/sessionstore/test/browser_465215.js b/browser/components/sessionstore/test/browser_465215.js
new file mode 100644
index 000000000..3a0a7b9c5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_465215.js
@@ -0,0 +1,28 @@
+"use strict";
+
+var uniqueName = "bug 465215";
+var uniqueValue1 = "as good as unique: " + Date.now();
+var uniqueValue2 = "as good as unique: " + Math.random();
+
+add_task(function* () {
+ // set a unique value on a new, blank tab
+ let tab1 = gBrowser.addTab("about:blank");
+ yield promiseBrowserLoaded(tab1.linkedBrowser);
+ ss.setTabValue(tab1, uniqueName, uniqueValue1);
+
+ // duplicate the tab with that value
+ let tab2 = ss.duplicateTab(window, tab1);
+ yield promiseTabRestored(tab2);
+ is(ss.getTabValue(tab2, uniqueName), uniqueValue1, "tab value was duplicated");
+
+ ss.setTabValue(tab2, uniqueName, uniqueValue2);
+ isnot(ss.getTabValue(tab1, uniqueName), uniqueValue2, "tab values aren't sync'd");
+
+ // overwrite the tab with the value which should remove it
+ yield promiseTabState(tab1, {entries: []});
+ is(ss.getTabValue(tab1, uniqueName), "", "tab value was cleared");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab1);
+});
diff --git a/browser/components/sessionstore/test/browser_465223.js b/browser/components/sessionstore/test/browser_465223.js
new file mode 100644
index 000000000..04f888b30
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_465223.js
@@ -0,0 +1,45 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 465223 **/
+
+ // test setup
+ waitForExplicitFinish();
+
+ let uniqueKey1 = "bug 465223.1";
+ let uniqueKey2 = "bug 465223.2";
+ let uniqueValue1 = "unik" + Date.now();
+ let uniqueValue2 = "pi != " + Math.random();
+
+ // open a window and set a value on it
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ promiseWindowLoaded(newWin).then(() => {
+ ss.setWindowValue(newWin, uniqueKey1, uniqueValue1);
+
+ let newState = { windows: [{ tabs:[{ entries: [] }], extData: {} }] };
+ newState.windows[0].extData[uniqueKey2] = uniqueValue2;
+ ss.setWindowState(newWin, JSON.stringify(newState), false);
+
+ is(newWin.gBrowser.tabs.length, 2,
+ "original tab wasn't overwritten");
+ is(ss.getWindowValue(newWin, uniqueKey1), uniqueValue1,
+ "window value wasn't overwritten when the tabs weren't");
+ is(ss.getWindowValue(newWin, uniqueKey2), uniqueValue2,
+ "new window value was correctly added");
+
+ newState.windows[0].extData[uniqueKey2] = uniqueValue1;
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ is(newWin.gBrowser.tabs.length, 1,
+ "original tabs were overwritten");
+ is(ss.getWindowValue(newWin, uniqueKey1), "",
+ "window value was cleared");
+ is(ss.getWindowValue(newWin, uniqueKey2), uniqueValue1,
+ "window value was correctly overwritten");
+
+ // clean up
+ BrowserTestUtils.closeWindow(newWin).then(finish);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_466937.js b/browser/components/sessionstore/test/browser_466937.js
new file mode 100644
index 000000000..0a07caa0c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_466937.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_466937_sample.html";
+
+/**
+ * Bug 466937 - Prevent file stealing with sessionstore.
+ */
+add_task(function test_prevent_file_stealing() {
+ // Add a tab with some file input fields.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Generate a path to a 'secret' file.
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("466937_test.file");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ let testPath = file.path;
+
+ // Fill in form values.
+ yield setInputValue(browser, {id: "reverse_thief", value: "/home/user/secret2"});
+ yield setInputValue(browser, {id: "bystander", value: testPath});
+
+ // Duplicate and check form values.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2);
+
+ let thief = yield getInputValue(browser2, {id: "thief"});
+ is(thief, "", "file path wasn't set to text field value");
+ let reverse_thief = yield getInputValue(browser2, {id: "reverse_thief"});
+ is(reverse_thief, "", "text field value wasn't set to full file path");
+ let bystander = yield getInputValue(browser2, {id: "bystander"});
+ is(bystander, testPath, "normal case: file path was correctly preserved");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(tab2);
+});
diff --git a/browser/components/sessionstore/test/browser_466937_sample.html b/browser/components/sessionstore/test/browser_466937_sample.html
new file mode 100644
index 000000000..1d46c649a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_466937_sample.html
@@ -0,0 +1,22 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for bug 466937</title>
+
+<input id="thief" value="/home/user/secret">
+<input type="file" id="reverse_thief">
+<input type="file" id="bystander">
+
+<script>
+ window.addEventListener("DOMContentLoaded", function() {
+ window.removeEventListener("DOMContentLoaded", arguments.callee, false);
+ if (!document.location.hash) {
+ document.location.hash = "#ready";
+ }
+ else {
+ document.getElementById("thief").type = "file";
+ document.getElementById("reverse_thief").type = "text";
+ }
+ }, false);
+</script>
diff --git a/browser/components/sessionstore/test/browser_467409-backslashplosion.js b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
new file mode 100644
index 000000000..0e990c614
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test Summary:
+// 1. Open about:sessionrestore where formdata is a JS object, not a string
+// 1a. Check that #sessionData on the page is readable after JSON.parse (skipped, checking formdata is sufficient)
+// 1b. Check that there are no backslashes in the formdata
+// 1c. Check that formdata doesn't require JSON.parse
+//
+// 2. Use the current state (currently about:sessionrestore with data) and then open that in a new instance of about:sessionrestore
+// 2a. Check that there are no backslashes in the formdata
+// 2b. Check that formdata doesn't require JSON.parse
+//
+// 3. [backwards compat] Use a stringified state as formdata when opening about:sessionrestore
+// 3a. Make sure there are nodes in the tree on about:sessionrestore (skipped, checking formdata is sufficient)
+// 3b. Check that there are no backslashes in the formdata
+// 3c. Check that formdata doesn't require JSON.parse
+
+const CRASH_STATE = {windows: [{tabs: [{entries: [{url: "about:mozilla" }]}]}]};
+const STATE = createEntries(CRASH_STATE);
+const STATE2 = createEntries({windows: [{tabs: [STATE]}]});
+const STATE3 = createEntries(JSON.stringify(CRASH_STATE));
+
+function createEntries(sessionData) {
+ return {
+ entries: [{url: "about:sessionrestore"}],
+ formdata: {id: {sessionData: sessionData}, url: "about:sessionrestore"}
+ };
+}
+
+add_task(function test_nested_about_sessionrestore() {
+ // Prepare a blank tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // test 1
+ yield promiseTabState(tab, STATE);
+ yield checkState("test1", tab);
+
+ // test 2
+ yield promiseTabState(tab, STATE2);
+ yield checkState("test2", tab);
+
+ // test 3
+ yield promiseTabState(tab, STATE3);
+ yield checkState("test3", tab);
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+function* checkState(prefix, tab) {
+ // Flush and query tab state.
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ let {formdata} = JSON.parse(ss.getTabState(tab));
+
+ ok(formdata.id["sessionData"], prefix + ": we have form data for about:sessionrestore");
+
+ let sessionData_raw = JSON.stringify(formdata.id["sessionData"]);
+ ok(!/\\/.test(sessionData_raw), prefix + ": #sessionData contains no backslashes");
+ info(sessionData_raw);
+
+ let gotError = false;
+ try {
+ JSON.parse(formdata.id["sessionData"]);
+ } catch (e) {
+ info(prefix + ": got error: " + e);
+ gotError = true;
+ }
+ ok(gotError, prefix + ": attempting to JSON.parse form data threw error");
+}
diff --git a/browser/components/sessionstore/test/browser_477657.js b/browser/components/sessionstore/test/browser_477657.js
new file mode 100644
index 000000000..1a8ee3412
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_477657.js
@@ -0,0 +1,60 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 477657 **/
+ waitForExplicitFinish();
+
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ promiseWindowLoaded(newWin).then(() => {
+ let newState = { windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: [{
+ state: { entries: [{ url: "about:" }]},
+ title: "About:"
+ }],
+ sizemode: "maximized"
+ }] };
+
+ let uniqueKey = "bug 477657";
+ let uniqueValue = "unik" + Date.now();
+
+ ss.setWindowValue(newWin, uniqueKey, uniqueValue);
+ is(ss.getWindowValue(newWin, uniqueKey), uniqueValue,
+ "window value was set before the window was overwritten");
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ // use newWin.setTimeout(..., 0) to mirror sss_restoreWindowFeatures
+ newWin.setTimeout(function() {
+ is(ss.getWindowValue(newWin, uniqueKey), "",
+ "window value was implicitly cleared");
+
+ is(newWin.windowState, newWin.STATE_MAXIMIZED,
+ "the window was maximized");
+
+ is(JSON.parse(ss.getClosedTabData(newWin)).length, 1,
+ "the closed tab was added before the window was overwritten");
+ delete newState.windows[0]._closedTabs;
+ delete newState.windows[0].sizemode;
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ newWin.setTimeout(function() {
+ is(JSON.parse(ss.getClosedTabData(newWin)).length, 0,
+ "closed tabs were implicitly cleared");
+
+ is(newWin.windowState, newWin.STATE_MAXIMIZED,
+ "the window remains maximized");
+ newState.windows[0].sizemode = "normal";
+ ss.setWindowState(newWin, JSON.stringify(newState), true);
+
+ newWin.setTimeout(function() {
+ isnot(newWin.windowState, newWin.STATE_MAXIMIZED,
+ "the window was explicitly unmaximized");
+
+ BrowserTestUtils.closeWindow(newWin).then(finish);
+ }, 0);
+ }, 0);
+ }, 0);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_480893.js b/browser/components/sessionstore/test/browser_480893.js
new file mode 100644
index 000000000..e3ddb39b7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_480893.js
@@ -0,0 +1,47 @@
+"use strict";
+
+/**
+ * Tests that we get sent to the right page when the user clicks
+ * the "Close" button in about:sessionrestore
+ */
+add_task(function*() {
+ yield SpecialPowers.pushPrefEnv({
+ "set": [
+ ["browser.startup.page", 0],
+ ]
+ });
+
+ let tab = gBrowser.addTab("about:sessionrestore");
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser, false, "about:sessionrestore");
+
+ let doc = browser.contentDocument;
+
+ // Click on the "Close" button after about:sessionrestore is loaded.
+ doc.getElementById("errorCancel").click();
+
+ yield BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ // Test that starting a new session loads the homepage (set to http://mochi.test:8888)
+ // if Firefox is configured to display a homepage at startup (browser.startup.page = 1)
+ let homepage = "http://mochi.test:8888/";
+ yield SpecialPowers.pushPrefEnv({
+ "set": [
+ ["browser.startup.homepage", homepage],
+ ["browser.startup.page", 1],
+ ]
+ });
+
+ browser.loadURI("about:sessionrestore");
+ yield BrowserTestUtils.browserLoaded(browser, false, "about:sessionrestore");
+ doc = browser.contentDocument;
+
+ // Click on the "Close" button after about:sessionrestore is loaded.
+ doc.getElementById("errorCancel").click();
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ is(browser.currentURI.spec, homepage, "loaded page is the homepage");
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_485482.js b/browser/components/sessionstore/test/browser_485482.js
new file mode 100644
index 000000000..68ec9941b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_485482.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_485482_sample.html";
+
+/**
+ * Bug 485482 - Make sure that we produce valid XPath expressions even for very
+ * weird HTML documents.
+ */
+add_task(function test_xpath_exp_for_strange_documents() {
+ // Load a page with weird tag names.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Fill in some values.
+ let uniqueValue = Math.random();
+ yield setInputValue(browser, {selector: "input[type=text]", value: uniqueValue});
+ yield setInputChecked(browser, {selector: "input[type=checkbox]", checked: true});
+
+ // Duplicate the tab.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2);
+
+ // Check that we generated valid XPath expressions to restore form values.
+ let text = yield getInputValue(browser2, {selector: "input[type=text]"});
+ is(text, uniqueValue, "generated XPath expression was valid");
+ let checkbox = yield getInputChecked(browser2, {selector: "input[type=checkbox]"});
+ ok(checkbox, "generated XPath expression was valid");
+
+ // Cleanup.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_485482_sample.html b/browser/components/sessionstore/test/browser_485482_sample.html
new file mode 100644
index 000000000..c2097b593
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_485482_sample.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Test for bug 485482</title>
+
+<bad=name>
+ <input type="text">
+</bad=name>
+
+<worse=name>
+ <l0c@l+na~e"'§>
+ <input type="checkbox" name="check"> Check
+ </l0c@l+na~e"'§>
+</worse=name>
diff --git a/browser/components/sessionstore/test/browser_485563.js b/browser/components/sessionstore/test/browser_485563.js
new file mode 100644
index 000000000..f4e6b31cc
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_485563.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 485563 **/
+
+ waitForExplicitFinish();
+
+ let uniqueValue = Math.random() + "\u2028Second line\u2029Second paragraph\u2027";
+
+ let tab = gBrowser.addTab();
+ promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+ ss.setTabValue(tab, "bug485563", uniqueValue);
+ let tabState = JSON.parse(ss.getTabState(tab));
+ is(tabState.extData["bug485563"], uniqueValue,
+ "unicode line separator wasn't over-encoded");
+ ss.deleteTabValue(tab, "bug485563");
+ ss.setTabState(tab, JSON.stringify(tabState));
+ is(ss.getTabValue(tab, "bug485563"), uniqueValue,
+ "unicode line separator was correctly preserved");
+
+ gBrowser.removeTab(tab);
+ finish();
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_490040.js b/browser/components/sessionstore/test/browser_490040.js
new file mode 100644
index 000000000..bc680c32f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_490040.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+// Only windows with open tabs are restorable. Windows where a lone tab is
+// detached may have _closedTabs, but is left with just an empty tab.
+const STATES = [{
+ shouldBeAdded: true,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [{ url: "http://example.com", title: "example.com" }] }],
+ selected: 1,
+ _closedTabs: []
+ }]
+ }
+ }, {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: []
+ }]
+ }
+ }, {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: [{ state: { entries: [{ url: "http://example.com", index: 1 }] } }]
+ }]
+ }
+ }, {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [{
+ tabs: [{ entries: [] }],
+ _closedTabs: [],
+ extData: { keyname: "pi != " + Math.random() }
+ }]
+ }
+ }];
+
+add_task(function* test_bug_490040() {
+ for (let state of STATES) {
+ // Ensure we can store the window if needed.
+ let startingClosedWindowCount = ss.getClosedWindowCount();
+ yield pushPrefs(["browser.sessionstore.max_windows_undo",
+ startingClosedWindowCount + 1]);
+
+ let curClosedWindowCount = ss.getClosedWindowCount();
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+
+ ss.setWindowState(win, JSON.stringify(state.windowState), true);
+ if (state.windowState.windows[0].tabs.length) {
+ yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ }
+
+ yield BrowserTestUtils.closeWindow(win);
+
+ is(ss.getClosedWindowCount(),
+ curClosedWindowCount + (state.shouldBeAdded ? 1 : 0),
+ "That window should " + (state.shouldBeAdded ? "" : "not ") +
+ "be restorable");
+ }
+});
diff --git a/browser/components/sessionstore/test/browser_491168.js b/browser/components/sessionstore/test/browser_491168.js
new file mode 100644
index 000000000..ae66afe77
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_491168.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const REFERRER1 = "http://example.org/?" + Date.now();
+const REFERRER2 = "http://example.org/?" + Math.random();
+
+add_task(function* () {
+ function* checkDocumentReferrer(referrer, msg) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { referrer, msg }, function* (args) {
+ Assert.equal(content.document.referrer, args.referrer, args.msg);
+ });
+ }
+
+ // Add a new tab.
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Load a new URI with a specific referrer.
+ let referrerURI = Services.io.newURI(REFERRER1, null, null);
+ browser.loadURI("http://example.org", referrerURI, null);
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+ let tabState = JSON.parse(ss.getTabState(tab));
+ is(tabState.entries[0].referrer, REFERRER1,
+ "Referrer retrieved via getTabState matches referrer set via loadURI.");
+
+ tabState.entries[0].referrer = REFERRER2;
+ yield promiseTabState(tab, tabState);
+
+ yield checkDocumentReferrer(REFERRER2,
+ "document.referrer matches referrer set via setTabState.");
+ gBrowser.removeCurrentTab();
+
+ // Restore the closed tab.
+ tab = ss.undoCloseTab(window, 0);
+ yield promiseTabRestored(tab);
+
+ yield checkDocumentReferrer(REFERRER2,
+ "document.referrer is still correct after closing and reopening the tab.");
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/sessionstore/test/browser_491577.js b/browser/components/sessionstore/test/browser_491577.js
new file mode 100644
index 000000000..0e088d702
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_491577.js
@@ -0,0 +1,120 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 491577 **/
+
+ // test setup
+ waitForExplicitFinish();
+
+ const REMEMBER = Date.now(), FORGET = Math.random();
+ let test_state = {
+ windows: [ { tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 } ],
+ _closedWindows : [
+ // _closedWindows[0]
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com/", title: "title" }] },
+ { entries: [{ url: "http://mozilla.org/", title: "title" }] }
+ ],
+ selected: 2,
+ title: FORGET,
+ _closedTabs: []
+ },
+ // _closedWindows[1]
+ {
+ tabs: [
+ { entries: [{ url: "http://mozilla.org/", title: "title" }] },
+ { entries: [{ url: "http://example.com/", title: "title" }] },
+ { entries: [{ url: "http://mozilla.org/", title: "title" }] },
+ ],
+ selected: 3,
+ title: REMEMBER,
+ _closedTabs: []
+ },
+ // _closedWindows[2]
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com/", title: "title" }] }
+ ],
+ selected: 1,
+ title: FORGET,
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ { url: "http://mozilla.org/", title: "title" },
+ { url: "http://mozilla.org/again", title: "title" }
+ ]
+ },
+ pos: 1,
+ title: "title"
+ },
+ {
+ state: {
+ entries: [
+ { url: "http://example.com", title: "title" }
+ ]
+ },
+ title: "title"
+ }
+ ]
+ }
+ ]
+ };
+ let remember_count = 1;
+
+ function countByTitle(aClosedWindowList, aTitle) {
+ return aClosedWindowList.filter(aData => aData.title == aTitle).length;
+ }
+
+ function testForError(aFunction) {
+ try {
+ aFunction();
+ return false;
+ }
+ catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE";
+ }
+ }
+
+ // open a window and add the above closed window list
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ promiseWindowLoaded(newWin).then(() => {
+ gPrefService.setIntPref("browser.sessionstore.max_windows_undo",
+ test_state._closedWindows.length);
+ ss.setWindowState(newWin, JSON.stringify(test_state), true);
+
+ let closedWindows = JSON.parse(ss.getClosedWindowData());
+ is(closedWindows.length, test_state._closedWindows.length,
+ "Closed window list has the expected length");
+ is(countByTitle(closedWindows, FORGET),
+ test_state._closedWindows.length - remember_count,
+ "The correct amount of windows are to be forgotten");
+ is(countByTitle(closedWindows, REMEMBER), remember_count,
+ "Everything is set up.");
+
+ // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
+ ok(testForError(() => ss.forgetClosedWindow(-1)),
+ "Invalid window for forgetClosedWindow throws");
+ ok(testForError(() => ss.forgetClosedWindow(test_state._closedWindows.length + 1)),
+ "Invalid window for forgetClosedWindow throws");
+
+ // Remove third window, then first window
+ ss.forgetClosedWindow(2);
+ ss.forgetClosedWindow(null);
+
+ closedWindows = JSON.parse(ss.getClosedWindowData());
+ is(closedWindows.length, remember_count,
+ "The correct amount of windows were removed");
+ is(countByTitle(closedWindows, FORGET), 0,
+ "All windows specifically forgotten were indeed removed");
+ is(countByTitle(closedWindows, REMEMBER), remember_count,
+ "... and windows not specifically forgetten weren't.");
+
+ // clean up
+ gPrefService.clearUserPref("browser.sessionstore.max_windows_undo");
+ BrowserTestUtils.closeWindow(newWin).then(finish);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_495495.js b/browser/components/sessionstore/test/browser_495495.js
new file mode 100644
index 000000000..658f81c20
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_495495.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 495495 **/
+
+ waitForExplicitFinish();
+
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no,toolbar=yes");
+ promiseWindowLoaded(newWin).then(() => {
+ let state1 = ss.getWindowState(newWin);
+ BrowserTestUtils.closeWindow(newWin).then(() => {
+
+ newWin = openDialog(location, "_blank",
+ "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar=no,location,personal,directories,dialog=no");
+ promiseWindowLoaded(newWin).then(() => {
+ let state2 = ss.getWindowState(newWin);
+
+ function testState(state, expected, callback) {
+ let win = openDialog(location, "_blank", "chrome,all,dialog=no");
+ promiseWindowLoaded(win).then(() => {
+
+ is(win.gURLBar.readOnly, false,
+ "URL bar should not be read-only before setting the state");
+ is(win.gURLBar.getAttribute("enablehistory"), "true",
+ "URL bar autocomplete should be enabled before setting the state");
+ ss.setWindowState(win, state, true);
+ is(win.gURLBar.readOnly, expected.readOnly,
+ "URL bar read-only state should be restored correctly");
+ is(win.gURLBar.getAttribute("enablehistory"), expected.enablehistory,
+ "URL bar autocomplete state should be restored correctly");
+
+ BrowserTestUtils.closeWindow(win).then(callback);
+ });
+ }
+
+ BrowserTestUtils.closeWindow(newWin).then(() => {
+ testState(state1, {readOnly: false, enablehistory: "true"}, function() {
+ testState(state2, {readOnly: true, enablehistory: "false"}, finish);
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_500328.js b/browser/components/sessionstore/test/browser_500328.js
new file mode 100644
index 000000000..44650ef8b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_500328.js
@@ -0,0 +1,120 @@
+/* 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/. */
+
+let checkState = Task.async(function*(browser) {
+ // Go back and then forward, and make sure that the state objects received
+ // from the popState event are as we expect them to be.
+ //
+ // We also add a node to the document's body when after going back and make
+ // sure it's still there after we go forward -- this is to test that the two
+ // history entries correspond to the same document.
+
+ let deferred = {};
+ deferred.promise = new Promise(resolve => deferred.resolve = resolve);
+
+ let popStateCount = 0;
+
+ browser.addEventListener("popstate", function(aEvent) {
+ if (popStateCount == 0) {
+ popStateCount++;
+
+ ok(aEvent.state, "Event should have a state property.");
+
+ ContentTask.spawn(browser, null, function() {
+ is(content.testState, "foo",
+ "testState after going back");
+ is(JSON.stringify(content.history.state), JSON.stringify({obj1:1}),
+ "first popstate object.");
+
+ // Add a node with id "new-elem" to the document.
+ let doc = content.document;
+ ok(!doc.getElementById("new-elem"),
+ "doc shouldn't contain new-elem before we add it.");
+ let elem = doc.createElement("div");
+ elem.id = "new-elem";
+ doc.body.appendChild(elem);
+ }).then(() => {
+ browser.goForward();
+ });
+ } else if (popStateCount == 1) {
+ popStateCount++;
+ // When content fires a PopStateEvent and we observe it from a chrome event
+ // listener (as we do here, and, thankfully, nowhere else in the tree), the
+ // state object will be a cross-compartment wrapper to an object that was
+ // deserialized in the content scope. And in this case, since RegExps are
+ // not currently Xrayable (see bug 1014991), trying to pull |obj3| (a RegExp)
+ // off of an Xrayed Object won't work. So we need to waive.
+ ContentTask.spawn(browser, aEvent.state, function(state) {
+ Assert.equal(Cu.waiveXrays(state).obj3.toString(),
+ "/^a$/", "second popstate object.");
+
+ // Make sure that the new-elem node is present in the document. If it's
+ // not, then this history entry has a different doc identifier than the
+ // previous entry, which is bad.
+ let doc = content.document;
+ let newElem = doc.getElementById("new-elem");
+ ok(newElem, "doc should contain new-elem.");
+ newElem.parentNode.removeChild(newElem);
+ ok(!doc.getElementById("new-elem"), "new-elem should be removed.");
+ }).then(() => {
+ browser.removeEventListener("popstate", arguments.callee, true);
+ deferred.resolve();
+ });
+ }
+ });
+
+ // Set some state in the page's window. When we go back(), the page should
+ // be retrieved from bfcache, and this state should still be there.
+ yield ContentTask.spawn(browser, null, function() {
+ content.testState = "foo";
+ });
+
+ // Now go back. This should trigger the popstate event handler above.
+ browser.goBack();
+
+ yield deferred.promise;
+});
+
+add_task(function* test() {
+ // Tests session restore functionality of history.pushState and
+ // history.replaceState(). (Bug 500328)
+
+ // We open a new blank window, let it load, and then load in
+ // http://example.com. We need to load the blank window first, otherwise the
+ // docshell gets confused and doesn't have a current history entry.
+ let state;
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
+ BrowserTestUtils.loadURI(browser, "http://example.com");
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ // After these push/replaceState calls, the window should have three
+ // history entries:
+ // testURL (state object: null) <-- oldest
+ // testURL (state object: {obj1:1})
+ // testURL?page2 (state object: {obj3:/^a$/}) <-- newest
+ function contentTest() {
+ let history = content.window.history;
+ history.pushState({obj1:1}, "title-obj1");
+ history.pushState({obj2:2}, "title-obj2", "?page2");
+ history.replaceState({obj3:/^a$/}, "title-obj3");
+ }
+ yield ContentTask.spawn(browser, null, contentTest);
+ yield TabStateFlusher.flush(browser);
+
+ state = ss.getTabState(gBrowser.getTabForBrowser(browser));
+ });
+
+ // Restore the state into a new tab. Things don't work well when we
+ // restore into the old tab, but that's not a real use case anyway.
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
+ let tab2 = gBrowser.getTabForBrowser(browser);
+
+ let tabRestoredPromise = promiseTabRestored(tab2);
+ ss.setTabState(tab2, state, true);
+
+ // Run checkState() once the tab finishes loading its restored state.
+ yield tabRestoredPromise;
+ yield checkState(browser);
+ });
+});
diff --git a/browser/components/sessionstore/test/browser_506482.js b/browser/components/sessionstore/test/browser_506482.js
new file mode 100644
index 000000000..6e5bd83bd
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_506482.js
@@ -0,0 +1,73 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 506482 **/
+
+ // test setup
+ waitForExplicitFinish();
+
+ // read the sessionstore.js mtime (picked from browser_248970_a.js)
+ let profilePath = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).
+ get("ProfD", Ci.nsIFile);
+ function getSessionstoreFile() {
+ let sessionStoreJS = profilePath.clone();
+ sessionStoreJS.append("sessionstore.js");
+ return sessionStoreJS;
+ }
+ function getSessionstorejsModificationTime() {
+ let file = getSessionstoreFile();
+ if (file.exists())
+ return file.lastModifiedTime;
+ else
+ return -1;
+ }
+
+ // delete existing sessionstore.js, to make sure we're not reading
+ // the mtime of an old one initially.
+ let sessionStoreJS = getSessionstoreFile();
+ if (sessionStoreJS.exists())
+ sessionStoreJS.remove(false);
+
+ // test content URL
+ const TEST_URL = "data:text/html;charset=utf-8,"
+ + "<body style='width: 100000px; height: 100000px;'><p>top</p></body>"
+
+ // preferences that we use
+ const PREF_INTERVAL = "browser.sessionstore.interval";
+
+ // make sure sessionstore.js is saved ASAP on all events
+ gPrefService.setIntPref(PREF_INTERVAL, 0);
+
+ // get the initial sessionstore.js mtime (-1 if it doesn't exist yet)
+ let mtime0 = getSessionstorejsModificationTime();
+
+ // create and select a first tab
+ let tab = gBrowser.addTab(TEST_URL);
+ promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+ // step1: the above has triggered some saveStateDelayed(), sleep until
+ // it's done, and get the initial sessionstore.js mtime
+ setTimeout(function step1(e) {
+ let mtime1 = getSessionstorejsModificationTime();
+ isnot(mtime1, mtime0, "initial sessionstore.js update");
+
+ // step2: test sessionstore.js is not updated on tab selection
+ // or content scrolling
+ gBrowser.selectedTab = tab;
+ tab.linkedBrowser.contentWindow.scrollTo(1100, 1200);
+ setTimeout(function step2(e) {
+ let mtime2 = getSessionstorejsModificationTime();
+ is(mtime2, mtime1,
+ "tab selection and scrolling: sessionstore.js not updated");
+
+ // ok, done, cleanup and finish
+ if (gPrefService.prefHasUserValue(PREF_INTERVAL))
+ gPrefService.clearUserPref(PREF_INTERVAL);
+ gBrowser.removeTab(tab);
+ finish();
+ }, 3500); // end of sleep after tab selection and scrolling
+ }, 3500); // end of sleep after initial saveStateDelayed()
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_514751.js b/browser/components/sessionstore/test/browser_514751.js
new file mode 100644
index 000000000..ff80245c4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_514751.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 514751 (Wallpaper) **/
+
+ waitForExplicitFinish();
+
+ let state = {
+ windows: [{
+ tabs: [{
+ entries: [
+ { url: "about:mozilla", title: "Mozilla" },
+ {}
+ ]
+ }]
+ }]
+ };
+
+ var theWin = openDialog(location, "", "chrome,all,dialog=no");
+ theWin.addEventListener("load", function () {
+ theWin.removeEventListener("load", arguments.callee, false);
+
+ executeSoon(function () {
+ var gotError = false;
+ try {
+ ss.setWindowState(theWin, JSON.stringify(state), true);
+ } catch (e) {
+ if (/NS_ERROR_MALFORMED_URI/.test(e))
+ gotError = true;
+ }
+ ok(!gotError, "Didn't get a malformed URI error.");
+ BrowserTestUtils.closeWindow(theWin).then(finish);
+ });
+ }, false);
+}
+
diff --git a/browser/components/sessionstore/test/browser_522375.js b/browser/components/sessionstore/test/browser_522375.js
new file mode 100644
index 000000000..50b74d6cd
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_522375.js
@@ -0,0 +1,21 @@
+function test() {
+ var startup_info = Components.classes["@mozilla.org/toolkit/app-startup;1"].getService(Components.interfaces.nsIAppStartup).getStartupInfo();
+ // No .process info on mac
+
+ // Check if we encountered a telemetry error for the the process creation
+ // timestamp and turn the first test into a known failure.
+ var telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
+ var snapshot = telemetry.getHistogramById("STARTUP_MEASUREMENT_ERRORS")
+ .snapshot();
+
+ if (snapshot.counts[0] == 0)
+ ok(startup_info.process <= startup_info.main, "process created before main is run " + uneval(startup_info));
+ else
+ todo(false, "An error occurred while recording the process creation timestamp, skipping this test");
+
+ // on linux firstPaint can happen after everything is loaded (especially with remote X)
+ if (startup_info.firstPaint)
+ ok(startup_info.main <= startup_info.firstPaint, "main ran before first paint " + uneval(startup_info));
+
+ ok(startup_info.main < startup_info.sessionRestored, "Session restored after main " + uneval(startup_info));
+}
diff --git a/browser/components/sessionstore/test/browser_522545.js b/browser/components/sessionstore/test/browser_522545.js
new file mode 100644
index 000000000..f4d373166
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_522545.js
@@ -0,0 +1,269 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 522545 **/
+
+ waitForExplicitFinish();
+ requestLongerTimeout(4);
+
+ // This tests the following use case:
+ // User opens a new tab which gets focus. The user types something into the
+ // address bar, then crashes or quits.
+ function test_newTabFocused() {
+ let state = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }] },
+ { entries: [], userTypedValue: "example.com", userTypedClear: 0 }
+ ],
+ selected: 2
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:blank",
+ "No history entries still sets currentURI to about:blank");
+ is(browser.userTypedValue, "example.com",
+ "userTypedValue was correctly restored");
+ ok(!browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing");
+ is(gURLBar.value, "example.com",
+ "Address bar's value correctly restored");
+ // Change tabs to make sure address bar value gets updated
+ gBrowser.selectedTab = gBrowser.tabContainer.getItemAtIndex(0);
+ is(gURLBar.value, "about:mozilla",
+ "Address bar's value correctly updated");
+ runNextTest();
+ });
+ }
+
+ // This tests the following use case:
+ // User opens a new tab which gets focus. The user types something into the
+ // address bar, switches back to the first tab, then crashes or quits.
+ function test_newTabNotFocused() {
+ let state = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }] },
+ { entries: [], userTypedValue: "example.org", userTypedClear: 0 }
+ ],
+ selected: 1
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = gBrowser.getBrowserAtIndex(1);
+ is(browser.currentURI.spec, "about:blank",
+ "No history entries still sets currentURI to about:blank");
+ is(browser.userTypedValue, "example.org",
+ "userTypedValue was correctly restored");
+ ok(!browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing");
+ is(gURLBar.value, "about:mozilla",
+ "Address bar's value correctly restored");
+ // Change tabs to make sure address bar value gets updated
+ gBrowser.selectedTab = gBrowser.tabContainer.getItemAtIndex(1);
+ is(gURLBar.value, "example.org",
+ "Address bar's value correctly updated");
+ runNextTest();
+ });
+ }
+
+ // This tests the following use case:
+ // User is in a tab with session history, then types something in the
+ // address bar, then crashes or quits.
+ function test_existingSHEnd_noClear() {
+ let state = {
+ windows: [{
+ tabs: [{
+ entries: [{ url: "about:mozilla" }, { url: "about:config" }],
+ index: 2,
+ userTypedValue: "example.com",
+ userTypedClear: 0
+ }]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:config",
+ "browser.currentURI set to current entry in SH");
+ is(browser.userTypedValue, "example.com",
+ "userTypedValue was correctly restored");
+ ok(!browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing");
+ is(gURLBar.value, "example.com",
+ "Address bar's value correctly restored to userTypedValue");
+ runNextTest();
+ });
+ }
+
+ // This tests the following use case:
+ // User is in a tab with session history, presses back at some point, then
+ // types something in the address bar, then crashes or quits.
+ function test_existingSHMiddle_noClear() {
+ let state = {
+ windows: [{
+ tabs: [{
+ entries: [{ url: "about:mozilla" }, { url: "about:config" }],
+ index: 1,
+ userTypedValue: "example.org",
+ userTypedClear: 0
+ }]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:mozilla",
+ "browser.currentURI set to current entry in SH");
+ is(browser.userTypedValue, "example.org",
+ "userTypedValue was correctly restored");
+ ok(!browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing");
+ is(gURLBar.value, "example.org",
+ "Address bar's value correctly restored to userTypedValue");
+ runNextTest();
+ });
+ }
+
+ // This test simulates lots of tabs opening at once and then quitting/crashing.
+ function test_getBrowserState_lotsOfTabsOpening() {
+ gBrowser.stop();
+
+ let uris = [];
+ for (let i = 0; i < 25; i++)
+ uris.push("http://example.com/" + i);
+
+ // We're waiting for the first location change, which should indicate
+ // one of the tabs has loaded and the others haven't. So one should
+ // be in a non-userTypedValue case, while others should still have
+ // userTypedValue and userTypedClear set.
+ gBrowser.addTabsProgressListener({
+ onLocationChange: function (aBrowser) {
+ if (uris.indexOf(aBrowser.currentURI.spec) > -1) {
+ gBrowser.removeTabsProgressListener(this);
+ firstLocationChange();
+ }
+ }
+ });
+
+ function firstLocationChange() {
+ let state = JSON.parse(ss.getBrowserState());
+ let hasUTV = state.windows[0].tabs.some(function(aTab) {
+ return aTab.userTypedValue && aTab.userTypedClear && !aTab.entries.length;
+ });
+
+ ok(hasUTV, "At least one tab has a userTypedValue with userTypedClear with no loaded URL");
+
+ BrowserTestUtils.waitForMessage(gBrowser.selectedBrowser.messageManager, "SessionStore:update").then(firstLoad);
+ }
+
+ function firstLoad() {
+ let state = JSON.parse(ss.getTabState(gBrowser.selectedTab));
+ let hasSH = !("userTypedValue" in state) && state.entries[0].url;
+ ok(hasSH, "The selected tab has its entry in SH");
+
+ runNextTest();
+ }
+
+ gBrowser.loadTabs(uris);
+ }
+
+ // This simulates setting a userTypedValue and ensures that just typing in the
+ // URL bar doesn't set userTypedClear as well.
+ function test_getBrowserState_userTypedValue() {
+ let state = {
+ windows: [{
+ tabs: [{ entries: [] }]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = gBrowser.selectedBrowser;
+ // Make sure this tab isn't loading and state is clear before we test.
+ is(browser.userTypedValue, null, "userTypedValue is empty to start");
+ ok(!browser.didStartLoadSinceLastUserTyping(),
+ "Initially, no load should be ongoing");
+
+ let inputText = "example.org";
+ gURLBar.focus();
+ gURLBar.value = inputText.slice(0, -1);
+ EventUtils.synthesizeKey(inputText.slice(-1) , {});
+
+ executeSoon(function () {
+ is(browser.userTypedValue, "example.org",
+ "userTypedValue was set when changing URLBar value");
+ ok(!browser.didStartLoadSinceLastUserTyping(),
+ "No load started since changing URLBar value");
+
+ // Now make sure ss gets these values too
+ let newState = JSON.parse(ss.getBrowserState());
+ is(newState.windows[0].tabs[0].userTypedValue, "example.org",
+ "sessionstore got correct userTypedValue");
+ is(newState.windows[0].tabs[0].userTypedClear, 0,
+ "sessionstore got correct userTypedClear");
+ runNextTest();
+ });
+ });
+ }
+
+ // test_getBrowserState_lotsOfTabsOpening tested userTypedClear in a few cases,
+ // but not necessarily any that had legitimate URIs in the state of loading
+ // (eg, "http://example.com"), so this test will cover that case.
+ function test_userTypedClearLoadURI() {
+ let state = {
+ windows: [{
+ tabs: [
+ { entries: [], userTypedValue: "http://example.com", userTypedClear: 2 }
+ ]
+ }]
+ };
+
+ waitForBrowserState(state, function() {
+ let browser = gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "http://example.com/",
+ "userTypedClear=2 caused userTypedValue to be loaded");
+ is(browser.userTypedValue, null,
+ "userTypedValue was null after loading a URI");
+ ok(!browser.didStartLoadSinceLastUserTyping(),
+ "We should have reset the load state when the tab loaded");
+ is(gURLBar.textValue, gURLBar.trimValue("http://example.com/"),
+ "Address bar's value set after loading URI");
+ runNextTest();
+ });
+ }
+
+
+ let tests = [test_newTabFocused, test_newTabNotFocused,
+ test_existingSHEnd_noClear, test_existingSHMiddle_noClear,
+ test_getBrowserState_lotsOfTabsOpening,
+ test_getBrowserState_userTypedValue, test_userTypedClearLoadURI];
+ let originalState = JSON.parse(ss.getBrowserState());
+ let state = {
+ windows: [{
+ tabs: [{ entries: [{ url: "about:blank" }] }]
+ }]
+ };
+ function runNextTest() {
+ if (tests.length) {
+ waitForBrowserState(state, function() {
+ gBrowser.selectedBrowser.userTypedValue = null;
+ URLBarSetURI();
+ (tests.shift())();
+ });
+ } else {
+ waitForBrowserState(originalState, function() {
+ gBrowser.selectedBrowser.userTypedValue = null;
+ URLBarSetURI();
+ finish();
+ });
+ }
+ }
+
+ // Run the tests!
+ runNextTest();
+}
diff --git a/browser/components/sessionstore/test/browser_524745.js b/browser/components/sessionstore/test/browser_524745.js
new file mode 100644
index 000000000..de53f6c92
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_524745.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 524745 **/
+
+ let uniqKey = "bug524745";
+ let uniqVal = Date.now().toString();
+
+ waitForExplicitFinish();
+
+ whenNewWindowLoaded({ private: false }, function (window_B) {
+ waitForFocus(function() {
+ // Add identifying information to window_B
+ ss.setWindowValue(window_B, uniqKey, uniqVal);
+ let state = JSON.parse(ss.getBrowserState());
+ let selectedWindow = state.windows[state.selectedWindow - 1];
+ is(selectedWindow.extData && selectedWindow.extData[uniqKey], uniqVal,
+ "selectedWindow is window_B");
+
+ // Now minimize window_B. The selected window shouldn't have the secret data
+ window_B.minimize();
+ waitForFocus(function() {
+ state = JSON.parse(ss.getBrowserState());
+ selectedWindow = state.windows[state.selectedWindow - 1];
+ ok(!selectedWindow.extData || !selectedWindow.extData[uniqKey],
+ "selectedWindow is not window_B after minimizing it");
+
+ // Now minimize the last open window (assumes no other tests left windows open)
+ window.minimize();
+ state = JSON.parse(ss.getBrowserState());
+ is(state.selectedWindow, 0,
+ "selectedWindow should be 0 when all windows are minimized");
+
+ // Cleanup
+ window.restore();
+ BrowserTestUtils.closeWindow(window_B).then(finish);
+ });
+ }, window_B);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_526613.js b/browser/components/sessionstore/test/browser_526613.js
new file mode 100644
index 000000000..7e7fe8059
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_526613.js
@@ -0,0 +1,72 @@
+/* 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/. */
+
+function test() {
+ /** Test for Bug 526613 **/
+
+ // test setup
+ waitForExplicitFinish();
+
+ function browserWindowsCount(expected) {
+ let count = 0;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ is(count, expected,
+ "number of open browser windows according to nsIWindowMediator");
+ let state = ss.getBrowserState();
+ info(state);
+ is(JSON.parse(state).windows.length, expected,
+ "number of open browser windows according to getBrowserState");
+ }
+
+ browserWindowsCount(1);
+
+ // backup old state
+ let oldState = ss.getBrowserState();
+ // create a new state for testing
+ let testState = {
+ windows: [
+ { tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 },
+ { tabs: [{ entries: [{ url: "about:mozilla" }] }], selected: 1 },
+ ],
+ // make sure the first window is focused, otherwise when restoring the
+ // old state, the first window is closed and the test harness gets unloaded
+ selectedWindow: 1
+ };
+
+ let pass = 1;
+ function observer(aSubject, aTopic, aData) {
+ is(aTopic, "sessionstore-browser-state-restored",
+ "The sessionstore-browser-state-restored notification was observed");
+
+ if (pass++ == 1) {
+ browserWindowsCount(2);
+
+ // let the first window be focused (see above)
+ function pollMostRecentWindow() {
+ if (Services.wm.getMostRecentWindow("navigator:browser") == window) {
+ ss.setBrowserState(oldState);
+ } else {
+ info("waiting for the current window to become active");
+ setTimeout(pollMostRecentWindow, 0);
+ window.focus(); //XXX Why is this needed?
+ }
+ }
+ pollMostRecentWindow();
+ }
+ else {
+ browserWindowsCount(1);
+ ok(!window.closed, "Restoring the old state should have left this window open");
+ Services.obs.removeObserver(observer, "sessionstore-browser-state-restored");
+ finish();
+ }
+ }
+ Services.obs.addObserver(observer, "sessionstore-browser-state-restored", false);
+
+ // set browser to test state
+ ss.setBrowserState(JSON.stringify(testState));
+}
diff --git a/browser/components/sessionstore/test/browser_528776.js b/browser/components/sessionstore/test/browser_528776.js
new file mode 100644
index 000000000..d799c9740
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_528776.js
@@ -0,0 +1,21 @@
+function browserWindowsCount(expected) {
+ var count = 0;
+ var e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements()) {
+ if (!e.getNext().closed)
+ ++count;
+ }
+ is(count, expected,
+ "number of open browser windows according to nsIWindowMediator");
+ is(JSON.parse(ss.getBrowserState()).windows.length, expected,
+ "number of open browser windows according to getBrowserState");
+}
+
+add_task(function() {
+ browserWindowsCount(1);
+
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+ browserWindowsCount(2);
+ yield BrowserTestUtils.closeWindow(win);
+ browserWindowsCount(1);
+});
diff --git a/browser/components/sessionstore/test/browser_579868.js b/browser/components/sessionstore/test/browser_579868.js
new file mode 100644
index 000000000..d6c6245d0
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_579868.js
@@ -0,0 +1,30 @@
+/* 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/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab1 = gBrowser.addTab("about:rights");
+ let tab2 = gBrowser.addTab("about:mozilla");
+
+ promiseBrowserLoaded(tab1.linkedBrowser).then(() => {
+ // Tell the session storer that the tab is pinned
+ let newTabState = '{"entries":[{"url":"about:rights"}],"pinned":true,"userTypedValue":"Hello World!"}';
+ ss.setTabState(tab1, newTabState);
+
+ // Undo pinning
+ gBrowser.unpinTab(tab1);
+
+ // Close and restore tab
+ gBrowser.removeTab(tab1);
+ let savedState = JSON.parse(ss.getClosedTabData(window))[0].state;
+ isnot(savedState.pinned, true, "Pinned should not be true");
+ tab1 = ss.undoCloseTab(window, 0);
+
+ isnot(tab1.pinned, true, "Should not be pinned");
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ finish();
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_579879.js b/browser/components/sessionstore/test/browser_579879.js
new file mode 100644
index 000000000..6886be038
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_579879.js
@@ -0,0 +1,20 @@
+"use strict";
+
+add_task(function* () {
+ let tab1 = gBrowser.addTab("data:text/plain;charset=utf-8,foo");
+ gBrowser.pinTab(tab1);
+
+ yield promiseBrowserLoaded(tab1.linkedBrowser);
+ let tab2 = gBrowser.addTab();
+ gBrowser.pinTab(tab2);
+
+ is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 is at the first position");
+ yield promiseRemoveTab(tab1);
+
+ tab1 = undoCloseTab();
+ ok(tab1.pinned, "pinned tab 1 has been restored as a pinned tab");
+ is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 has been restored to the first position");
+
+ yield BrowserTestUtils.removeTab(tab1);
+ yield BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/sessionstore/test/browser_580512.js b/browser/components/sessionstore/test/browser_580512.js
new file mode 100644
index 000000000..ef048cd37
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_580512.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const URIS_PINNED = ["about:license", "about:about"];
+const URIS_NORMAL_A = ["about:mozilla"];
+const URIS_NORMAL_B = ["about:buildconfig"];
+
+function test() {
+ waitForExplicitFinish();
+
+ isnot(Services.prefs.getIntPref("browser.startup.page"), 3,
+ "pref to save session must not be set for this test");
+ ok(!Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"),
+ "pref to save session once must not be set for this test");
+
+ document.documentElement.setAttribute("windowtype", "navigator:browsertestdummy");
+
+ openWinWithCb(closeFirstWin, URIS_PINNED.concat(URIS_NORMAL_A));
+}
+
+function closeFirstWin(win) {
+ win.gBrowser.pinTab(win.gBrowser.tabs[0]);
+ win.gBrowser.pinTab(win.gBrowser.tabs[1]);
+
+ let winClosed = BrowserTestUtils.windowClosed(win);
+ // We need to call BrowserTryToCloseWindow in order to trigger
+ // the machinery that chooses whether or not to save the session
+ // for the last window.
+ win.BrowserTryToCloseWindow();
+ ok(win.closed, "window closed");
+
+ winClosed.then(() => {
+ openWinWithCb(checkSecondWin, URIS_NORMAL_B, URIS_PINNED.concat(URIS_NORMAL_B));
+ });
+}
+
+function checkSecondWin(win) {
+ is(win.gBrowser.browsers[0].currentURI.spec, URIS_PINNED[0], "first pinned tab restored");
+ is(win.gBrowser.browsers[1].currentURI.spec, URIS_PINNED[1], "second pinned tab restored");
+ ok(win.gBrowser.tabs[0].pinned, "first pinned tab is still pinned");
+ ok(win.gBrowser.tabs[1].pinned, "second pinned tab is still pinned");
+
+ BrowserTestUtils.closeWindow(win).then(() => {
+ // cleanup
+ document.documentElement.setAttribute("windowtype", "navigator:browser");
+ finish();
+ });
+}
+
+function openWinWithCb(cb, argURIs, expectedURIs) {
+ if (!expectedURIs)
+ expectedURIs = argURIs;
+
+ var win = openDialog(getBrowserURL(), "_blank",
+ "chrome,all,dialog=no", argURIs.join("|"));
+
+ win.addEventListener("load", function () {
+ win.removeEventListener("load", arguments.callee, false);
+ info("the window loaded");
+
+ var expectedLoads = expectedURIs.length;
+
+ win.gBrowser.addTabsProgressListener({
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aRequest &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ expectedURIs.indexOf(aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec) > -1 &&
+ --expectedLoads <= 0) {
+ win.gBrowser.removeTabsProgressListener(this);
+ info("all tabs loaded");
+ is(win.gBrowser.tabs.length, expectedURIs.length, "didn't load any unexpected tabs");
+ executeSoon(function () {
+ cb(win);
+ });
+ }
+ }
+ });
+ }, false);
+}
diff --git a/browser/components/sessionstore/test/browser_581937.js b/browser/components/sessionstore/test/browser_581937.js
new file mode 100644
index 000000000..74ddaa9d2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_581937.js
@@ -0,0 +1,19 @@
+// Tests that an about:blank tab with no history will not be saved into
+// session store and thus, it will not show up in Recently Closed Tabs.
+
+"use strict";
+
+add_task(function* () {
+ let tab = gBrowser.addTab("about:blank");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ is(tab.linkedBrowser.currentURI.spec, "about:blank",
+ "we will be removing an about:blank tab");
+
+ let r = `rand-${Math.random()}`;
+ ss.setTabValue(tab, "foobar", r);
+
+ yield promiseRemoveTab(tab);
+ let closedTabData = ss.getClosedTabData(window);
+ ok(!closedTabData.includes(r), "tab not stored in _closedTabs");
+});
diff --git a/browser/components/sessionstore/test/browser_586068-apptabs.js b/browser/components/sessionstore/test/browser_586068-apptabs.js
new file mode 100644
index 000000000..f8727c04f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-apptabs.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+requestLongerTimeout(2);
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
+ { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
+ { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
+ { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
+ ], selected: 5 }] };
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ loadCount++;
+
+ // We'll make sure that the loads we get come from pinned tabs or the
+ // the selected tab.
+
+ // get the tab
+ let tab;
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+ tab = window.gBrowser.tabs[i];
+ }
+
+ ok(tab.pinned || tab.selected,
+ "load came from pinned or selected tab");
+
+ // We should get 4 loads: 3 app tabs + 1 normal selected tab
+ if (loadCount < 4)
+ return;
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js b/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js
new file mode 100644
index 000000000..b58aa649b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+const PREF_RESTORE_PINNED_TABS_ON_DEMAND = "browser.sessionstore.restore_pinned_tabs_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ Services.prefs.setBoolPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND, true);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ Services.prefs.clearUserPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND);
+ });
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
+ { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
+ { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
+ { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
+ ], selected: 5 }] };
+
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ // get the tab
+ let tab;
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+ tab = window.gBrowser.tabs[i];
+ }
+
+ // Check that the load only comes from the selected tab.
+ ok(tab.selected, "load came from selected tab");
+ is(aNeedRestore, 6, "six tabs left to restore");
+ is(aRestoring, 1, "one tab is restoring");
+ is(aRestored, 0, "no tabs have been restored, yet");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
new file mode 100644
index 000000000..de8f1aba0
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+requestLongerTimeout(2);
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // The first state will be loaded using setBrowserState, followed by the 2nd
+ // state also being loaded using setBrowserState, interrupting the first restore.
+ let state1 = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
+ ],
+ selected: 1
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+ ],
+ selected: 3
+ }
+ ] };
+ let state2 = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#7" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#8" }], extData: { "uniq": r() } }
+ ],
+ selected: 3
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#7" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#8" }], extData: { "uniq": r() } },
+ ],
+ selected: 1
+ }
+ ] };
+
+ // interruptedAfter will be set after the selected tab from each window have loaded.
+ let interruptedAfter = 0;
+ let loadedWindow1 = false;
+ let loadedWindow2 = false;
+ let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ loadCount++;
+
+ if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
+ loadedWindow1 = true;
+ if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
+ loadedWindow2 = true;
+
+ if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
+ interruptedAfter = loadCount;
+ ss.setBrowserState(JSON.stringify(state2));
+ return;
+ }
+
+ if (loadCount < numTabs + interruptedAfter)
+ return;
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs + interruptedAfter, "all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ // Remove the progress listener.
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ // We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
+ Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ Services.ww.unregisterNotification(observer);
+ win.gBrowser.addTabsProgressListener(gProgressListener);
+ });
+ }
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state1));
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseAllButPrimaryWindowClosed();
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-cascade.js b/browser/components/sessionstore/test/browser_586068-cascade.js
new file mode 100644
index 000000000..041aea85c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-cascade.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } }
+ ] }] };
+
+ let expectedCounts = [
+ [3, 3, 0],
+ [2, 3, 1],
+ [1, 3, 2],
+ [0, 3, 3],
+ [0, 2, 4],
+ [0, 1, 5]
+ ];
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ loadCount++;
+ let expected = expectedCounts[loadCount - 1];
+
+ is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
+ is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
+ is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
+
+ if (loadCount == state.windows[0].tabs.length) {
+ gProgressListener.unsetCallback();
+ resolve();
+ }
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-multi_window.js b/browser/components/sessionstore/test/browser_586068-multi_window.js
new file mode 100644
index 000000000..03337568e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-multi_window.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // The first window will be put into the already open window and the second
+ // window will be opened with _openWindowWithState, which is the source of the problem.
+ let state = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#0" }], extData: { "uniq": r() } }
+ ],
+ selected: 1
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } }
+ ],
+ selected: 4
+ }
+ ] };
+ let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ if (++loadCount == numTabs) {
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ }
+ });
+ });
+
+ // We also want to catch the 2nd window, so we need to observe domwindowopened
+ Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ Services.ww.unregisterNotification(observer);
+ win.gBrowser.addTabsProgressListener(gProgressListener);
+ });
+ }
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseAllButPrimaryWindowClosed();
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-reload.js b/browser/components/sessionstore/test/browser_586068-reload.js
new file mode 100644
index 000000000..630c91f2d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-reload.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } },
+ ], selected: 1 }] };
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener("SSTabRestored", function onRestored(event) {
+ let tab = event.target;
+ let browser = tab.linkedBrowser;
+ let tabData = state.windows[0].tabs[loadCount++];
+
+ // double check that this tab was the right one
+ is(browser.currentURI.spec, tabData.entries[0].url,
+ "load " + loadCount + " - browser loaded correct url");
+ is(ss.getTabValue(tab, "uniq"), tabData.extData.uniq,
+ "load " + loadCount + " - correct tab was restored");
+
+ if (loadCount == state.windows[0].tabs.length) {
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onRestored);
+ resolve();
+ } else {
+ // reload the next tab
+ gBrowser.browsers[loadCount].reload();
+ }
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-select.js b/browser/components/sessionstore/test/browser_586068-select.js
new file mode 100644
index 000000000..433e1754c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-select.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } }
+ ], selected: 1 }] };
+
+ let expectedCounts = [
+ [5, 1, 0],
+ [4, 1, 1],
+ [3, 1, 2],
+ [2, 1, 3],
+ [1, 1, 4],
+ [0, 1, 5]
+ ];
+ let tabOrder = [0, 5, 1, 4, 3, 2];
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ loadCount++;
+ let expected = expectedCounts[loadCount - 1];
+
+ is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
+ is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
+ is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
+
+ if (loadCount < state.windows[0].tabs.length) {
+ // double check that this tab was the right one
+ let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
+ let tab;
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+ tab = window.gBrowser.tabs[i];
+ }
+
+ is(ss.getTabValue(tab, "uniq"), expectedData,
+ "load " + loadCount + " - correct tab was restored");
+
+ // select the next tab
+ window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
+ } else {
+ gProgressListener.unsetCallback();
+ resolve();
+ }
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-window_state.js b/browser/components/sessionstore/test/browser_586068-window_state.js
new file mode 100644
index 000000000..6097a70db
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-window_state.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // We'll use 2 states so that we can make sure calling setWindowState doesn't
+ // wipe out currently restoring data.
+ let state1 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.com#1" }] },
+ { entries: [{ url: "http://example.com#2" }] },
+ { entries: [{ url: "http://example.com#3" }] },
+ { entries: [{ url: "http://example.com#4" }] },
+ { entries: [{ url: "http://example.com#5" }] },
+ ] }] };
+ let state2 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org#1" }] },
+ { entries: [{ url: "http://example.org#2" }] },
+ { entries: [{ url: "http://example.org#3" }] },
+ { entries: [{ url: "http://example.org#4" }] },
+ { entries: [{ url: "http://example.org#5" }] }
+ ] }] };
+ let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ // When loadCount == 2, we'll also restore state2 into the window
+ if (++loadCount == 2) {
+ ss.setWindowState(window, JSON.stringify(state2), false);
+ }
+
+ if (loadCount < numTabs) {
+ return;
+ }
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setWindowState(window, JSON.stringify(state1), true);
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-window_state_override.js b/browser/components/sessionstore/test/browser_586068-window_state_override.js
new file mode 100644
index 000000000..731e03307
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-window_state_override.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // We'll use 2 states so that we can make sure calling setWindowState doesn't
+ // wipe out currently restoring data.
+ let state1 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.com#1" }] },
+ { entries: [{ url: "http://example.com#2" }] },
+ { entries: [{ url: "http://example.com#3" }] },
+ { entries: [{ url: "http://example.com#4" }] },
+ { entries: [{ url: "http://example.com#5" }] },
+ ] }] };
+ let state2 = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org#1" }] },
+ { entries: [{ url: "http://example.org#2" }] },
+ { entries: [{ url: "http://example.org#3" }] },
+ { entries: [{ url: "http://example.org#4" }] },
+ { entries: [{ url: "http://example.org#5" }] }
+ ] }] };
+ let numTabs = 2 + state2.windows[0].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+ // When loadCount == 2, we'll also restore state2 into the window
+ if (++loadCount == 2) {
+ executeSoon(() => ss.setWindowState(window, JSON.stringify(state2), true));
+ }
+
+ if (loadCount < numTabs) {
+ return;
+ }
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setWindowState(window, JSON.stringify(state1), true);
+ yield promiseRestoringTabs;
+
+ // Cleanup.
+ yield promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586147.js b/browser/components/sessionstore/test/browser_586147.js
new file mode 100644
index 000000000..fbfec53c7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586147.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+function observeOneRestore(callback) {
+ let topic = "sessionstore-browser-state-restored";
+ Services.obs.addObserver(function onRestore() {
+ Services.obs.removeObserver(onRestore, topic);
+ callback();
+ }, topic, false);
+};
+
+function test() {
+ waitForExplicitFinish();
+
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+ let hiddenTab = gBrowser.addTab();
+
+ is(gBrowser.visibleTabs.length, 2, "should have 2 tabs before hiding");
+ gBrowser.showOnlyTheseTabs([origTab]);
+ is(gBrowser.visibleTabs.length, 1, "only 1 after hiding");
+ ok(hiddenTab.hidden, "sanity check that it's hidden");
+
+ let extraTab = gBrowser.addTab();
+ let state = ss.getBrowserState();
+ let stateObj = JSON.parse(state);
+ let tabs = stateObj.windows[0].tabs;
+ is(tabs.length, 3, "just checking that browser state is correct");
+ ok(!tabs[0].hidden, "first tab is visible");
+ ok(tabs[1].hidden, "second is hidden");
+ ok(!tabs[2].hidden, "third is visible");
+
+ // Make the third tab hidden and then restore the modified state object
+ tabs[2].hidden = true;
+
+ observeOneRestore(function() {
+ let testWindow = Services.wm.getEnumerator("navigator:browser").getNext();
+ is(testWindow.gBrowser.visibleTabs.length, 1, "only restored 1 visible tab");
+ let tabs = testWindow.gBrowser.tabs;
+ ok(!tabs[0].hidden, "first is still visible");
+ ok(tabs[1].hidden, "second tab is still hidden");
+ ok(tabs[2].hidden, "third tab is now hidden");
+
+ // Restore the original state and clean up now that we're done
+ gBrowser.removeTab(hiddenTab);
+ gBrowser.removeTab(extraTab);
+
+ finish();
+ });
+ ss.setBrowserState(JSON.stringify(stateObj));
+}
diff --git a/browser/components/sessionstore/test/browser_588426.js b/browser/components/sessionstore/test/browser_588426.js
new file mode 100644
index 000000000..d2462f2bd
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_588426.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let state = { windows: [{ tabs: [
+ {entries: [{url: "about:mozilla"}], hidden: true},
+ {entries: [{url: "about:rights"}], hidden: true}
+ ] }] };
+
+ waitForExplicitFinish();
+
+ newWindowWithState(state, function (win) {
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ is(win.gBrowser.tabs.length, 2, "two tabs were restored");
+ is(win.gBrowser.visibleTabs.length, 1, "one tab is visible");
+
+ let tab = win.gBrowser.visibleTabs[0];
+ is(tab.linkedBrowser.currentURI.spec, "about:mozilla", "visible tab is about:mozilla");
+
+ finish();
+ });
+}
+
+function newWindowWithState(state, callback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(getBrowserURL(), "_blank", opts);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ executeSoon(function () {
+ win.addEventListener("SSWindowStateReady", function onReady() {
+ win.removeEventListener("SSWindowStateReady", onReady, false);
+ promiseTabRestored(win.gBrowser.tabs[0]).then(() => callback(win));
+ }, false);
+
+ ss.setWindowState(win, JSON.stringify(state), true);
+ });
+ }, false);
+}
diff --git a/browser/components/sessionstore/test/browser_589246.js b/browser/components/sessionstore/test/browser_589246.js
new file mode 100644
index 000000000..d1f539073
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_589246.js
@@ -0,0 +1,242 @@
+/* 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/. */
+
+// Mirrors WINDOW_ATTRIBUTES IN nsSessionStore.js
+const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
+
+var stateBackup = ss.getBrowserState();
+
+var originalWarnOnClose = gPrefService.getBoolPref("browser.tabs.warnOnClose");
+var originalStartupPage = gPrefService.getIntPref("browser.startup.page");
+var originalWindowType = document.documentElement.getAttribute("windowtype");
+
+var gotLastWindowClosedTopic = false;
+var shouldPinTab = false;
+var shouldOpenTabs = false;
+var shouldCloseTab = false;
+var testNum = 0;
+var afterTestCallback;
+
+// Set state so we know the closed windows content
+var testState = {
+ windows: [
+ { tabs: [{ entries: [{ url: "http://example.org" }] }] }
+ ],
+ _closedWindows: []
+};
+
+// We'll push a set of conditions and callbacks into this array
+// Ideally we would also test win/linux under a complete set of conditions, but
+// the tests for osx mirror the other set of conditions possible on win/linux.
+var tests = [];
+
+// the third & fourth test share a condition check, keep it DRY
+function checkOSX34Generator(num) {
+ return function(aPreviousState, aCurState) {
+ // In here, we should have restored the pinned tab, so only the unpinned tab
+ // should be in aCurState. So let's shape our expectations.
+ let expectedState = JSON.parse(aPreviousState);
+ expectedState[0].tabs.shift();
+ // size attributes are stripped out in _prepDataForDeferredRestore in nsSessionStore.
+ // This isn't the best approach, but neither is comparing JSON strings
+ WINDOW_ATTRIBUTES.forEach(attr => delete expectedState[0][attr]);
+
+ is(aCurState, JSON.stringify(expectedState),
+ "test #" + num + ": closedWindowState is as expected");
+ };
+}
+function checkNoWindowsGenerator(num) {
+ return function(aPreviousState, aCurState) {
+ is(aCurState, "[]", "test #" + num + ": there should be no closedWindowsLeft");
+ };
+}
+
+// The first test has 0 pinned tabs and 1 unpinned tab
+tests.push({
+ pinned: false,
+ extra: false,
+ close: false,
+ checkWinLin: checkNoWindowsGenerator(1),
+ checkOSX: function(aPreviousState, aCurState) {
+ is(aCurState, aPreviousState, "test #1: closed window state is unchanged");
+ }
+});
+
+// The second test has 1 pinned tab and 0 unpinned tabs.
+tests.push({
+ pinned: true,
+ extra: false,
+ close: false,
+ checkWinLin: checkNoWindowsGenerator(2),
+ checkOSX: checkNoWindowsGenerator(2)
+});
+
+// The third test has 1 pinned tab and 2 unpinned tabs.
+tests.push({
+ pinned: true,
+ extra: true,
+ close: false,
+ checkWinLin: checkNoWindowsGenerator(3),
+ checkOSX: checkOSX34Generator(3)
+});
+
+// The fourth test has 1 pinned tab, 2 unpinned tabs, and closes one unpinned tab.
+tests.push({
+ pinned: true,
+ extra: true,
+ close: "one",
+ checkWinLin: checkNoWindowsGenerator(4),
+ checkOSX: checkOSX34Generator(4)
+});
+
+// The fifth test has 1 pinned tab, 2 unpinned tabs, and closes both unpinned tabs.
+tests.push({
+ pinned: true,
+ extra: true,
+ close: "both",
+ checkWinLin: checkNoWindowsGenerator(5),
+ checkOSX: checkNoWindowsGenerator(5)
+});
+
+
+function test() {
+ /** Test for Bug 589246 - Closed window state getting corrupted when closing
+ and reopening last browser window without exiting browser **/
+ waitForExplicitFinish();
+ // windows opening & closing, so extending the timeout
+ requestLongerTimeout(2);
+
+ // We don't want the quit dialog pref
+ gPrefService.setBoolPref("browser.tabs.warnOnClose", false);
+ // Ensure that we would restore the session (important for Windows)
+ gPrefService.setIntPref("browser.startup.page", 3);
+
+ runNextTestOrFinish();
+}
+
+function runNextTestOrFinish() {
+ if (tests.length) {
+ setupForTest(tests.shift())
+ }
+ else {
+ // some state is cleaned up at the end of each test, but not all
+ ["browser.tabs.warnOnClose", "browser.startup.page"].forEach(function(p) {
+ if (gPrefService.prefHasUserValue(p))
+ gPrefService.clearUserPref(p);
+ });
+
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ }
+}
+
+function setupForTest(aConditions) {
+ // reset some checks
+ gotLastWindowClosedTopic = false;
+ shouldPinTab = aConditions.pinned;
+ shouldOpenTabs = aConditions.extra;
+ shouldCloseTab = aConditions.close;
+ testNum++;
+
+ // set our test callback
+ afterTestCallback = /Mac/.test(navigator.platform) ? aConditions.checkOSX
+ : aConditions.checkWinLin;
+
+ // Add observers
+ Services.obs.addObserver(onLastWindowClosed, "browser-lastwindow-close-granted", false);
+
+ // Set the state
+ Services.obs.addObserver(onStateRestored, "sessionstore-browser-state-restored", false);
+ ss.setBrowserState(JSON.stringify(testState));
+}
+
+function onStateRestored(aSubject, aTopic, aData) {
+ info("test #" + testNum + ": onStateRestored");
+ Services.obs.removeObserver(onStateRestored, "sessionstore-browser-state-restored");
+
+ // change this window's windowtype so that closing a new window will trigger
+ // browser-lastwindow-close-granted.
+ document.documentElement.setAttribute("windowtype", "navigator:testrunner");
+
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no", "http://example.com");
+ newWin.addEventListener("load", function(aEvent) {
+ newWin.removeEventListener("load", arguments.callee, false);
+
+ promiseBrowserLoaded(newWin.gBrowser.selectedBrowser).then(() => {
+ // pin this tab
+ if (shouldPinTab)
+ newWin.gBrowser.pinTab(newWin.gBrowser.selectedTab);
+
+ newWin.addEventListener("unload", function () {
+ newWin.removeEventListener("unload", arguments.callee, false);
+ onWindowUnloaded();
+ }, false);
+ // Open a new tab as well. On Windows/Linux this will be restored when the
+ // new window is opened below (in onWindowUnloaded). On OS X we'll just
+ // restore the pinned tabs, leaving the unpinned tab in the closedWindowsData.
+ if (shouldOpenTabs) {
+ let newTab = newWin.gBrowser.addTab("about:config");
+ let newTab2 = newWin.gBrowser.addTab("about:buildconfig");
+
+ newTab.linkedBrowser.addEventListener("load", function() {
+ newTab.linkedBrowser.removeEventListener("load", arguments.callee, true);
+
+ if (shouldCloseTab == "one") {
+ newWin.gBrowser.removeTab(newTab2);
+ }
+ else if (shouldCloseTab == "both") {
+ newWin.gBrowser.removeTab(newTab);
+ newWin.gBrowser.removeTab(newTab2);
+ }
+ newWin.BrowserTryToCloseWindow();
+ }, true);
+ }
+ else {
+ newWin.BrowserTryToCloseWindow();
+ }
+ });
+ }, false);
+}
+
+// This will be called before the window is actually closed
+function onLastWindowClosed(aSubject, aTopic, aData) {
+ info("test #" + testNum + ": onLastWindowClosed");
+ Services.obs.removeObserver(onLastWindowClosed, "browser-lastwindow-close-granted");
+ gotLastWindowClosedTopic = true;
+}
+
+// This is the unload event listener on the new window (from onStateRestored).
+// Unload is fired after the window is closed, so sessionstore has already
+// updated _closedWindows (which is important). We'll open a new window here
+// which should actually trigger the bug.
+function onWindowUnloaded() {
+ info("test #" + testNum + ": onWindowClosed");
+ ok(gotLastWindowClosedTopic, "test #" + testNum + ": browser-lastwindow-close-granted was notified prior");
+
+ let previousClosedWindowData = ss.getClosedWindowData();
+
+ // Now we want to open a new window
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no", "about:mozilla");
+ newWin.addEventListener("load", function(aEvent) {
+ newWin.removeEventListener("load", arguments.callee, false);
+
+ newWin.gBrowser.selectedBrowser.addEventListener("load", function () {
+ newWin.gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+
+ // Good enough for checking the state
+ afterTestCallback(previousClosedWindowData, ss.getClosedWindowData());
+ afterTestCleanup(newWin);
+ }, true);
+
+ }, false);
+}
+
+function afterTestCleanup(aNewWin) {
+ executeSoon(function() {
+ BrowserTestUtils.closeWindow(aNewWin).then(() => {
+ document.documentElement.setAttribute("windowtype", originalWindowType);
+ runNextTestOrFinish();
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_590268.js b/browser/components/sessionstore/test/browser_590268.js
new file mode 100644
index 000000000..2b0c2f32d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_590268.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const NUM_TABS = 12;
+
+var stateBackup = ss.getBrowserState();
+
+function test() {
+ /** Test for Bug 590268 - Provide access to sessionstore tab data sooner **/
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ let startedTest = false;
+
+ // wasLoaded will be used to keep track of tabs that have already had SSTabRestoring
+ // fired for them.
+ let wasLoaded = { };
+ let restoringTabsCount = 0;
+ let restoredTabsCount = 0;
+ let uniq2 = { };
+ let uniq2Count = 0;
+ let state = { windows: [{ tabs: [] }] };
+ // We're going to put a bunch of tabs into this state
+ for (let i = 0; i < NUM_TABS; i++) {
+ let uniq = r();
+ let tabData = {
+ entries: [{ url: "http://example.com/#" + i }],
+ extData: { "uniq": uniq, "baz": "qux" }
+ };
+ state.windows[0].tabs.push(tabData);
+ wasLoaded[uniq] = false;
+ }
+
+
+ function onSSTabRestoring(aEvent) {
+ restoringTabsCount++;
+ let uniq = ss.getTabValue(aEvent.originalTarget, "uniq");
+ wasLoaded[uniq] = true;
+
+ is(ss.getTabValue(aEvent.originalTarget, "foo"), "",
+ "There is no value for 'foo'");
+
+ // On the first SSTabRestoring we're going to run the the real test.
+ // We'll keep this listener around so we can keep marking tabs as restored.
+ if (restoringTabsCount == 1)
+ onFirstSSTabRestoring();
+ else if (restoringTabsCount == NUM_TABS)
+ onLastSSTabRestoring();
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++restoredTabsCount < NUM_TABS)
+ return;
+ cleanup();
+ }
+
+ function onTabOpen(aEvent) {
+ // To test bug 614708, we'll just set a value on the tab here. This value
+ // would previously cause us to not recognize the values in extData until
+ // much later. So testing "uniq" failed.
+ ss.setTabValue(aEvent.originalTarget, "foo", "bar");
+ }
+
+ // This does the actual testing. SSTabRestoring should be firing on tabs from
+ // left to right, so we're going to start with the rightmost tab.
+ function onFirstSSTabRestoring() {
+ info("onFirstSSTabRestoring...");
+ for (let i = gBrowser.tabs.length - 1; i >= 0; i--) {
+ let tab = gBrowser.tabs[i];
+ let actualUniq = ss.getTabValue(tab, "uniq");
+ let expectedUniq = state.windows[0].tabs[i].extData["uniq"];
+
+ if (wasLoaded[actualUniq]) {
+ info("tab " + i + ": already restored");
+ continue;
+ }
+ is(actualUniq, expectedUniq, "tab " + i + ": extData was correct");
+
+ // Now we're going to set a piece of data back on the tab so it can be read
+ // to test setting a value "early".
+ uniq2[actualUniq] = r();
+ ss.setTabValue(tab, "uniq2", uniq2[actualUniq]);
+
+ // Delete the value we have for "baz". This tests that deleteTabValue
+ // will delete "early access" values (c.f. bug 617175). If this doesn't throw
+ // then the test is successful.
+ try {
+ ss.deleteTabValue(tab, "baz");
+ }
+ catch (e) {
+ ok(false, "no error calling deleteTabValue - " + e);
+ }
+
+ // This will be used in the final comparison to make sure we checked the
+ // same number as we set.
+ uniq2Count++;
+ }
+ }
+
+ function onLastSSTabRestoring() {
+ let checked = 0;
+ for (let i = 0; i < gBrowser.tabs.length; i++) {
+ let tab = gBrowser.tabs[i];
+ let uniq = ss.getTabValue(tab, "uniq");
+
+ // Look to see if we set a uniq2 value for this uniq value
+ if (uniq in uniq2) {
+ is(ss.getTabValue(tab, "uniq2"), uniq2[uniq], "tab " + i + " has correct uniq2 value");
+ checked++;
+ }
+ }
+ ok(uniq2Count > 0, "at least 1 tab properly checked 'early access'");
+ is(checked, uniq2Count, "checked the same number of uniq2 as we set");
+ }
+
+ function cleanup() {
+ // remove the event listener and clean up before finishing
+ gBrowser.tabContainer.removeEventListener("SSTabRestoring", onSSTabRestoring, false);
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true);
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, false);
+ // Put this in an executeSoon because we still haven't called restoreNextTab
+ // in sessionstore for the last tab (we'll call it after this). We end up
+ // trying to restore the tab (since we then add a closed tab to the array).
+ executeSoon(function() {
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ });
+ }
+
+ // Add the event listeners
+ gBrowser.tabContainer.addEventListener("SSTabRestoring", onSSTabRestoring, false);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true);
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, false);
+ // Restore state
+ ss.setBrowserState(JSON.stringify(state));
+}
diff --git a/browser/components/sessionstore/test/browser_590563.js b/browser/components/sessionstore/test/browser_590563.js
new file mode 100644
index 000000000..5d1d8f866
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_590563.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let sessionData = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }], hidden: true },
+ { entries: [{ url: "about:blank" }], hidden: false }
+ ]
+ }]
+ };
+ let url = "about:sessionrestore";
+ let formdata = {id: {sessionData}, url};
+ let state = { windows: [{ tabs: [{ entries: [{url}], formdata }] }] };
+
+ waitForExplicitFinish();
+
+ newWindowWithState(state, function (win) {
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ is(gBrowser.tabs.length, 1, "The total number of tabs should be 1");
+ is(gBrowser.visibleTabs.length, 1, "The total number of visible tabs should be 1");
+
+ executeSoon(function () {
+ waitForFocus(function () {
+ middleClickTest(win);
+ finish();
+ }, win);
+ });
+ });
+}
+
+function middleClickTest(win) {
+ let browser = win.gBrowser.selectedBrowser;
+ let tree = browser.contentDocument.getElementById("tabList");
+ is(tree.view.rowCount, 3, "There should be three items");
+
+ // click on the first tab item
+ var rect = tree.treeBoxObject.getCoordsForCellItem(1, tree.columns[1], "text");
+ EventUtils.synthesizeMouse(tree.body, rect.x, rect.y, { button: 1 },
+ browser.contentWindow);
+ // click on the second tab item
+ rect = tree.treeBoxObject.getCoordsForCellItem(2, tree.columns[1], "text");
+ EventUtils.synthesizeMouse(tree.body, rect.x, rect.y, { button: 1 },
+ browser.contentWindow);
+
+ is(win.gBrowser.tabs.length, 3,
+ "The total number of tabs should be 3 after restoring 2 tabs by middle click.");
+ is(win.gBrowser.visibleTabs.length, 3,
+ "The total number of visible tabs should be 3 after restoring 2 tabs by middle click");
+}
+
+function newWindowWithState(state, callback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(getBrowserURL(), "_blank", opts);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ let tab = win.gBrowser.selectedTab;
+
+ // The form data will be restored before SSTabRestored, so we want to listen
+ // for that on the currently selected tab (it will be reused)
+ tab.addEventListener("SSTabRestored", function onRestored() {
+ tab.removeEventListener("SSTabRestored", onRestored, true);
+ callback(win);
+ }, true);
+
+ executeSoon(function () {
+ ss.setWindowState(win, JSON.stringify(state), true);
+ });
+ }, false);
+}
diff --git a/browser/components/sessionstore/test/browser_595601-restore_hidden.js b/browser/components/sessionstore/test/browser_595601-restore_hidden.js
new file mode 100644
index 000000000..4c2b2d24a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_595601-restore_hidden.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var state = {windows:[{tabs:[
+ {entries:[{url:"http://example.com#1"}]},
+ {entries:[{url:"http://example.com#2"}]},
+ {entries:[{url:"http://example.com#3"}]},
+ {entries:[{url:"http://example.com#4"}]},
+ {entries:[{url:"http://example.com#5"}], hidden: true},
+ {entries:[{url:"http://example.com#6"}], hidden: true},
+ {entries:[{url:"http://example.com#7"}], hidden: true},
+ {entries:[{url:"http://example.com#8"}], hidden: true}
+]}]};
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.restore_hidden_tabs");
+ });
+
+ // First stage: restoreHiddenTabs = true
+ // Second stage: restoreHiddenTabs = false
+ test_loadTabs(true, function () {
+ test_loadTabs(false, finish);
+ });
+}
+
+function test_loadTabs(restoreHiddenTabs, callback) {
+ Services.prefs.setBoolPref("browser.sessionstore.restore_hidden_tabs", restoreHiddenTabs);
+
+ let expectedTabs = restoreHiddenTabs ? 8 : 4;
+ let firstProgress = true;
+
+ newWindowWithState(state, function (win, needsRestore, isRestoring) {
+ if (firstProgress) {
+ firstProgress = false;
+ is(isRestoring, 3, "restoring 3 tabs concurrently");
+ } else {
+ ok(isRestoring < 4, "restoring max. 3 tabs concurrently");
+ }
+
+ // We're explicity checking for (isRestoring == 1) here because the test
+ // progress listener is called before the session store one. So when we're
+ // called with one tab left to restore we know that the last tab has
+ // finished restoring and will soon be handled by the SS listener.
+ let tabsNeedingRestore = win.gBrowser.tabs.length - needsRestore;
+ if (isRestoring == 1 && tabsNeedingRestore == expectedTabs) {
+ is(win.gBrowser.visibleTabs.length, 4, "only 4 visible tabs");
+
+ TabsProgressListener.uninit();
+ executeSoon(callback);
+ }
+ });
+}
+
+var TabsProgressListener = {
+ init: function (win) {
+ this.window = win;
+ Services.obs.addObserver(this, "sessionstore-debug-tab-restored", false);
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "sessionstore-debug-tab-restored");
+
+ delete this.window;
+ delete this.callback;
+ },
+
+ setCallback: function (callback) {
+ this.callback = callback;
+ },
+
+ observe: function (browser) {
+ TabsProgressListener.onRestored(browser);
+ },
+
+ onRestored: function (browser) {
+ if (this.callback && browser.__SS_restoreState == TAB_STATE_RESTORING)
+ this.callback.apply(null, [this.window].concat(this.countTabs()));
+ },
+
+ countTabs: function () {
+ let needsRestore = 0, isRestoring = 0;
+
+ for (let i = 0; i < this.window.gBrowser.tabs.length; i++) {
+ let browser = this.window.gBrowser.tabs[i].linkedBrowser;
+ if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+ isRestoring++;
+ else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+ needsRestore++;
+ }
+
+ return [needsRestore, isRestoring];
+ }
+}
+
+// ----------
+function newWindowWithState(state, callback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(getBrowserURL(), "_blank", opts);
+
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ whenWindowLoaded(win, function onWindowLoaded(aWin) {
+ TabsProgressListener.init(aWin);
+ TabsProgressListener.setCallback(callback);
+
+ ss.setWindowState(aWin, JSON.stringify(state), true);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_597071.js b/browser/components/sessionstore/test/browser_597071.js
new file mode 100644
index 000000000..f8ddaaf54
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_597071.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 597071 - Closed windows should only be resurrected when there is a single
+ * popup window
+ */
+add_task(function test_close_last_nonpopup_window() {
+ // Purge the list of closed windows.
+ forgetClosedWindows();
+
+ let oldState = ss.getWindowState(window);
+
+ let popupState = {windows: [
+ {tabs: [{entries: []}], isPopup: true, hidden: "toolbar"}
+ ]};
+
+ // Set this window to be a popup.
+ ss.setWindowState(window, JSON.stringify(popupState), true);
+
+ // Open a new window with a tab.
+ let win = yield BrowserTestUtils.openNewBrowserWindow({private: false});
+ let tab = win.gBrowser.addTab("http://example.com/");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Make sure sessionstore sees this window.
+ let state = JSON.parse(ss.getBrowserState());
+ is(state.windows.length, 2, "sessionstore knows about this window");
+
+ // Closed the window and check the closed window count.
+ yield BrowserTestUtils.closeWindow(win);
+ is(ss.getClosedWindowCount(), 1, "correct closed window count");
+
+ // Cleanup.
+ ss.setWindowState(window, oldState, true);
+});
diff --git a/browser/components/sessionstore/test/browser_599909.js b/browser/components/sessionstore/test/browser_599909.js
new file mode 100644
index 000000000..1d2c411fe
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_599909.js
@@ -0,0 +1,120 @@
+/* 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/. */
+
+var stateBackup = ss.getBrowserState();
+
+function cleanup() {
+ // Reset the pref
+ try {
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+ } catch (e) {}
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+}
+
+function test() {
+ /** Bug 599909 - to-be-reloaded tabs don't show up in switch-to-tab **/
+ waitForExplicitFinish();
+
+ // Set the pref to true so we know exactly how many tabs should be restoring at
+ // any given time. This guarantees that a finishing load won't start another.
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org/#1" }] },
+ { entries: [{ url: "http://example.org/#2" }] },
+ { entries: [{ url: "http://example.org/#3" }] },
+ { entries: [{ url: "http://example.org/#4" }] }
+ ], selected: 1 }] };
+
+ let tabsForEnsure = {};
+ state.windows[0].tabs.forEach(function(tab) {
+ tabsForEnsure[tab.entries[0].url] = 1;
+ });
+
+ let tabsRestoring = 0;
+ let tabsRestored = 0;
+
+ function handleEvent(aEvent) {
+ if (aEvent.type == "SSTabRestoring")
+ tabsRestoring++;
+ else
+ tabsRestored++;
+
+ if (tabsRestoring < state.windows[0].tabs.length ||
+ tabsRestored < 1)
+ return;
+
+ gBrowser.tabContainer.removeEventListener("SSTabRestoring", handleEvent, true);
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", handleEvent, true);
+ executeSoon(function() {
+ checkAutocompleteResults(tabsForEnsure, cleanup);
+ });
+ }
+
+ // currentURI is set before SSTabRestoring is fired, so we can sucessfully check
+ // after that has fired for all tabs. Since 1 tab will be restored though, we
+ // also need to wait for 1 SSTabRestored since currentURI will be set, unset, then set.
+ gBrowser.tabContainer.addEventListener("SSTabRestoring", handleEvent, true);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", handleEvent, true);
+ ss.setBrowserState(JSON.stringify(state));
+}
+
+// The following was taken from browser/base/content/test/general/browser_tabMatchesInAwesomebar.js
+// so that we could do the same sort of checking.
+var gController = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+function checkAutocompleteResults(aExpected, aCallback) {
+ gController.input = {
+ timeout: 10,
+ textValue: "",
+ searches: ["unifiedcomplete"],
+ searchParam: "enable-actions",
+ popupOpen: false,
+ minResultsForPopup: 0,
+ invalidate: function() {},
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+ get popup() { return this; },
+ onSearchBegin: function() {},
+ onSearchComplete: function ()
+ {
+ info("Found " + gController.matchCount + " matches.");
+ // Check to see the expected uris and titles match up (in any order)
+ for (let i = 0; i < gController.matchCount; i++) {
+ if (gController.getStyleAt(i).includes("heuristic")) {
+ info("Skip heuristic match");
+ continue;
+ }
+ let action = gURLBar.popup.input._parseActionUrl(gController.getValueAt(i));
+ let uri = action.params.url;
+
+ info("Search for '" + uri + "' in open tabs.");
+ ok(uri in aExpected, "Registered open page found in autocomplete.");
+ // Remove the found entry from expected results.
+ delete aExpected[uri];
+ }
+
+ // Make sure there is no reported open page that is not open.
+ for (let entry in aExpected) {
+ ok(false, "'" + entry + "' not found in autocomplete.");
+ }
+
+ executeSoon(aCallback);
+ },
+ setSelectedIndex: function() {},
+ get searchCount() { return this.searches.length; },
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ ])
+ };
+
+ info("Searching open pages.");
+ gController.startSearch(Services.prefs.getCharPref("browser.urlbar.restrict.openpage"));
+}
diff --git a/browser/components/sessionstore/test/browser_600545.js b/browser/components/sessionstore/test/browser_600545.js
new file mode 100644
index 000000000..6852357c2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_600545.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+requestLongerTimeout(2);
+
+var stateBackup = JSON.parse(ss.getBrowserState());
+
+function test() {
+ /** Test for Bug 600545 **/
+ waitForExplicitFinish();
+ testBug600545();
+}
+
+function testBug600545() {
+ // Set the pref to false to cause non-app tabs to be stripped out on a save
+ Services.prefs.setBoolPref("browser.sessionstore.resume_from_crash", false);
+ Services.prefs.setIntPref("browser.sessionstore.interval", 2000);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.resume_from_crash");
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ });
+
+ // This tests the following use case: When multiple windows are open
+ // and browser.sessionstore.resume_from_crash preference is false,
+ // tab session data for non-active window is stripped for non-pinned
+ // tabs. This occurs after "sessionstore-state-write-complete"
+ // fires which will only fire in this case if there is at least one
+ // pinned tab.
+ let state = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#0" }], pinned:true },
+ { entries: [{ url: "http://example.com#1" }] },
+ { entries: [{ url: "http://example.com#2" }] },
+ ],
+ selected: 2
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#3" }] },
+ { entries: [{ url: "http://example.com#4" }] },
+ { entries: [{ url: "http://example.com#5" }] },
+ { entries: [{ url: "http://example.com#6" }] }
+ ],
+ selected: 3
+ }
+ ] };
+
+ waitForBrowserState(state, function() {
+ // Need to wait for SessionStore's saveState function to be called
+ // so that non-pinned tabs will be stripped from non-active window
+ waitForSaveState(function () {
+ let expectedNumberOfTabs = getStateTabCount(state);
+ let retrievedState = JSON.parse(ss.getBrowserState());
+ let actualNumberOfTabs = getStateTabCount(retrievedState);
+
+ is(actualNumberOfTabs, expectedNumberOfTabs,
+ "Number of tabs in retreived session data, matches number of tabs set.");
+
+ done();
+ });
+ });
+}
+
+function done() {
+ // Enumerate windows and close everything but our primary window. We can't
+ // use waitForFocus() because apparently it's buggy. See bug 599253.
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ let closeWinPromises = [];
+ while (windowsEnum.hasMoreElements()) {
+ let currentWindow = windowsEnum.getNext();
+ if (currentWindow != window)
+ closeWinPromises.push(BrowserTestUtils.closeWindow(currentWindow));
+ }
+
+ Promise.all(closeWinPromises).then(() => {
+ waitForBrowserState(stateBackup, finish);
+ });
+}
+
+// Count up the number of tabs in the state data
+function getStateTabCount(aState) {
+ let tabCount = 0;
+ for (let i in aState.windows)
+ tabCount += aState.windows[i].tabs.length;
+ return tabCount;
+}
diff --git a/browser/components/sessionstore/test/browser_601955.js b/browser/components/sessionstore/test/browser_601955.js
new file mode 100644
index 000000000..797d5d7cc
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_601955.js
@@ -0,0 +1,54 @@
+/* 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 tests that pinning/unpinning a tab, on its own, eventually triggers a
+// session store.
+
+function test() {
+ waitForExplicitFinish();
+ // We speed up the interval between session saves to ensure that the test
+ // runs quickly.
+ Services.prefs.setIntPref("browser.sessionstore.interval", 2000);
+
+ // Loading a tab causes a save state and this is meant to catch that event.
+ waitForSaveState(testBug601955_1);
+
+ // Assumption: Only one window is open and it has one tab open.
+ gBrowser.addTab("about:mozilla");
+}
+
+function testBug601955_1() {
+ // Because pinned tabs are at the front of |gBrowser.tabs|, pinning tabs
+ // re-arranges the |tabs| array.
+ ok(!gBrowser.tabs[0].pinned, "first tab should not be pinned yet");
+ ok(!gBrowser.tabs[1].pinned, "second tab should not be pinned yet");
+
+ waitForSaveState(testBug601955_2);
+ gBrowser.pinTab(gBrowser.tabs[0]);
+}
+
+function testBug601955_2() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(state.windows[0].tabs[0].pinned, "first tab should be pinned by now");
+ ok(!state.windows[0].tabs[1].pinned, "second tab should still not be pinned");
+
+ waitForSaveState(testBug601955_3);
+ gBrowser.unpinTab(window.gBrowser.tabs[0]);
+}
+
+function testBug601955_3() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(!state.windows[0].tabs[0].pinned, "first tab should not be pinned");
+ ok(!state.windows[0].tabs[1].pinned, "second tab should not be pinned");
+
+ done();
+}
+
+function done() {
+ gBrowser.removeTab(window.gBrowser.tabs[1]);
+
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+
+ executeSoon(finish);
+}
diff --git a/browser/components/sessionstore/test/browser_607016.js b/browser/components/sessionstore/test/browser_607016.js
new file mode 100644
index 000000000..ed4b03b9c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_607016.js
@@ -0,0 +1,98 @@
+"use strict";
+
+var stateBackup = ss.getBrowserState();
+
+add_task(function* () {
+ /** Bug 607016 - If a tab is never restored, attributes (eg. hidden) aren't updated correctly **/
+ ignoreAllUncaughtExceptions();
+
+ // Set the pref to true so we know exactly how many tabs should be restoring at
+ // any given time. This guarantees that a finishing load won't start another.
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ let state = { windows: [{ tabs: [
+ { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } }, // overwriting
+ { entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } }, // hiding
+ { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }, // adding
+ { entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } }, // deleting
+ { entries: [{ url: "http://example.org#6" }] } // creating
+ ], selected: 1 }] };
+
+ function* progressCallback() {
+ let curState = JSON.parse(ss.getBrowserState());
+ for (let i = 0; i < curState.windows[0].tabs.length; i++) {
+ let tabState = state.windows[0].tabs[i];
+ let tabCurState = curState.windows[0].tabs[i];
+ if (tabState.extData) {
+ is(tabCurState.extData["uniq"], tabState.extData["uniq"],
+ "sanity check that tab has correct extData");
+ }
+ else {
+ // We aren't expecting there to be any data on extData, but panorama
+ // may be setting something, so we need to make sure that if we do have
+ // data, we just don't have anything for "uniq".
+ ok(!("extData" in tabCurState) || !("uniq" in tabCurState.extData),
+ "sanity check that tab doesn't have extData or extData doesn't have 'uniq'");
+ }
+ }
+
+ // Now we'll set a new unique value on 1 of the tabs
+ let newUniq = r();
+ ss.setTabValue(gBrowser.tabs[1], "uniq", newUniq);
+ let tabState = JSON.parse(ss.getTabState(gBrowser.tabs[1]));
+ is(tabState.extData.uniq, newUniq,
+ "(overwriting) new data is stored in extData");
+
+ // hide the next tab before closing it
+ gBrowser.hideTab(gBrowser.tabs[2]);
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[2]));
+ ok(tabState.hidden, "(hiding) tab data has hidden == true");
+
+ // set data that's not in a conflicting key
+ let stillUniq = r();
+ ss.setTabValue(gBrowser.tabs[3], "stillUniq", stillUniq);
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[3]));
+ is(tabState.extData.stillUniq, stillUniq,
+ "(adding) new data is stored in extData");
+
+ // remove the uniq value and make sure it's not there in the closed data
+ ss.deleteTabValue(gBrowser.tabs[4], "uniq");
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[4]));
+ // Since Panorama might have put data in, first check if there is extData.
+ // If there is explicitly check that "uniq" isn't in it. Otherwise, we're ok
+ if ("extData" in tabState) {
+ ok(!("uniq" in tabState.extData),
+ "(deleting) uniq not in existing extData");
+ }
+ else {
+ ok(true, "(deleting) no data is stored in extData");
+ }
+
+ // set unique data on the tab that never had any set, make sure that's saved
+ let newUniq2 = r();
+ ss.setTabValue(gBrowser.tabs[5], "uniq", newUniq2);
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[5]));
+ is(tabState.extData.uniq, newUniq2,
+ "(creating) new data is stored in extData where there was none");
+
+ while (gBrowser.tabs.length > 1) {
+ yield promiseRemoveTab(gBrowser.tabs[1]);
+ }
+ }
+
+ // Set the test state.
+ ss.setBrowserState(JSON.stringify(state));
+
+ // Wait until the selected tab is restored and all others are pending.
+ yield Promise.all(Array.map(gBrowser.tabs, tab => {
+ return (tab == gBrowser.selectedTab) ?
+ promiseTabRestored(tab) : promiseTabRestoring(tab)
+ }));
+
+ // Kick off the actual tests.
+ yield progressCallback();
+
+ // Cleanup.
+ yield promiseBrowserState(stateBackup);
+});
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js
new file mode 100644
index 000000000..0b6b8faa6
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js
@@ -0,0 +1,361 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const stateBackup = JSON.parse(ss.getBrowserState());
+const testState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:blank" }] },
+ { entries: [{ url: "about:rights" }] }
+ ]
+ }]
+};
+const lameMultiWindowState = { windows: [
+ {
+ tabs: [
+ { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
+ ],
+ selected: 1
+ },
+ {
+ tabs: [
+ { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+ { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+ ],
+ selected: 3
+ }
+ ] };
+
+
+function getOuterWindowID(aWindow) {
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+}
+
+function test() {
+ /** Test for Bug 615394 - Session Restore should notify when it is beginning and ending a restore **/
+ waitForExplicitFinish();
+ // Preemptively extend the timeout to prevent [orange]
+ requestLongerTimeout(4);
+ runNextTest();
+}
+
+
+var tests = [
+ test_setTabState,
+ test_duplicateTab,
+ test_undoCloseTab,
+ test_setWindowState,
+ test_setBrowserState,
+ test_undoCloseWindow
+];
+function runNextTest() {
+ // set an empty state & run the next test, or finish
+ if (tests.length) {
+ // Enumerate windows and close everything but our primary window. We can't
+ // use waitForFocus() because apparently it's buggy. See bug 599253.
+ var windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ let closeWinPromises = [];
+ while (windowsEnum.hasMoreElements()) {
+ var currentWindow = windowsEnum.getNext();
+ if (currentWindow != window) {
+ closeWinPromises.push(BrowserTestUtils.closeWindow(currentWindow));
+ }
+ }
+
+ Promise.all(closeWinPromises).then(() => {
+ let currentTest = tests.shift();
+ info("prepping for " + currentTest.name);
+ waitForBrowserState(testState, currentTest);
+ });
+ }
+ else {
+ waitForBrowserState(stateBackup, finish);
+ }
+}
+
+/** ACTUAL TESTS **/
+
+function test_setTabState() {
+ let tab = gBrowser.tabs[1];
+ let newTabState = JSON.stringify({ entries: [{ url: "http://example.org" }], extData: { foo: "bar" } });
+ let busyEventCount = 0;
+ let readyEventCount = 0;
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getTabValue(tab, "foo"), "bar");
+ ss.setTabValue(tab, "baz", "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getTabValue(tab, "baz"), "qux");
+ is(tab.linkedBrowser.currentURI.spec, "http://example.org/");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, false);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, false);
+ ss.setTabState(tab, newTabState);
+}
+
+
+function test_duplicateTab() {
+ let tab = gBrowser.tabs[1];
+ let busyEventCount = 0;
+ let readyEventCount = 0;
+ let newTab;
+
+ // We'll look to make sure this value is on the duplicated tab
+ ss.setTabValue(tab, "foo", "bar");
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ newTab = gBrowser.tabs[2];
+ readyEventCount++;
+ is(ss.getTabValue(newTab, "foo"), "bar");
+ ss.setTabValue(newTab, "baz", "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getTabValue(newTab, "baz"), "qux");
+ is(newTab.linkedBrowser.currentURI.spec, "about:rights");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, false);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, false);
+
+ newTab = ss.duplicateTab(window, tab);
+}
+
+
+function test_undoCloseTab() {
+ let tab = gBrowser.tabs[1],
+ busyEventCount = 0,
+ readyEventCount = 0,
+ reopenedTab;
+
+ ss.setTabValue(tab, "foo", "bar");
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ reopenedTab = gBrowser.tabs[1];
+ readyEventCount++;
+ is(ss.getTabValue(reopenedTab, "foo"), "bar");
+ ss.setTabValue(reopenedTab, "baz", "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getTabValue(reopenedTab, "baz"), "qux");
+ is(reopenedTab.linkedBrowser.currentURI.spec, "about:rights");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, false);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, false);
+
+ gBrowser.removeTab(tab);
+ reopenedTab = ss.undoCloseTab(window, 0);
+}
+
+
+function test_setWindowState() {
+ let testState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }], extData: { "foo": "bar" } },
+ { entries: [{ url: "http://example.org" }], extData: { "baz": "qux" } }
+ ]
+ }]
+ };
+
+ let busyEventCount = 0,
+ readyEventCount = 0,
+ tabRestoredCount = 0;
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getTabValue(gBrowser.tabs[0], "foo"), "bar");
+ is(ss.getTabValue(gBrowser.tabs[1], "baz"), "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++tabRestoredCount < 2)
+ return;
+
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(gBrowser.tabs[0].linkedBrowser.currentURI.spec, "about:mozilla");
+ is(gBrowser.tabs[1].linkedBrowser.currentURI.spec, "http://example.org/");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, false);
+
+ runNextTest();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, false);
+
+ ss.setWindowState(window, JSON.stringify(testState), true);
+}
+
+
+function test_setBrowserState() {
+ // We'll track events per window so we are sure that they are each happening once
+ // pre window.
+ let windowEvents = {};
+ windowEvents[getOuterWindowID(window)] = { busyEventCount: 0, readyEventCount: 0 };
+
+ // waitForBrowserState does it's own observing for windows, but doesn't attach
+ // the listeners we want here, so do it ourselves.
+ let newWindow;
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ newWindow.addEventListener("load", function() {
+ newWindow.removeEventListener("load", arguments.callee, false);
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ windowEvents[getOuterWindowID(newWindow)] = { busyEventCount: 0, readyEventCount: 0 };
+
+ newWindow.addEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ newWindow.addEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ }, false);
+ }
+ }
+
+ function onSSWindowStateBusy(aEvent) {
+ windowEvents[getOuterWindowID(aEvent.originalTarget)].busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ windowEvents[getOuterWindowID(aEvent.originalTarget)].readyEventCount++;
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ Services.ww.registerNotification(windowObserver);
+
+ waitForBrowserState(lameMultiWindowState, function() {
+ let checkedWindows = 0;
+ for (let id of Object.keys(windowEvents)) {
+ let winEvents = windowEvents[id];
+ is(winEvents.busyEventCount, 1,
+ "[test_setBrowserState] window" + id + " busy event count correct");
+ is(winEvents.readyEventCount, 1,
+ "[test_setBrowserState] window" + id + " ready event count correct");
+ checkedWindows++;
+ }
+ is(checkedWindows, 2,
+ "[test_setBrowserState] checked 2 windows");
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ newWindow.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ newWindow.removeEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ runNextTest();
+ });
+}
+
+
+function test_undoCloseWindow() {
+ let newWindow, reopenedWindow;
+
+ function firstWindowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ Services.ww.unregisterNotification(firstWindowObserver);
+ }
+ }
+ Services.ww.registerNotification(firstWindowObserver);
+
+ waitForBrowserState(lameMultiWindowState, function() {
+ // Close the window which isn't window
+ BrowserTestUtils.closeWindow(newWindow).then(() => {
+ // Now give it time to close
+ reopenedWindow = ss.undoCloseWindow(0);
+ reopenedWindow.addEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ reopenedWindow.addEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+
+ reopenedWindow.addEventListener("load", function() {
+ reopenedWindow.removeEventListener("load", arguments.callee, false);
+
+ reopenedWindow.gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, false);
+ }, false);
+ });
+ });
+
+ let busyEventCount = 0,
+ readyEventCount = 0,
+ tabRestoredCount = 0;
+ // These will listen to the reopened closed window...
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++tabRestoredCount < 4)
+ return;
+
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+
+ reopenedWindow.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy, false);
+ reopenedWindow.removeEventListener("SSWindowStateReady", onSSWindowStateReady, false);
+ reopenedWindow.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, false);
+
+ BrowserTestUtils.closeWindow(reopenedWindow).then(runNextTest);
+ }
+}
diff --git a/browser/components/sessionstore/test/browser_618151.js b/browser/components/sessionstore/test/browser_618151.js
new file mode 100644
index 000000000..bdc268e6c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_618151.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const stateBackup = ss.getBrowserState();
+const testState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:blank" }] },
+ { entries: [{ url: "about:mozilla" }] }
+ ]
+ }]
+};
+
+
+function test() {
+ /** Test for Bug 618151 - Overwriting state can lead to unrestored tabs **/
+ waitForExplicitFinish();
+ runNextTest();
+}
+
+// Just a subset of tests from bug 615394 that causes a timeout.
+var tests = [test_setup, test_hang];
+function runNextTest() {
+ // set an empty state & run the next test, or finish
+ if (tests.length) {
+ // Enumerate windows and close everything but our primary window. We can't
+ // use waitForFocus() because apparently it's buggy. See bug 599253.
+ var windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ let closeWinPromises = [];
+ while (windowsEnum.hasMoreElements()) {
+ var currentWindow = windowsEnum.getNext();
+ if (currentWindow != window) {
+ closeWinPromises.push(BrowserTestUtils.closeWindow(currentWindow));
+ }
+ }
+
+ Promise.all(closeWinPromises).then(() => {
+ let currentTest = tests.shift();
+ info("running " + currentTest.name);
+ waitForBrowserState(testState, currentTest);
+ });
+ }
+ else {
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ }
+}
+
+function test_setup() {
+ function onSSTabRestored(aEvent) {
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, false);
+ runNextTest();
+ }
+
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, false);
+ ss.setTabState(gBrowser.tabs[1], JSON.stringify({
+ entries: [{ url: "http://example.org" }],
+ extData: { foo: "bar" } }));
+}
+
+function test_hang() {
+ ok(true, "test didn't time out");
+ runNextTest();
+}
diff --git a/browser/components/sessionstore/test/browser_623779.js b/browser/components/sessionstore/test/browser_623779.js
new file mode 100644
index 000000000..267bccb2d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_623779.js
@@ -0,0 +1,13 @@
+"use strict";
+
+add_task(function* () {
+ gBrowser.pinTab(gBrowser.selectedTab);
+
+ let newTab = gBrowser.duplicateTab(gBrowser.selectedTab);
+ yield promiseTabRestored(newTab);
+
+ ok(!newTab.pinned, "duplicating a pinned tab creates unpinned tab");
+ yield promiseRemoveTab(newTab);
+
+ gBrowser.unpinTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/sessionstore/test/browser_624727.js b/browser/components/sessionstore/test/browser_624727.js
new file mode 100644
index 000000000..85d6ff042
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_624727.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var TEST_STATE = { windows: [{ tabs: [{ url: "about:blank" }] }] };
+
+add_task(function* () {
+ function assertNumberOfTabs(num, msg) {
+ is(gBrowser.tabs.length, num, msg);
+ }
+
+ function assertNumberOfPinnedTabs(num, msg) {
+ is(gBrowser._numPinnedTabs, num, msg);
+ }
+
+ // check prerequisites
+ assertNumberOfTabs(1, "we start off with one tab");
+ assertNumberOfPinnedTabs(0, "no pinned tabs so far");
+
+ // setup
+ gBrowser.addTab("about:blank");
+ assertNumberOfTabs(2, "there are two tabs, now");
+
+ let [tab1, tab2] = gBrowser.tabs;
+ let linkedBrowser = tab1.linkedBrowser;
+ gBrowser.pinTab(tab1);
+ gBrowser.pinTab(tab2);
+ assertNumberOfPinnedTabs(2, "both tabs are now pinned");
+
+ // run the test
+ yield promiseBrowserState(TEST_STATE);
+
+ assertNumberOfTabs(1, "one tab left after setBrowserState()");
+ assertNumberOfPinnedTabs(0, "there are no pinned tabs");
+ is(gBrowser.tabs[0].linkedBrowser, linkedBrowser, "first tab's browser got re-used");
+});
diff --git a/browser/components/sessionstore/test/browser_625016.js b/browser/components/sessionstore/test/browser_625016.js
new file mode 100644
index 000000000..b551fcbb3
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_625016.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ /** Test for Bug 625016 - Restore windows closed in succession to quit (non-OSX only) **/
+
+ // We'll test this by opening a new window, waiting for the save
+ // event, then closing that window. We'll observe the
+ // "sessionstore-state-write-complete" notification and check that
+ // the state contains no _closedWindows. We'll then add a new tab
+ // and make sure that the state following that was reset and the
+ // closed window is now in _closedWindows.
+
+ requestLongerTimeout(2);
+
+ yield forceSaveState();
+
+ // We'll clear all closed windows to make sure our state is clean
+ // forgetClosedWindow doesn't trigger a delayed save
+ forgetClosedWindows();
+ is(ss.getClosedWindowCount(), 0, "starting with no closed windows");
+});
+
+add_task(function* new_window() {
+ let newWin;
+ try {
+ newWin = yield promiseNewWindowLoaded();
+ let tab = newWin.gBrowser.addTab("http://example.com/browser_625016.js?" + Math.random());
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ // Double check that we have no closed windows
+ is(ss.getClosedWindowCount(), 0, "no closed windows on first save");
+
+ yield BrowserTestUtils.closeWindow(newWin);
+ newWin = null;
+
+ let state = JSON.parse((yield promiseRecoveryFileContents()));
+ is(state.windows.length, 2,
+ "observe1: 2 windows in data written to disk");
+ is(state._closedWindows.length, 0,
+ "observe1: no closed windows in data written to disk");
+
+ // The API still treats the closed window as closed, so ensure that window is there
+ is(ss.getClosedWindowCount(), 1,
+ "observe1: 1 closed window according to API");
+ } finally {
+ if (newWin) {
+ yield BrowserTestUtils.closeWindow(newWin);
+ }
+ yield forceSaveState();
+ }
+});
+
+// We'll open a tab, which should trigger another state save which would wipe
+// the _shouldRestore attribute from the closed window
+add_task(function* new_tab() {
+ let newTab;
+ try {
+ newTab = gBrowser.addTab("about:mozilla");
+
+ let state = JSON.parse((yield promiseRecoveryFileContents()));
+ is(state.windows.length, 1,
+ "observe2: 1 window in data being written to disk");
+ is(state._closedWindows.length, 1,
+ "observe2: 1 closed window in data being written to disk");
+
+ // The API still treats the closed window as closed, so ensure that window is there
+ is(ss.getClosedWindowCount(), 1,
+ "observe2: 1 closed window according to API");
+ } finally {
+ gBrowser.removeTab(newTab);
+ }
+});
+
+
+add_task(function* done() {
+ // The API still represents the closed window as closed, so we can clear it
+ // with the API, but just to make sure...
+// is(ss.getClosedWindowCount(), 1, "1 closed window according to API");
+ forgetClosedWindows();
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+});
diff --git a/browser/components/sessionstore/test/browser_628270.js b/browser/components/sessionstore/test/browser_628270.js
new file mode 100644
index 000000000..f552cbfda
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_628270.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let assertNumberOfTabs = function (num, msg) {
+ is(gBrowser.tabs.length, num, msg);
+ }
+
+ let assertNumberOfVisibleTabs = function (num, msg) {
+ is(gBrowser.visibleTabs.length, num, msg);
+ }
+
+ let assertNumberOfPinnedTabs = function (num, msg) {
+ is(gBrowser._numPinnedTabs, num, msg);
+ }
+
+ waitForExplicitFinish();
+
+ // check prerequisites
+ assertNumberOfTabs(1, "we start off with one tab");
+
+ // setup
+ let tab = gBrowser.addTab("about:mozilla");
+
+ whenTabIsLoaded(tab, function () {
+ // hide the newly created tab
+ assertNumberOfVisibleTabs(2, "there are two visible tabs");
+ gBrowser.showOnlyTheseTabs([gBrowser.tabs[0]]);
+ assertNumberOfVisibleTabs(1, "there is one visible tab");
+ ok(tab.hidden, "newly created tab is now hidden");
+
+ // close and restore hidden tab
+ promiseRemoveTab(tab).then(() => {
+ tab = ss.undoCloseTab(window, 0);
+
+ // check that everything was restored correctly, clean up and finish
+ whenTabIsLoaded(tab, function () {
+ is(tab.linkedBrowser.currentURI.spec, "about:mozilla", "restored tab has correct url");
+
+ gBrowser.removeTab(tab);
+ finish();
+ });
+ });
+ });
+}
+
+function whenTabIsLoaded(tab, callback) {
+ tab.linkedBrowser.addEventListener("load", function onLoad() {
+ tab.linkedBrowser.removeEventListener("load", onLoad, true);
+ callback();
+ }, true);
+}
diff --git a/browser/components/sessionstore/test/browser_635418.js b/browser/components/sessionstore/test/browser_635418.js
new file mode 100644
index 000000000..3b21c5b0f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_635418.js
@@ -0,0 +1,55 @@
+/* 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 tests that hiding/showing a tab, on its own, eventually triggers a
+// session store.
+
+function test() {
+ waitForExplicitFinish();
+
+ // We speed up the interval between session saves to ensure that the test
+ // runs quickly.
+ Services.prefs.setIntPref("browser.sessionstore.interval", 2000);
+
+ // Loading a tab causes a save state and this is meant to catch that event.
+ waitForSaveState(testBug635418_1);
+
+ // Assumption: Only one window is open and it has one tab open.
+ gBrowser.addTab("about:mozilla");
+}
+
+function testBug635418_1() {
+ ok(!gBrowser.tabs[0].hidden, "first tab should not be hidden");
+ ok(!gBrowser.tabs[1].hidden, "second tab should not be hidden");
+
+ waitForSaveState(testBug635418_2);
+
+ // We can't hide the selected tab, so hide the new one
+ gBrowser.hideTab(gBrowser.tabs[1]);
+}
+
+function testBug635418_2() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(!state.windows[0].tabs[0].hidden, "first tab should still not be hidden");
+ ok(state.windows[0].tabs[1].hidden, "second tab should be hidden by now");
+
+ waitForSaveState(testBug635418_3);
+ gBrowser.showTab(gBrowser.tabs[1]);
+}
+
+function testBug635418_3() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(!state.windows[0].tabs[0].hidden, "first tab should still still not be hidden");
+ ok(!state.windows[0].tabs[1].hidden, "second tab should not be hidden again");
+
+ done();
+}
+
+function done() {
+ gBrowser.removeTab(window.gBrowser.tabs[1]);
+
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+
+ executeSoon(finish);
+}
diff --git a/browser/components/sessionstore/test/browser_636279.js b/browser/components/sessionstore/test/browser_636279.js
new file mode 100644
index 000000000..250995606
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_636279.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var stateBackup = ss.getBrowserState();
+
+var statePinned = {windows:[{tabs:[
+ {entries:[{url:"http://example.com#1"}], pinned: true}
+]}]};
+
+var state = {windows:[{tabs:[
+ {entries:[{url:"http://example.com#1"}]},
+ {entries:[{url:"http://example.com#2"}]},
+ {entries:[{url:"http://example.com#3"}]},
+ {entries:[{url:"http://example.com#4"}]},
+]}]};
+
+function test() {
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ TabsProgressListener.uninit();
+ ss.setBrowserState(stateBackup);
+ });
+
+
+ TabsProgressListener.init();
+
+ window.addEventListener("SSWindowStateReady", function onReady() {
+ window.removeEventListener("SSWindowStateReady", onReady, false);
+
+ let firstProgress = true;
+
+ TabsProgressListener.setCallback(function (needsRestore, isRestoring) {
+ if (firstProgress) {
+ firstProgress = false;
+ is(isRestoring, 3, "restoring 3 tabs concurrently");
+ } else {
+ ok(isRestoring <= 3, "restoring max. 2 tabs concurrently");
+ }
+
+ if (0 == needsRestore) {
+ TabsProgressListener.unsetCallback();
+ waitForFocus(finish);
+ }
+ });
+
+ ss.setBrowserState(JSON.stringify(state));
+ }, false);
+
+ ss.setBrowserState(JSON.stringify(statePinned));
+}
+
+function countTabs() {
+ let needsRestore = 0, isRestoring = 0;
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ if (window.closed)
+ continue;
+
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ let browser = window.gBrowser.tabs[i].linkedBrowser;
+ if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+ isRestoring++;
+ else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+ needsRestore++;
+ }
+ }
+
+ return [needsRestore, isRestoring];
+}
+
+var TabsProgressListener = {
+ init: function () {
+ Services.obs.addObserver(this, "sessionstore-debug-tab-restored", false);
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "sessionstore-debug-tab-restored");
+ this.unsetCallback();
+ },
+
+ setCallback: function (callback) {
+ this.callback = callback;
+ },
+
+ unsetCallback: function () {
+ delete this.callback;
+ },
+
+ observe: function (browser, topic, data) {
+ TabsProgressListener.onRestored(browser);
+ },
+
+ onRestored: function (browser) {
+ if (this.callback && browser.__SS_restoreState == TAB_STATE_RESTORING) {
+ this.callback.apply(null, countTabs());
+ }
+ }
+}
diff --git a/browser/components/sessionstore/test/browser_637020.js b/browser/components/sessionstore/test/browser_637020.js
new file mode 100644
index 000000000..1c1f357d7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_637020.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_637020_slow.sjs";
+
+const TEST_STATE = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:mozilla" }] },
+ { entries: [{ url: "about:robots" }] }
+ ]
+ }, {
+ tabs: [
+ { entries: [{ url: TEST_URL }] },
+ { entries: [{ url: TEST_URL }] }
+ ]
+ }]
+};
+
+/**
+ * This test ensures that windows that have just been restored will be marked
+ * as dirty, otherwise _getCurrentState() might ignore them when collecting
+ * state for the first time and we'd just save them as empty objects.
+ *
+ * The dirty state acts as a cache to not collect data from all windows all the
+ * time, so at the beginning, each window must be dirty so that we collect
+ * their state at least once.
+ */
+
+add_task(function* test() {
+ // Wait until the new window has been opened.
+ let promiseWindow = new Promise(resolve => {
+ Services.obs.addObserver(function onOpened(subject) {
+ Services.obs.removeObserver(onOpened, "domwindowopened");
+ resolve(subject);
+ }, "domwindowopened", false);
+ });
+
+ // Set the new browser state that will
+ // restore a window with two slowly loading tabs.
+ let backupState = SessionStore.getBrowserState();
+ SessionStore.setBrowserState(JSON.stringify(TEST_STATE));
+ let win = yield promiseWindow;
+
+ // The window has now been opened. Check the state that is returned,
+ // this should come from the cache while the window isn't restored, yet.
+ info("the window has been opened");
+ checkWindows();
+
+ // The history has now been restored and the tabs are loading. The data must
+ // now come from the window, if it's correctly been marked as dirty before.
+ yield new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+ info("the delayed startup has finished");
+ checkWindows();
+
+ // Cleanup.
+ yield BrowserTestUtils.closeWindow(win);
+ yield promiseBrowserState(backupState);
+});
+
+function checkWindows() {
+ let state = JSON.parse(SessionStore.getBrowserState());
+ is(state.windows[0].tabs.length, 2, "first window has two tabs");
+ is(state.windows[1].tabs.length, 2, "second window has two tabs");
+}
diff --git a/browser/components/sessionstore/test/browser_637020_slow.sjs b/browser/components/sessionstore/test/browser_637020_slow.sjs
new file mode 100644
index 000000000..41da3c2ad
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_637020_slow.sjs
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const DELAY_MS = "2000";
+
+let timer;
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+ resp.setHeader("Cache-Control", "no-cache", false);
+ resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(() => {
+ resp.write("hi");
+ resp.finish();
+ }, DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+}
diff --git a/browser/components/sessionstore/test/browser_644409-scratchpads.js b/browser/components/sessionstore/test/browser_644409-scratchpads.js
new file mode 100644
index 000000000..56826801a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_644409-scratchpads.js
@@ -0,0 +1,68 @@
+ /* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const testState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:blank" }] },
+ ]
+ }],
+ scratchpads: [
+ { text: "text1", executionContext: 1 },
+ { text: "", executionContext: 2, filename: "test.js" }
+ ]
+};
+
+// only finish() when correct number of windows opened
+var restored = [];
+function addState(state) {
+ restored.push(state);
+
+ if (restored.length == testState.scratchpads.length) {
+ ok(statesMatch(restored, testState.scratchpads),
+ "Two scratchpad windows restored");
+
+ Services.ww.unregisterNotification(windowObserver);
+ finish();
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.ww.registerNotification(windowObserver);
+
+ ss.setBrowserState(JSON.stringify(testState));
+}
+
+function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ if (win.Scratchpad) {
+ win.Scratchpad.addObserver({
+ onReady: function() {
+ win.Scratchpad.removeObserver(this);
+
+ let state = win.Scratchpad.getState();
+ BrowserTestUtils.closeWindow(win).then(() => {
+ addState(state);
+ });
+ },
+ });
+ }
+ }, false);
+ }
+}
+
+function statesMatch(restored, states) {
+ return states.every(function(state) {
+ return restored.some(function(restoredState) {
+ return state.filename == restoredState.filename &&
+ state.text == restoredState.text &&
+ state.executionContext == restoredState.executionContext;
+ })
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_645428.js b/browser/components/sessionstore/test/browser_645428.js
new file mode 100644
index 000000000..124a7aea9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_645428.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NOTIFICATION = "sessionstore-browser-state-restored";
+
+function test() {
+ waitForExplicitFinish();
+
+ function observe(subject, topic, data) {
+ if (NOTIFICATION == topic) {
+ finish();
+ ok(true, "TOPIC received");
+ }
+ }
+
+ Services.obs.addObserver(observe, NOTIFICATION, false);
+ registerCleanupFunction(function () {
+ Services.obs.removeObserver(observe, NOTIFICATION);
+ });
+
+ ss.setBrowserState(JSON.stringify({ windows: [] }));
+}
diff --git a/browser/components/sessionstore/test/browser_659591.js b/browser/components/sessionstore/test/browser_659591.js
new file mode 100644
index 000000000..60b1dcd2e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_659591.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ let eventReceived = false;
+
+ registerCleanupFunction(function () {
+ ok(eventReceived, "SSWindowClosing event received");
+ });
+
+ newWindow(function (win) {
+ win.addEventListener("SSWindowClosing", function onWindowClosing() {
+ win.removeEventListener("SSWindowClosing", onWindowClosing, false);
+ eventReceived = true;
+ }, false);
+
+ BrowserTestUtils.closeWindow(win).then(() => {
+ waitForFocus(finish);
+ });
+ });
+}
+
+function newWindow(callback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(getBrowserURL(), "_blank", opts);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ executeSoon(() => callback(win));
+ }, false);
+}
diff --git a/browser/components/sessionstore/test/browser_662743.js b/browser/components/sessionstore/test/browser_662743.js
new file mode 100644
index 000000000..212180213
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_662743.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This tests that session restore component does restore the right <select> option.
+// Session store should not rely only on previous user's selectedIndex, it should
+// check its value as well.
+
+function test() {
+ /** Tests selected options **/
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ let testTabCount = 0;
+ let formData = [
+ // default case
+ { },
+
+ // new format
+ // index doesn't match value (testing an option in between (two))
+ { id:{ "select_id": {"selectedIndex":0,"value":"val2"} } },
+ // index doesn't match value (testing an invalid value)
+ { id:{ "select_id": {"selectedIndex":4,"value":"val8"} } },
+ // index doesn't match value (testing an invalid index)
+ { id:{ "select_id": {"selectedIndex":8,"value":"val5"} } },
+ // index and value match position zero
+ { id:{ "select_id": {"selectedIndex":0,"value":"val0"} }, xpath: {} },
+ // index doesn't match value (testing the last option (seven))
+ { id:{},"xpath":{ "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']": {"selectedIndex":1,"value":"val7"} } },
+ // index and value match the default option "selectedIndex":3,"value":"val3"
+ { xpath: { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : {"selectedIndex":3,"value":"val3"} } },
+ // index matches default option however it doesn't match value
+ { id:{ "select_id": {"selectedIndex":3,"value":"val4"} } },
+ ];
+
+ let expectedValues = [
+ null, // default value
+ "val2",
+ null, // default value (invalid value)
+ "val5", // value is still valid (even it has an invalid index)
+ "val0",
+ "val7",
+ null,
+ "val4",
+ ];
+ let callback = function() {
+ testTabCount--;
+ if (testTabCount == 0) {
+ finish();
+ }
+ };
+
+ for (let i = 0; i < formData.length; i++) {
+ testTabCount++;
+ testTabRestoreData(formData[i], expectedValues[i], callback);
+ }
+}
+
+function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
+ let testURL =
+ getRootDirectory(gTestPath) + "browser_662743_sample.html";
+ let tab = gBrowser.addTab(testURL);
+
+ aFormData.url = testURL;
+ let tabState = { entries: [{ url: testURL, }], formdata: aFormData };
+
+ promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+ promiseTabState(tab, tabState).then(() => {
+ // Flush to make sure we have the latest form data.
+ return TabStateFlusher.flush(tab.linkedBrowser);
+ }).then(() => {
+ let doc = tab.linkedBrowser.contentDocument;
+ let select = doc.getElementById("select_id");
+ let value = select.options[select.selectedIndex].value;
+ let restoredTabState = JSON.parse(ss.getTabState(tab));
+
+ // If aExpectedValue=null we don't expect any form data to be collected.
+ if (!aExpectedValue) {
+ ok(!restoredTabState.hasOwnProperty("formdata"), "no formdata collected");
+ gBrowser.removeTab(tab);
+ aCallback();
+ return;
+ }
+
+ // test select options values
+ is(value, aExpectedValue,
+ "Select Option by selectedIndex &/or value has been restored correctly");
+
+ let restoredFormData = restoredTabState.formdata;
+ let selectIdFormData = restoredFormData.id.select_id;
+ value = restoredFormData.id.select_id.value;
+
+ // test format
+ ok("id" in restoredFormData || "xpath" in restoredFormData,
+ "FormData format is valid");
+ // test format
+ ok("selectedIndex" in selectIdFormData && "value" in selectIdFormData,
+ "select format is valid");
+ // test set collection values
+ is(value, aExpectedValue,
+ "Collection has been saved correctly");
+
+ // clean up
+ gBrowser.removeTab(tab);
+
+ aCallback();
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_662743_sample.html b/browser/components/sessionstore/test/browser_662743_sample.html
new file mode 100644
index 000000000..de48fa0c9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_662743_sample.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Test 662743</title>
+
+<!-- Select events -->
+<h3>Select options</h3>
+<select id="select_id" name="select_name">
+ <option value="val0">Zero</option>
+ <option value="val1">One</option>
+ <option value="val2">Two</option>
+ <option value="val3" selected="selected">Three</option>
+ <option value="val4">Four</option>
+ <option value="val5">Five</option>
+ <option value="val6">Six</option>
+ <option value="val7">Seven</option>
+</select> \ No newline at end of file
diff --git a/browser/components/sessionstore/test/browser_662812.js b/browser/components/sessionstore/test/browser_662812.js
new file mode 100644
index 000000000..1bbaf67dc
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_662812.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ window.addEventListener("SSWindowStateBusy", function onBusy() {
+ window.removeEventListener("SSWindowStateBusy", onBusy, false);
+
+ let state = JSON.parse(ss.getWindowState(window));
+ ok(state.windows[0].busy, "window is busy");
+
+ window.addEventListener("SSWindowStateReady", function onReady() {
+ window.removeEventListener("SSWindowStateReady", onReady, false);
+
+ let state = JSON.parse(ss.getWindowState(window));
+ ok(!state.windows[0].busy, "window is not busy");
+
+ executeSoon(() => {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ finish();
+ });
+ }, false);
+ }, false);
+
+ // create a new tab
+ let tab = gBrowser.addTab("about:mozilla");
+ let browser = tab.linkedBrowser;
+
+ // close and restore it
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ gBrowser.removeTab(tab);
+ ss.undoCloseTab(window, 0);
+ }, true);
+}
diff --git a/browser/components/sessionstore/test/browser_665702-state_session.js b/browser/components/sessionstore/test/browser_665702-state_session.js
new file mode 100644
index 000000000..524b4969f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_665702-state_session.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function compareArray(a, b) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function test() {
+ let currentState = JSON.parse(ss.getBrowserState());
+ ok(currentState.session, "session data returned by getBrowserState");
+
+ let keys = Object.keys(currentState.session);
+ let expectedKeys = ["lastUpdate", "startTime", "recentCrashes"];
+ ok(compareArray(keys.sort(), expectedKeys.sort()),
+ "session object from getBrowserState has correct keys");
+}
diff --git a/browser/components/sessionstore/test/browser_682507.js b/browser/components/sessionstore/test/browser_682507.js
new file mode 100644
index 000000000..52b95341b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_682507.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+ gBrowser.addTab("about:mozilla");
+
+ ss.setTabState(gBrowser.tabs[1], ss.getTabState(gBrowser.tabs[1]));
+ ok(gBrowser.tabs[1].hasAttribute("pending"), "second tab should have 'pending' attribute");
+
+ gBrowser.selectedTab = gBrowser.tabs[1];
+ ok(!gBrowser.tabs[1].hasAttribute("pending"), "second tab should have not 'pending' attribute");
+
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+}
diff --git a/browser/components/sessionstore/test/browser_687710.js b/browser/components/sessionstore/test/browser_687710.js
new file mode 100644
index 000000000..372ecf7ae
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_687710.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that sessionrestore handles cycles in the shentry graph properly.
+//
+// These cycles shouldn't be there in the first place, but they cause hangs
+// when they mysteriously appear (bug 687710). Docshell code assumes this
+// graph is a tree and tires to walk to the root. But if there's a cycle,
+// there is no root, and we loop forever.
+
+var stateBackup = ss.getBrowserState();
+
+var state = {windows:[{tabs:[{entries:[
+ {
+ docIdentifier: 1,
+ url: "http://example.com",
+ children: [
+ {
+ docIdentifier: 2,
+ url: "http://example.com"
+ }
+ ]
+ },
+ {
+ docIdentifier: 2,
+ url: "http://example.com",
+ children: [
+ {
+ docIdentifier: 1,
+ url: "http://example.com"
+ }
+ ]
+ }
+]}]}]}
+
+function test() {
+ registerCleanupFunction(function () {
+ ss.setBrowserState(stateBackup);
+ });
+
+ /* This test fails by hanging. */
+ ss.setBrowserState(JSON.stringify(state));
+ ok(true, "Didn't hang!");
+}
diff --git a/browser/components/sessionstore/test/browser_687710_2.js b/browser/components/sessionstore/test/browser_687710_2.js
new file mode 100644
index 000000000..c22e73750
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_687710_2.js
@@ -0,0 +1,64 @@
+/* eslint-env mozilla/frame-script */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the fix for bug 687710 isn't too aggressive -- shentries which are
+// cousins should be able to share bfcache entries.
+
+var stateBackup = ss.getBrowserState();
+
+var state = {entries:[
+ {
+ docIdentifier: 1,
+ url: "http://example.com?1",
+ children: [{ docIdentifier: 10,
+ url: "http://example.com?10" }]
+ },
+ {
+ docIdentifier: 1,
+ url: "http://example.com?1#a",
+ children: [{ docIdentifier: 10,
+ url: "http://example.com?10#aa" }]
+ }
+]};
+
+add_task(function* test() {
+ let tab = gBrowser.addTab("about:blank");
+ yield promiseTabState(tab, state);
+ yield ContentTask.spawn(tab.linkedBrowser, null, function() {
+ function compareEntries(i, j, history) {
+ let e1 = history.getEntryAtIndex(i, false)
+ .QueryInterface(Ci.nsISHEntry)
+ .QueryInterface(Ci.nsISHContainer);
+
+ let e2 = history.getEntryAtIndex(j, false)
+ .QueryInterface(Ci.nsISHEntry)
+ .QueryInterface(Ci.nsISHContainer);
+
+ ok(e1.sharesDocumentWith(e2),
+ `${i} should share doc with ${j}`);
+ is(e1.childCount, e2.childCount,
+ `Child count mismatch (${i}, ${j})`);
+
+ for (let c = 0; c < e1.childCount; c++) {
+ let c1 = e1.GetChildAt(c);
+ let c2 = e2.GetChildAt(c);
+
+ ok(c1.sharesDocumentWith(c2),
+ `Cousins should share documents. (${i}, ${j}, ${c})`);
+ }
+ }
+
+ let history = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+
+ is(history.count, 2, "history.count");
+ for (let i = 0; i < history.count; i++) {
+ for (let j = 0; j < history.count; j++) {
+ compareEntries(i, j, history);
+ }
+ }
+ });
+
+ ss.setBrowserState(stateBackup);
+});
diff --git a/browser/components/sessionstore/test/browser_694378.js b/browser/components/sessionstore/test/browser_694378.js
new file mode 100644
index 000000000..8578428d8
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_694378.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test Summary:
+// 1. call ss.setWindowState with a broken state
+// 1a. ensure that it doesn't throw.
+
+function test() {
+ waitForExplicitFinish();
+
+ let brokenState = {
+ windows: [
+ { tabs: [{ entries: [{ url: "about:mozilla" }] }] }
+ ],
+ selectedWindow: 2
+ };
+ let brokenStateString = JSON.stringify(brokenState);
+
+ let gotError = false;
+ try {
+ ss.setWindowState(window, brokenStateString, true);
+ }
+ catch (ex) {
+ gotError = true;
+ info(ex);
+ }
+
+ ok(!gotError, "ss.setWindowState did not throw an error");
+
+ // Make sure that we reset the state. Use a full state just in case things get crazy.
+ let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
+ waitForBrowserState(blankState, finish);
+}
diff --git a/browser/components/sessionstore/test/browser_701377.js b/browser/components/sessionstore/test/browser_701377.js
new file mode 100644
index 000000000..1bf2625ef
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_701377.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var state = {windows:[{tabs:[
+ {entries:[{url:"http://example.com#1"}]},
+ {entries:[{url:"http://example.com#2"}], hidden: true}
+]}]};
+
+function test() {
+ waitForExplicitFinish();
+
+ newWindowWithState(state, function (aWindow) {
+ let tab = aWindow.gBrowser.tabs[1];
+ ok(tab.hidden, "the second tab is hidden");
+
+ let tabShown = false;
+ let tabShowCallback = () => tabShown = true;
+ tab.addEventListener("TabShow", tabShowCallback, false);
+
+ let tabState = ss.getTabState(tab);
+ ss.setTabState(tab, tabState);
+
+ tab.removeEventListener("TabShow", tabShowCallback, false);
+ ok(tab.hidden && !tabShown, "tab remains hidden");
+
+ finish();
+ });
+}
+
+// ----------
+function newWindowWithState(aState, aCallback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(getBrowserURL(), "_blank", opts);
+
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ whenWindowLoaded(win, function onWindowLoaded(aWin) {
+ ss.setWindowState(aWin, JSON.stringify(aState), true);
+ executeSoon(() => aCallback(aWin));
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_705597.js b/browser/components/sessionstore/test/browser_705597.js
new file mode 100644
index 000000000..efadcfe88
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_705597.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var tabState = {
+ entries: [{url: "about:robots", children: [{url: "about:mozilla"}]}]
+};
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ Services.prefs.setIntPref("browser.sessionstore.interval", 4000);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ });
+
+ let tab = gBrowser.addTab("about:blank");
+
+ let browser = tab.linkedBrowser;
+
+ promiseTabState(tab, tabState).then(() => {
+ let sessionHistory = browser.sessionHistory;
+ let entry = sessionHistory.getEntryAtIndex(0, false);
+ entry.QueryInterface(Ci.nsISHContainer);
+
+ whenChildCount(entry, 1, function () {
+ whenChildCount(entry, 2, function () {
+ promiseBrowserLoaded(browser).then(() => {
+ return TabStateFlusher.flush(browser);
+ }).then(() => {
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "tab has one history entry");
+ ok(!entries[0].children, "history entry has no subframes");
+
+ // Make sure that we reset the state.
+ let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
+ waitForBrowserState(blankState, finish);
+ });
+
+ // reload the browser to deprecate the subframes
+ browser.reload();
+ });
+
+ // create a dynamic subframe
+ let doc = browser.contentDocument;
+ let iframe = doc.createElement("iframe");
+ doc.body.appendChild(iframe);
+ iframe.setAttribute("src", "about:mozilla");
+ });
+ });
+}
+
+function whenChildCount(aEntry, aChildCount, aCallback) {
+ if (aEntry.childCount == aChildCount)
+ aCallback();
+ else
+ setTimeout(() => whenChildCount(aEntry, aChildCount, aCallback), 100);
+}
diff --git a/browser/components/sessionstore/test/browser_707862.js b/browser/components/sessionstore/test/browser_707862.js
new file mode 100644
index 000000000..e12c44af4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_707862.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var tabState = {
+ entries: [{url: "about:robots", children: [{url: "about:mozilla"}]}]
+};
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ Services.prefs.setIntPref("browser.sessionstore.interval", 4000);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ });
+
+ let tab = gBrowser.addTab("about:blank");
+
+ let browser = tab.linkedBrowser;
+
+ promiseTabState(tab, tabState).then(() => {
+ let sessionHistory = browser.sessionHistory;
+ let entry = sessionHistory.getEntryAtIndex(0, false);
+ entry.QueryInterface(Ci.nsISHContainer);
+
+ whenChildCount(entry, 1, function () {
+ whenChildCount(entry, 2, function () {
+ promiseBrowserLoaded(browser).then(() => {
+ let sessionHistory = browser.sessionHistory;
+ let entry = sessionHistory.getEntryAtIndex(0, false);
+
+ whenChildCount(entry, 0, function () {
+ // Make sure that we reset the state.
+ let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
+ waitForBrowserState(blankState, finish);
+ });
+ });
+
+ // reload the browser to deprecate the subframes
+ browser.reload();
+ });
+
+ // create a dynamic subframe
+ let doc = browser.contentDocument;
+ let iframe = doc.createElement("iframe");
+ doc.body.appendChild(iframe);
+ iframe.setAttribute("src", "about:mozilla");
+ });
+ });
+
+ // This test relies on the test timing out in order to indicate failure so
+ // let's add a dummy pass.
+ ok(true, "Each test requires at least one pass, fail or todo so here is a pass.");
+}
+
+function whenChildCount(aEntry, aChildCount, aCallback) {
+ if (aEntry.childCount == aChildCount)
+ aCallback();
+ else
+ setTimeout(() => whenChildCount(aEntry, aChildCount, aCallback), 100);
+}
diff --git a/browser/components/sessionstore/test/browser_739531.js b/browser/components/sessionstore/test/browser_739531.js
new file mode 100644
index 000000000..e5927caf6
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_739531.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that attempts made to save/restore ("duplicate") pages
+// using designmode AND make changes to document structure (remove body)
+// don't result in uncaught errors and a broken browser state.
+
+function test() {
+ waitForExplicitFinish();
+
+ let testURL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_739531_sample.html";
+
+ let loadCount = 0;
+ let tab = gBrowser.addTab(testURL);
+ tab.linkedBrowser.addEventListener("load", function onLoad(aEvent) {
+ // make sure both the page and the frame are loaded
+ if (++loadCount < 2)
+ return;
+ tab.linkedBrowser.removeEventListener("load", onLoad, true);
+
+ // executeSoon to allow the JS to execute on the page
+ executeSoon(function() {
+
+ let tab2;
+ let caughtError = false;
+ try {
+ tab2 = ss.duplicateTab(window, tab);
+ }
+ catch (e) {
+ caughtError = true;
+ info(e);
+ }
+
+ is(gBrowser.tabs.length, 3, "there should be 3 tabs")
+
+ ok(!caughtError, "duplicateTab didn't throw");
+
+ // if the test fails, we don't want to try to close a tab that doesn't exist
+ if (tab2)
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ });
+ }, true);
+}
diff --git a/browser/components/sessionstore/test/browser_739531_sample.html b/browser/components/sessionstore/test/browser_739531_sample.html
new file mode 100644
index 000000000..ad317ab0c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_739531_sample.html
@@ -0,0 +1,25 @@
+<!-- originally a crash test for bug 713417
+ https://bug713417.bugzilla.mozilla.org/attachment.cgi?id=584240 -->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+
+function boom()
+{
+ var w = document.getElementById("f").contentWindow;
+ var d = w.document;
+ d.designMode = 'on';
+ var r = d.documentElement;
+ d.removeChild(r);
+ document.adoptNode(r);
+}
+
+</script>
+</head>
+<body onload="boom();">
+<iframe src="data:text/html;charset=utf-8,1" id="f"></iframe>
+</body>
+</html>
+
diff --git a/browser/components/sessionstore/test/browser_739805.js b/browser/components/sessionstore/test/browser_739805.js
new file mode 100644
index 000000000..f00871661
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_739805.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var url = "data:text/html;charset=utf-8,<input%20id='foo'>";
+var tabState = {
+ entries: [{ url }], formdata: { id: { "foo": "bar" }, url }
+};
+
+function test() {
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ registerCleanupFunction(function () {
+ if (gBrowser.tabs.length > 1)
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+ });
+
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+
+ promiseBrowserLoaded(browser).then(() => {
+ isnot(gBrowser.selectedTab, tab, "newly created tab is not selected");
+
+ ss.setTabState(tab, JSON.stringify(tabState));
+ is(browser.__SS_restoreState, TAB_STATE_NEEDS_RESTORE, "tab needs restoring");
+
+ let {formdata} = JSON.parse(ss.getTabState(tab));
+ is(formdata && formdata.id["foo"], "bar", "tab state's formdata is valid");
+
+ promiseTabRestored(tab).then(() => {
+ ContentTask.spawn(browser, null, function() {
+ let input = content.document.getElementById("foo");
+ is(input.value, "bar", "formdata has been restored correctly");
+ }).then(() => { finish(); });
+ });
+
+ // Restore the tab by selecting it.
+ gBrowser.selectedTab = tab;
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_819510_perwindowpb.js b/browser/components/sessionstore/test/browser_819510_perwindowpb.js
new file mode 100644
index 000000000..21f916f0d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_819510_perwindowpb.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test opening default mochitest-normal-private-normal-private windows
+// (saving the state with last window being private)
+
+requestLongerTimeout(2);
+
+add_task(function* test_1() {
+ let win = yield promiseNewWindowLoaded();
+ win.gBrowser.addTab("http://www.example.com/1");
+
+ win = yield promiseNewWindowLoaded({private: true});
+ win.gBrowser.addTab("http://www.example.com/2");
+
+ win = yield promiseNewWindowLoaded();
+ win.gBrowser.addTab("http://www.example.com/3");
+
+ win = yield promiseNewWindowLoaded({private: true});
+ win.gBrowser.addTab("http://www.example.com/4");
+
+ let curState = JSON.parse(ss.getBrowserState());
+ is(curState.windows.length, 5, "Browser has opened 5 windows");
+ is(curState.windows[2].isPrivate, true, "Window is private");
+ is(curState.windows[4].isPrivate, true, "Last window is private");
+ is(curState.selectedWindow, 5, "Last window opened is the one selected");
+
+ let state = JSON.parse(yield promiseRecoveryFileContents());
+
+ is(state.windows.length, 3,
+ "sessionstore state: 3 windows in data being written to disk");
+ is(state.selectedWindow, 3,
+ "Selected window is updated to match one of the saved windows");
+ ok(state.windows.every(win => !win.isPrivate),
+ "Saved windows are not private");
+ is(state._closedWindows.length, 0,
+ "sessionstore state: no closed windows in data being written to disk");
+
+ // Cleanup.
+ yield promiseAllButPrimaryWindowClosed();
+ forgetClosedWindows();
+});
+
+// Test opening default mochitest window + 2 private windows
+add_task(function* test_2() {
+ let win = yield promiseNewWindowLoaded({private: true});
+ win.gBrowser.addTab("http://www.example.com/1");
+
+ win = yield promiseNewWindowLoaded({private: true});
+ win.gBrowser.addTab("http://www.example.com/2");
+
+ let curState = JSON.parse(ss.getBrowserState());
+ is(curState.windows.length, 3, "Browser has opened 3 windows");
+ is(curState.windows[1].isPrivate, true, "Window 1 is private");
+ is(curState.windows[2].isPrivate, true, "Window 2 is private");
+ is(curState.selectedWindow, 3, "Last window opened is the one selected");
+
+ let state = JSON.parse(yield promiseRecoveryFileContents());
+
+ is(state.windows.length, 1,
+ "sessionstore state: 1 windows in data being written to disk");
+ is(state.selectedWindow, 1,
+ "Selected window is updated to match one of the saved windows");
+ is(state._closedWindows.length, 0,
+ "sessionstore state: no closed windows in data being written to disk");
+
+ // Cleanup.
+ yield promiseAllButPrimaryWindowClosed();
+ forgetClosedWindows();
+});
+
+// Test opening default-normal-private-normal windows and closing a normal window
+add_task(function* test_3() {
+ let normalWindow = yield promiseNewWindowLoaded();
+ yield promiseTabLoad(normalWindow, "http://www.example.com/");
+
+ let win = yield promiseNewWindowLoaded({private: true});
+ yield promiseTabLoad(win, "http://www.example.com/");
+
+ win = yield promiseNewWindowLoaded();
+ yield promiseTabLoad(win, "http://www.example.com/");
+
+ let curState = JSON.parse(ss.getBrowserState());
+ is(curState.windows.length, 4, "Browser has opened 4 windows");
+ is(curState.windows[2].isPrivate, true, "Window 2 is private");
+ is(curState.selectedWindow, 4, "Last window opened is the one selected");
+
+ yield BrowserTestUtils.closeWindow(normalWindow);
+
+ // Pin and unpin a tab before checking the written state so that
+ // the list of restoring windows gets cleared. Otherwise the
+ // window we just closed would be marked as not closed.
+ let tab = win.gBrowser.tabs[0];
+ win.gBrowser.pinTab(tab);
+ win.gBrowser.unpinTab(tab);
+
+ let state = JSON.parse(yield promiseRecoveryFileContents());
+
+ is(state.windows.length, 2,
+ "sessionstore state: 2 windows in data being written to disk");
+ is(state.selectedWindow, 2,
+ "Selected window is updated to match one of the saved windows");
+ ok(state.windows.every(win => !win.isPrivate),
+ "Saved windows are not private");
+ is(state._closedWindows.length, 1,
+ "sessionstore state: 1 closed window in data being written to disk");
+ ok(state._closedWindows.every(win => !win.isPrivate),
+ "Closed windows are not private");
+
+ // Cleanup.
+ yield promiseAllButPrimaryWindowClosed();
+ forgetClosedWindows();
+});
+
+function* promiseTabLoad(win, url) {
+ let browser = win.gBrowser.selectedBrowser;
+ browser.loadURI(url);
+ yield promiseBrowserLoaded(browser);
+ yield TabStateFlusher.flush(browser);
+}
diff --git a/browser/components/sessionstore/test/browser_911547.js b/browser/components/sessionstore/test/browser_911547.js
new file mode 100644
index 000000000..58b2e9ef1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_911547.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests that session restore component does restore the right content
+// security policy with the document.
+// The policy being tested disallows inline scripts
+
+add_task(function* test() {
+ // create a tab that has a CSP
+ let testURL = "http://mochi.test:8888/browser/browser/components/sessionstore/test/browser_911547_sample.html";
+ let tab = gBrowser.selectedTab = gBrowser.addTab(testURL);
+ gBrowser.selectedTab = tab;
+
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // this is a baseline to ensure CSP is active
+ // attempt to inject and run a script via inline (pre-restore, allowed)
+ yield injectInlineScript(browser, `document.getElementById("test_id").value = "fail";`);
+
+ let loadedPromise = promiseBrowserLoaded(browser);
+ yield ContentTask.spawn(browser, null, function() {
+ is(content.document.getElementById("test_id").value, "ok",
+ "CSP should block the inline script that modifies test_id");
+
+ // attempt to click a link to a data: URI (will inherit the CSP of the
+ // origin document) and navigate to the data URI in the link.
+ content.document.getElementById("test_data_link").click();
+ });
+
+ yield loadedPromise;
+
+ yield ContentTask.spawn(browser, null, function() {
+ is(content.document.getElementById("test_id2").value, "ok",
+ "CSP should block the script loaded by the clicked data URI");
+ });
+
+ // close the tab
+ yield promiseRemoveTab(tab);
+
+ // open new tab and recover the state
+ tab = ss.undoCloseTab(window, 0);
+ yield promiseTabRestored(tab);
+ browser = tab.linkedBrowser;
+
+ yield ContentTask.spawn(browser, null, function() {
+ is(content.document.getElementById("test_id2").value, "ok",
+ "CSP should block the script loaded by the clicked data URI after restore");
+ });
+
+ // clean up
+ gBrowser.removeTab(tab);
+});
+
+// injects an inline script element (with a text body)
+function injectInlineScript(browser, scriptText) {
+ return ContentTask.spawn(browser, scriptText, function(text) {
+ let scriptElt = content.document.createElement("script");
+ scriptElt.type = "text/javascript";
+ scriptElt.text = text;
+ content.document.body.appendChild(scriptElt);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_911547_sample.html b/browser/components/sessionstore/test/browser_911547_sample.html
new file mode 100644
index 000000000..ccc201159
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_911547_sample.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test 911547</title>
+ </head>
+<body>
+
+ <!--
+ this element gets modified by an injected script;
+ that script should be blocked by CSP.
+ Inline scripts can modify it, but not data uris.
+ -->
+ <input type="text" id="test_id" value="ok">
+
+ <a id="test_data_link" href="data:text/html;charset=utf-8,<input type='text' id='test_id2' value='ok'/> <script>document.getElementById('test_id2').value = 'fail';</script>">Test Link</a>
+
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_911547_sample.html^headers^ b/browser/components/sessionstore/test/browser_911547_sample.html^headers^
new file mode 100644
index 000000000..4623dec30
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_911547_sample.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'self'
diff --git a/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js b/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js
new file mode 100644
index 000000000..3050bd4c1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js
@@ -0,0 +1,21 @@
+"use strict";
+
+// Tests that an about:privatebrowsing tab with no history will not
+// be saved into session store and thus, it will not show up in
+// Recently Closed Tabs.
+
+add_task(function* () {
+ let tab = gBrowser.addTab("about:privatebrowsing");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ is(gBrowser.browsers[1].currentURI.spec, "about:privatebrowsing",
+ "we will be removing an about:privatebrowsing tab");
+
+ let r = `rand-${Math.random()}`;
+ ss.setTabValue(tab, "foobar", r);
+
+ yield promiseRemoveTab(tab);
+ let closedTabData = ss.getClosedTabData(window);
+ ok(!closedTabData.includes(r), "tab not stored in _closedTabs");
+});
diff --git a/browser/components/sessionstore/test/browser_aboutSessionRestore.js b/browser/components/sessionstore/test/browser_aboutSessionRestore.js
new file mode 100644
index 000000000..8ab91e4df
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_aboutSessionRestore.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CRASH_URL = "about:mozilla";
+const CRASH_FAVICON = "chrome://branding/content/icon32.png";
+const CRASH_SHENTRY = {url: CRASH_URL};
+const CRASH_TAB = {entries: [CRASH_SHENTRY], image: CRASH_FAVICON};
+const CRASH_STATE = {windows: [{tabs: [CRASH_TAB]}]};
+
+const TAB_URL = "about:sessionrestore";
+const TAB_FORMDATA = {url: TAB_URL, id: {sessionData: CRASH_STATE}};
+const TAB_SHENTRY = {url: TAB_URL};
+const TAB_STATE = {entries: [TAB_SHENTRY], formdata: TAB_FORMDATA};
+
+const FRAME_SCRIPT = "data:," +
+ "content.document.getElementById('errorTryAgain').click()";
+
+add_task(function* () {
+ // Prepare a blank tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Fake a post-crash tab.
+ ss.setTabState(tab, JSON.stringify(TAB_STATE));
+ yield promiseTabRestored(tab);
+
+ ok(gBrowser.tabs.length > 1, "we have more than one tab");
+
+ let view = browser.contentDocument.getElementById("tabList").view;
+ ok(view.isContainer(0), "first entry is the window");
+ is(view.getCellProperties(1, { id: "title" }), "icon",
+ "second entry is the tab and has a favicon");
+
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT, true);
+
+ // Wait until the new window was restored.
+ let win = yield waitForNewWindow();
+ yield BrowserTestUtils.closeWindow(win);
+
+ let [{tabs: [{entries: [{url}]}]}] = JSON.parse(ss.getClosedWindowData());
+ is(url, CRASH_URL, "session was restored correctly");
+ ss.forgetClosedWindow(0);
+});
+
+function waitForNewWindow() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observe(win, topic) {
+ Services.obs.removeObserver(observe, topic);
+ resolve(win);
+ }, "browser-delayed-startup-finished", false);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_async_duplicate_tab.js b/browser/components/sessionstore/test/browser_async_duplicate_tab.js
new file mode 100644
index 000000000..8696a284f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_duplicate_tab.js
@@ -0,0 +1,78 @@
+"use strict";
+
+const URL = "data:text/html;charset=utf-8,<a href=%23>clickme</a>";
+
+add_task(function* test_duplicate() {
+ // Create new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ yield TabStateFlusher.flush(browser);
+
+ // Click the link to navigate, this will add second shistory entry.
+ yield ContentTask.spawn(browser, null, function* () {
+ return new Promise(resolve => {
+ addEventListener("hashchange", function onHashChange() {
+ removeEventListener("hashchange", onHashChange);
+ resolve();
+ });
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Duplicate the tab.
+ let tab2 = ss.duplicateTab(window, tab);
+
+ // Wait until the tab has fully restored.
+ yield promiseTabRestored(tab2);
+ yield TabStateFlusher.flush(tab2.linkedBrowser);
+
+ // There should be two history entries now.
+ let {entries} = JSON.parse(ss.getTabState(tab2));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab2);
+ yield promiseRemoveTab(tab);
+});
+
+add_task(function* test_duplicate_remove() {
+ // Create new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ yield TabStateFlusher.flush(browser);
+
+ // Click the link to navigate, this will add second shistory entry.
+ yield ContentTask.spawn(browser, null, function* () {
+ return new Promise(resolve => {
+ addEventListener("hashchange", function onHashChange() {
+ removeEventListener("hashchange", onHashChange);
+ resolve();
+ });
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Duplicate the tab.
+ let tab2 = ss.duplicateTab(window, tab);
+
+ // Before the duplication finished, remove the tab.
+ yield Promise.all([promiseRemoveTab(tab), promiseTabRestored(tab2)]);
+ yield TabStateFlusher.flush(tab2.linkedBrowser);
+
+ // There should be two history entries now.
+ let {entries} = JSON.parse(ss.getTabState(tab2));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab2);
+});
diff --git a/browser/components/sessionstore/test/browser_async_flushes.js b/browser/components/sessionstore/test/browser_async_flushes.js
new file mode 100644
index 000000000..a4cbbfbc7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_flushes.js
@@ -0,0 +1,113 @@
+"use strict";
+
+const URL = "data:text/html;charset=utf-8,<a href=%23>clickme</a>";
+
+add_task(function* test_flush() {
+ // Create new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ yield TabStateFlusher.flush(browser);
+
+ // There should be one history entry.
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is a single history entry");
+
+ // Click the link to navigate, this will add second shistory entry.
+ yield ContentTask.spawn(browser, null, function* () {
+ return new Promise(resolve => {
+ addEventListener("hashchange", function onHashChange() {
+ removeEventListener("hashchange", onHashChange);
+ resolve();
+ });
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Flush to empty any queued update messages.
+ yield TabStateFlusher.flush(browser);
+
+ // There should be two history entries now.
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* test_crash() {
+ // Create new tab.
+ let tab = gBrowser.addTab(URL);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ yield TabStateFlusher.flush(browser);
+
+ // There should be one history entry.
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is a single history entry");
+
+ // Click the link to navigate.
+ yield ContentTask.spawn(browser, null, function* () {
+ return new Promise(resolve => {
+ addEventListener("hashchange", function onHashChange() {
+ removeEventListener("hashchange", onHashChange);
+ resolve();
+ });
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Crash the browser and flush. Both messages are async and will be sent to
+ // the content process. The "crash" message makes it first so that we don't
+ // get a chance to process the flush. The TabStateFlusher however should be
+ // notified so that the flush still completes.
+ let promise1 = BrowserTestUtils.crashBrowser(browser);
+ let promise2 = TabStateFlusher.flush(browser);
+ yield Promise.all([promise1, promise2]);
+
+ // The pending update should be lost.
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 1, "still only one history entry");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* test_remove() {
+ // Create new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ yield TabStateFlusher.flush(browser);
+
+ // There should be one history entry.
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is a single history entry");
+
+ // Click the link to navigate.
+ yield ContentTask.spawn(browser, null, function* () {
+ return new Promise(resolve => {
+ addEventListener("hashchange", function onHashChange() {
+ removeEventListener("hashchange", onHashChange);
+ resolve();
+ });
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Request a flush and remove the tab. The flush should still complete.
+ yield Promise.all([TabStateFlusher.flush(browser), promiseRemoveTab(tab)]);
+})
diff --git a/browser/components/sessionstore/test/browser_async_remove_tab.js b/browser/components/sessionstore/test/browser_async_remove_tab.js
new file mode 100644
index 000000000..20f3463d0
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_remove_tab.js
@@ -0,0 +1,242 @@
+"use strict";
+
+function* createTabWithRandomValue(url) {
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Set a random value.
+ let r = `rand-${Math.random()}`;
+ ss.setTabValue(tab, "foobar", r);
+
+ // Flush to ensure there are no scheduled messages.
+ yield TabStateFlusher.flush(browser);
+
+ return {tab, r};
+}
+
+function isValueInClosedData(rval) {
+ return ss.getClosedTabData(window).includes(rval);
+}
+
+function restoreClosedTabWithValue(rval) {
+ let closedTabData = JSON.parse(ss.getClosedTabData(window));
+ let index = closedTabData.findIndex(function (data) {
+ return (data.state.extData && data.state.extData.foobar) == rval;
+ });
+
+ if (index == -1) {
+ throw new Error("no closed tab found for given rval");
+ }
+
+ return ss.undoCloseTab(window, index);
+}
+
+function promiseNewLocationAndHistoryEntryReplaced(browser, snippet) {
+ return ContentTask.spawn(browser, snippet, function* (snippet) {
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let shistory = webNavigation.sessionHistory;
+
+ // Evaluate the snippet that the changes the location.
+ eval(snippet);
+
+ return new Promise(resolve => {
+ let listener = {
+ OnHistoryReplaceEntry() {
+ shistory.removeSHistoryListener(this);
+ resolve();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsISHistoryListener,
+ Ci.nsISupportsWeakReference
+ ])
+ };
+
+ shistory.addSHistoryListener(listener);
+
+ /* Keep the weak shistory listener alive. */
+ addEventListener("unload", function () {
+ try {
+ shistory.removeSHistoryListener(listener);
+ } catch (e) { /* Will most likely fail. */ }
+ });
+ });
+ });
+}
+
+function promiseHistoryEntryReplacedNonRemote(browser) {
+ let {listeners} = promiseHistoryEntryReplacedNonRemote;
+
+ return new Promise(resolve => {
+ let shistory = browser.webNavigation.sessionHistory;
+
+ let listener = {
+ OnHistoryReplaceEntry() {
+ shistory.removeSHistoryListener(this);
+ resolve();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsISHistoryListener,
+ Ci.nsISupportsWeakReference
+ ])
+ };
+
+ shistory.addSHistoryListener(listener);
+ listeners.set(browser, listener);
+ });
+}
+promiseHistoryEntryReplacedNonRemote.listeners = new WeakMap();
+
+add_task(function* dont_save_empty_tabs() {
+ let {tab, r} = yield createTabWithRandomValue("about:blank");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // No tab state worth saving.
+ ok(!isValueInClosedData(r), "closed tab not saved");
+ yield promise;
+
+ // Still no tab state worth saving.
+ ok(!isValueInClosedData(r), "closed tab not saved");
+});
+
+add_task(function* save_worthy_tabs_remote() {
+ let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+ ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+ yield promise;
+
+ // Tab state still deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* save_worthy_tabs_nonremote() {
+ let {tab, r} = yield createTabWithRandomValue("about:robots");
+ ok(!tab.linkedBrowser.isRemoteBrowser, "browser is not remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+ yield promise;
+
+ // Tab state still deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* save_worthy_tabs_remote_final() {
+ let {tab, r} = yield createTabWithRandomValue("about:blank");
+ let browser = tab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "browser is remote");
+
+ // Replace about:blank with a new remote page.
+ let snippet = 'webNavigation.loadURI("https://example.com/", null, null, null, null)';
+ yield promiseNewLocationAndHistoryEntryReplaced(browser, snippet);
+
+ // Remotness shouldn't have changed.
+ ok(browser.isRemoteBrowser, "browser is still remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // No tab state worth saving (that we know about yet).
+ ok(!isValueInClosedData(r), "closed tab not saved");
+ yield promise;
+
+ // Turns out there is a tab state worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* save_worthy_tabs_nonremote_final() {
+ let {tab, r} = yield createTabWithRandomValue("about:blank");
+ let browser = tab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "browser is remote");
+
+ // Replace about:blank with a non-remote entry.
+ yield BrowserTestUtils.loadURI(browser, "about:robots");
+ ok(!browser.isRemoteBrowser, "browser is not remote anymore");
+
+ // Wait until the new entry replaces about:blank.
+ yield promiseHistoryEntryReplacedNonRemote(browser);
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // No tab state worth saving (that we know about yet).
+ ok(!isValueInClosedData(r), "closed tab not saved");
+ yield promise;
+
+ // Turns out there is a tab state worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* dont_save_empty_tabs_final() {
+ let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+ let browser = tab.linkedBrowser;
+
+ // Replace the current page with an about:blank entry.
+ let snippet = 'content.location.replace("about:blank")';
+ yield promiseNewLocationAndHistoryEntryReplaced(browser, snippet);
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // Tab state deemed worth saving (yet).
+ ok(isValueInClosedData(r), "closed tab saved");
+ yield promise;
+
+ // Turns out we don't want to save the tab state.
+ ok(!isValueInClosedData(r), "closed tab not saved");
+});
+
+add_task(function* undo_worthy_tabs() {
+ let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+ ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+
+ // Restore the closed tab before receiving its final message.
+ tab = restoreClosedTabWithValue(r);
+
+ // Wait for the final update message.
+ yield promise;
+
+ // Check we didn't add the tab back to the closed list.
+ ok(!isValueInClosedData(r), "tab no longer closed");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+});
+
+add_task(function* forget_worthy_tabs_remote() {
+ let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+ ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTab(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+
+ // Forget the closed tab.
+ ss.forgetClosedTab(window, 0);
+
+ // Wait for the final update message.
+ yield promise;
+
+ // Check we didn't add the tab back to the closed list.
+ ok(!isValueInClosedData(r), "we forgot about the tab");
+});
diff --git a/browser/components/sessionstore/test/browser_async_window_flushing.js b/browser/components/sessionstore/test/browser_async_window_flushing.js
new file mode 100644
index 000000000..418c055c2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_window_flushing.js
@@ -0,0 +1,178 @@
+"use strict";
+
+const PAGE = "http://example.com/";
+
+/**
+ * Tests that if we initially discard a window as not interesting
+ * to save in the closed windows array, that we revisit that decision
+ * after a window flush has completed.
+ */
+add_task(function* test_add_interesting_window() {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ yield pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Depending on previous tests, we might already have some closed
+ // windows stored. We'll use its length to determine whether or not
+ // the window was added or not.
+ let initialClosedWindows = ss.getClosedWindowCount();
+
+ // Make sure we can actually store another closed window
+ yield pushPrefs(["browser.sessionstore.max_windows_undo",
+ initialClosedWindows + 1]);
+
+ // Create a new browser window. Since the default window will start
+ // at about:blank, SessionStore should find this tab (and therefore the
+ // whole window) uninteresting, and should not initially put it into
+ // the closed windows array.
+ let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+
+ let browser = newWin.gBrowser.selectedBrowser;
+
+ // Send a message that will cause the content to change its location
+ // to someplace more interesting. We've disabled auto updates from
+ // the browser, so the parent won't know about this
+ yield ContentTask.spawn(browser, PAGE, function*(PAGE) {
+ content.location = PAGE;
+ });
+
+ yield promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry");
+
+ // Clear out the userTypedValue so that the new window looks like
+ // it's really not worth restoring.
+ browser.userTypedValue = null;
+
+ // Once the domWindowClosed Promise resolves, the window should
+ // have closed, and SessionStore's onClose handler should have just
+ // run.
+ let domWindowClosed = BrowserTestUtils.domWindowClosed(newWin);
+
+ // Once this windowClosed Promise resolves, we should have finished
+ // the flush and revisited our decision to put this window into
+ // the closed windows array.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+
+ // Ok, let's close the window.
+ newWin.close();
+
+ yield domWindowClosed;
+ // OnClose has just finished running.
+ let currentClosedWindows = ss.getClosedWindowCount();
+ is(currentClosedWindows, initialClosedWindows,
+ "We should not have added the window to the closed windows array");
+
+ yield windowClosed;
+ // The window flush has finished
+ currentClosedWindows = ss.getClosedWindowCount();
+ is(currentClosedWindows,
+ initialClosedWindows + 1,
+ "We should have added the window to the closed windows array");
+});
+
+/**
+ * Tests that if we initially store a closed window as interesting
+ * to save in the closed windows array, that we revisit that decision
+ * after a window flush has completed, and stop storing a window that
+ * we've deemed no longer interesting.
+ */
+add_task(function* test_remove_uninteresting_window() {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ yield pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Depending on previous tests, we might already have some closed
+ // windows stored. We'll use its length to determine whether or not
+ // the window was added or not.
+ let initialClosedWindows = ss.getClosedWindowCount();
+
+ // Make sure we can actually store another closed window
+ yield pushPrefs(["browser.sessionstore.max_windows_undo",
+ initialClosedWindows + 1]);
+
+ let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+
+ // Now browse the initial tab of that window to an interesting
+ // site.
+ let tab = newWin.gBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+ browser.loadURI(PAGE);
+
+ yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ yield TabStateFlusher.flush(browser);
+
+ // Send a message that will cause the content to purge its
+ // history entries and make itself seem uninteresting.
+ yield ContentTask.spawn(browser, null, function*() {
+ // Epic hackery to make this browser seem suddenly boring.
+ Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+ docShell.setCurrentURI(BrowserUtils.makeURI("about:blank"));
+
+ let {sessionHistory} = docShell.QueryInterface(Ci.nsIWebNavigation);
+ sessionHistory.PurgeHistory(sessionHistory.count);
+ });
+
+ // Once the domWindowClosed Promise resolves, the window should
+ // have closed, and SessionStore's onClose handler should have just
+ // run.
+ let domWindowClosed = BrowserTestUtils.domWindowClosed(newWin);
+
+ // Once this windowClosed Promise resolves, we should have finished
+ // the flush and revisited our decision to put this window into
+ // the closed windows array.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+
+ // Ok, let's close the window.
+ newWin.close();
+
+ yield domWindowClosed;
+ // OnClose has just finished running.
+ let currentClosedWindows = ss.getClosedWindowCount();
+ is(currentClosedWindows, initialClosedWindows + 1,
+ "We should have added the window to the closed windows array");
+
+ yield windowClosed;
+ // The window flush has finished
+ currentClosedWindows = ss.getClosedWindowCount();
+ is(currentClosedWindows,
+ initialClosedWindows,
+ "We should have removed the window from the closed windows array");
+});
+
+/**
+ * Tests that when we close a window, it is immediately removed from the
+ * _windows array.
+ */
+add_task(function* test_synchronously_remove_window_state() {
+ // Depending on previous tests, we might already have some closed
+ // windows stored. We'll use its length to determine whether or not
+ // the window was added or not.
+ let state = JSON.parse(ss.getBrowserState());
+ ok(state, "Make sure we can get the state");
+ let initialWindows = state.windows.length;
+
+ // Open a new window and send the first tab somewhere
+ // interesting.
+ let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+ let browser = newWin.gBrowser.selectedBrowser;
+ browser.loadURI(PAGE);
+ yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ yield TabStateFlusher.flush(browser);
+
+ state = JSON.parse(ss.getBrowserState());
+ is(state.windows.length, initialWindows + 1,
+ "The new window to be in the state");
+
+ // Now close the window, and make sure that the window was removed
+ // from the windows list from the SessionState. We're specifically
+ // testing the case where the window is _not_ removed in between
+ // the close-initiated flush request and the flush response.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+ newWin.close();
+
+ state = JSON.parse(ss.getBrowserState());
+ is(state.windows.length, initialWindows,
+ "The new window should have been removed from the state");
+
+ // Wait for our window to go away
+ yield windowClosed;
+});
diff --git a/browser/components/sessionstore/test/browser_attributes.js b/browser/components/sessionstore/test/browser_attributes.js
new file mode 100644
index 000000000..40c7b4e02
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_attributes.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test makes sure that we correctly preserve tab attributes when storing
+ * and restoring tabs. It also ensures that we skip special attributes like
+ * 'image', 'muted' and 'pending' that need to be handled differently or internally.
+ */
+
+const PREF = "browser.sessionstore.restore_on_demand";
+
+add_task(function* test() {
+ Services.prefs.setBoolPref(PREF, true)
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ // Add a new tab with a nice icon.
+ let tab = gBrowser.addTab("about:robots");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ // Check that the tab has 'image' and 'iconLoadingPrincipal' attributes.
+ ok(tab.hasAttribute("image"), "tab.image exists");
+ ok(tab.hasAttribute("iconLoadingPrincipal"), "tab.iconLoadingPrincipal exists");
+
+ tab.toggleMuteAudio();
+ // Check that the tab has a 'muted' attribute.
+ ok(tab.hasAttribute("muted"), "tab.muted exists");
+
+ // Make sure we do not persist 'image' or 'muted' attributes.
+ ss.persistTabAttribute("image");
+ ss.persistTabAttribute("muted");
+ ss.persistTabAttribute("iconLoadingPrincipal");
+ let {attributes} = JSON.parse(ss.getTabState(tab));
+ ok(!("image" in attributes), "'image' attribute not saved");
+ ok(!("iconLoadingPrincipal" in attributes), "'iconLoadingPrincipal' attribute not saved");
+ ok(!("muted" in attributes), "'muted' attribute not saved");
+ ok(!("custom" in attributes), "'custom' attribute not saved");
+
+ // Test persisting a custom attribute.
+ tab.setAttribute("custom", "foobar");
+ ss.persistTabAttribute("custom");
+
+ ({attributes} = JSON.parse(ss.getTabState(tab)));
+ is(attributes.custom, "foobar", "'custom' attribute is correct");
+
+ // Make sure we're backwards compatible and restore old 'image' attributes.
+ let state = {
+ entries: [{url: "about:mozilla"}],
+ attributes: {custom: "foobaz"},
+ image: gBrowser.getIcon(tab)
+ };
+
+ // Prepare a pending tab waiting to be restored.
+ let promise = promiseTabRestoring(tab);
+ ss.setTabState(tab, JSON.stringify(state));
+ yield promise;
+
+ ok(tab.hasAttribute("pending"), "tab is pending");
+ is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
+ ok(!state.attributes.image, "'image' attribute not saved");
+
+ // Let the pending tab load.
+ gBrowser.selectedTab = tab;
+ yield promiseTabRestored(tab);
+
+ // Ensure no 'image' or 'pending' attributes are stored.
+ ({attributes} = JSON.parse(ss.getTabState(tab)));
+ ok(!("image" in attributes), "'image' attribute not saved");
+ ok(!("pending" in attributes), "'pending' attribute not saved");
+ is(attributes.custom, "foobaz", "'custom' attribute is correct");
+
+ // Clean up.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_background_tab_crash.js b/browser/components/sessionstore/test/browser_background_tab_crash.js
new file mode 100644
index 000000000..e804b177e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_background_tab_crash.js
@@ -0,0 +1,221 @@
+"use strict";
+
+/**
+ * These tests the behaviour of the browser when background tabs crash,
+ * while the foreground tab remains.
+ *
+ * The current behavioural rule is this: if only background tabs crash,
+ * then only the first tab shown of that group should show the tab crash
+ * page, and subsequent ones should restore on demand.
+ */
+
+/**
+ * Makes the current browser tab non-remote, and then sets up two remote
+ * background tabs, ensuring that both belong to the same content process.
+ * Callers should pass in a testing function that will execute (and possibly
+ * yield Promises) taking the created background tabs as arguments. Once
+ * the testing function completes, this function will take care of closing
+ * the opened tabs.
+ *
+ * @param testFn (function)
+ * A Promise-generating function that will be called once the tabs
+ * are opened and ready.
+ * @return Promise
+ * Resolves once the testing function completes and the opened tabs
+ * have been completely closed.
+ */
+function* setupBackgroundTabs(testFn) {
+ const REMOTE_PAGE = "http://www.example.com";
+ const NON_REMOTE_PAGE = "about:robots";
+
+ // Browse the initial tab to a non-remote page, which we'll have in the
+ // foreground.
+ let initialTab = gBrowser.selectedTab;
+ let initialBrowser = initialTab.linkedBrowser;
+ initialBrowser.loadURI(NON_REMOTE_PAGE);
+ yield BrowserTestUtils.browserLoaded(initialBrowser);
+
+ // Open some tabs that should be running in the content process.
+ let tab1 =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, REMOTE_PAGE);
+ let remoteBrowser1 = tab1.linkedBrowser;
+ yield TabStateFlusher.flush(remoteBrowser1);
+
+ let tab2 =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, REMOTE_PAGE);
+ let remoteBrowser2 = tab2.linkedBrowser;
+ yield TabStateFlusher.flush(remoteBrowser2);
+
+ // Quick sanity check - the two browsers should be remote and share the
+ // same childID, or else this test is not going to work.
+ Assert.ok(remoteBrowser1.isRemoteBrowser,
+ "Browser should be remote in order to crash.");
+ Assert.ok(remoteBrowser2.isRemoteBrowser,
+ "Browser should be remote in order to crash.");
+ Assert.equal(remoteBrowser1.frameLoader.childID,
+ remoteBrowser2.frameLoader.childID,
+ "Both remote browsers should share the same content process.");
+
+ // Now switch back to the non-remote browser...
+ yield BrowserTestUtils.switchTab(gBrowser, initialTab);
+
+ yield testFn([tab1, tab2]);
+
+ yield BrowserTestUtils.removeTab(tab1);
+ yield BrowserTestUtils.removeTab(tab2);
+}
+
+/**
+ * Takes some set of background tabs that are assumed to all belong to
+ * the same content process, and crashes them.
+ *
+ * @param tabs (Array(<xul:tab>))
+ * The tabs to crash.
+ * @return Promise
+ * Resolves once the tabs have crashed and entered the pending
+ * background state.
+ */
+function* crashBackgroundTabs(tabs) {
+ Assert.ok(tabs.length > 0, "Need to crash at least one tab.");
+ for (let tab of tabs) {
+ Assert.ok(tab.linkedBrowser.isRemoteBrowser, "tab is remote");
+ }
+
+ let remotenessChangePromises = tabs.map((t) => {
+ return BrowserTestUtils.waitForEvent(t, "TabRemotenessChange");
+ });
+
+ let tabsRevived = tabs.map((t) => {
+ return promiseTabRestoring(t);
+ });
+
+ yield BrowserTestUtils.crashBrowser(tabs[0].linkedBrowser, false);
+ yield Promise.all(remotenessChangePromises);
+ yield Promise.all(tabsRevived);
+
+ // Both background tabs should now be in the pending restore
+ // state.
+ for (let tab of tabs) {
+ Assert.ok(!tab.linkedBrowser.isRemoteBrowser, "tab is not remote");
+ Assert.ok(!tab.linkedBrowser.hasAttribute("crashed"), "tab is not crashed");
+ Assert.ok(tab.linkedBrowser.hasAttribute("pending"), "tab is pending");
+ }
+}
+
+add_task(function* setup() {
+ // We'll simplify by making sure we only ever one content process for this
+ // test.
+ yield SpecialPowers.pushPrefEnv({ set: [[ "dom.ipc.processCount", 1 ]] });
+
+ // On debug builds, crashing tabs results in much thinking, which
+ // slows down the test and results in intermittent test timeouts,
+ // so we'll pump up the expected timeout for this test.
+ requestLongerTimeout(5);
+});
+
+/**
+ * Tests that if a content process crashes taking down only
+ * background tabs, then the first of those tabs that the user
+ * selects will show the tab crash page, but the rest will restore
+ * on demand.
+ */
+add_task(function* test_background_crash_simple() {
+ yield setupBackgroundTabs(function*([tab1, tab2]) {
+ // Let's crash one of those background tabs now...
+ yield crashBackgroundTabs([tab1, tab2]);
+
+ // Selecting the first tab should now send it to the tab crashed page.
+ let tabCrashedPagePromise =
+ BrowserTestUtils.waitForContentEvent(tab1.linkedBrowser,
+ "AboutTabCrashedReady",
+ false, null, true);
+ yield BrowserTestUtils.switchTab(gBrowser, tab1);
+ yield tabCrashedPagePromise;
+
+ // Selecting the second tab should restore it.
+ let tabRestored = promiseTabRestored(tab2);
+ yield BrowserTestUtils.switchTab(gBrowser, tab2);
+ yield tabRestored;
+ });
+});
+
+/**
+ * Tests that if a content process crashes taking down only
+ * background tabs, and the user is configured to send backlogged
+ * crash reports automatically, that the tab crashed page is not
+ * shown.
+ */
+add_task(function* test_background_crash_autosubmit_backlogged() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", true]],
+ });
+
+ yield setupBackgroundTabs(function*([tab1, tab2]) {
+ // Let's crash one of those background tabs now...
+ yield crashBackgroundTabs([tab1, tab2]);
+
+ // Selecting the first tab should restore it.
+ let tabRestored = promiseTabRestored(tab1);
+ yield BrowserTestUtils.switchTab(gBrowser, tab1);
+ yield tabRestored;
+
+ // Selecting the second tab should restore it.
+ tabRestored = promiseTabRestored(tab2);
+ yield BrowserTestUtils.switchTab(gBrowser, tab2);
+ yield tabRestored;
+ });
+
+ yield SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that if there are two background tab crashes in a row, that
+ * the two sets of background crashes don't interfere with one another.
+ *
+ * Specifically, if we start with two background tabs (1, 2) which crash,
+ * and we visit 1, 1 should go to the tab crashed page. If we then have
+ * two new background tabs (3, 4) crash, visiting 2 should still restore.
+ * Visiting 4 should show us the tab crashed page, and then visiting 3
+ * should restore.
+ */
+add_task(function* test_background_crash_multiple() {
+ let initialTab = gBrowser.selectedTab;
+
+ yield setupBackgroundTabs(function*([tab1, tab2]) {
+ // Let's crash one of those background tabs now...
+ yield crashBackgroundTabs([tab1, tab2]);
+
+ // Selecting the first tab should now send it to the tab crashed page.
+ let tabCrashedPagePromise =
+ BrowserTestUtils.waitForContentEvent(tab1.linkedBrowser,
+ "AboutTabCrashedReady",
+ false, null, true);
+ yield BrowserTestUtils.switchTab(gBrowser, tab1);
+ yield tabCrashedPagePromise;
+
+ // Now switch back to the original non-remote tab...
+ yield BrowserTestUtils.switchTab(gBrowser, initialTab);
+
+ yield setupBackgroundTabs(function*([tab3, tab4]) {
+ yield crashBackgroundTabs([tab3, tab4]);
+
+ // Selecting the second tab should restore it.
+ let tabRestored = promiseTabRestored(tab2);
+ yield BrowserTestUtils.switchTab(gBrowser, tab2);
+ yield tabRestored;
+
+ // Selecting the fourth tab should now send it to the tab crashed page.
+ let tabCrashedPagePromise =
+ BrowserTestUtils.waitForContentEvent(tab4.linkedBrowser,
+ "AboutTabCrashedReady",
+ false, null, true);
+ yield BrowserTestUtils.switchTab(gBrowser, tab4);
+ yield tabCrashedPagePromise;
+
+ // Selecting the third tab should restore it.
+ tabRestored = promiseTabRestored(tab3);
+ yield BrowserTestUtils.switchTab(gBrowser, tab3);
+ yield tabRestored;
+ });
+ });
+});
diff --git a/browser/components/sessionstore/test/browser_backup_recovery.js b/browser/components/sessionstore/test/browser_backup_recovery.js
new file mode 100644
index 000000000..81f678856
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_backup_recovery.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests are for a sessionstore.js atomic backup.
+// Each test will wait for a write to the Session Store
+// before executing.
+
+var OS = Cu.import("resource://gre/modules/osfile.jsm", {}).OS;
+var {File, Constants, Path} = OS;
+
+const PREF_SS_INTERVAL = "browser.sessionstore.interval";
+const Paths = SessionFile.Paths;
+
+// A text decoder.
+var gDecoder = new TextDecoder();
+// Global variables that contain sessionstore.js and sessionstore.bak data for
+// comparison between tests.
+var gSSData;
+var gSSBakData;
+
+function promiseRead(path) {
+ return File.read(path, {encoding: "utf-8"});
+}
+
+add_task(function* init() {
+ // Make sure that we are not racing with SessionSaver's time based
+ // saves.
+ Services.prefs.setIntPref(PREF_SS_INTERVAL, 10000000);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF_SS_INTERVAL));
+});
+
+add_task(function* test_creation() {
+ // Create dummy sessionstore backups
+ let OLD_BACKUP = Path.join(Constants.Path.profileDir, "sessionstore.bak");
+ let OLD_UPGRADE_BACKUP = Path.join(Constants.Path.profileDir, "sessionstore.bak-0000000");
+
+ yield File.writeAtomic(OLD_BACKUP, "sessionstore.bak");
+ yield File.writeAtomic(OLD_UPGRADE_BACKUP, "sessionstore upgrade backup");
+
+ yield SessionFile.wipe();
+ yield SessionFile.read(); // Reinitializes SessionFile
+
+ // Ensure none of the sessionstore files and backups exists
+ for (let k of Paths.loadOrder) {
+ ok(!(yield File.exists(Paths[k])), "After wipe " + k + " sessionstore file doesn't exist");
+ }
+ ok(!(yield File.exists(OLD_BACKUP)), "After wipe, old backup doesn't exist");
+ ok(!(yield File.exists(OLD_UPGRADE_BACKUP)), "After wipe, old upgrade backup doesn't exist");
+
+ // Open a new tab, save session, ensure that the correct files exist.
+ let URL_BASE = "http://example.com/?atomic_backup_test_creation=" + Math.random();
+ let URL = URL_BASE + "?first_write";
+ let tab = gBrowser.addTab(URL);
+
+ info("Testing situation after a single write");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ yield SessionSaver.run();
+
+ ok((yield File.exists(Paths.recovery)), "After write, recovery sessionstore file exists again");
+ ok(!(yield File.exists(Paths.recoveryBackup)), "After write, recoveryBackup sessionstore doesn't exist");
+ ok((yield promiseRead(Paths.recovery)).indexOf(URL) != -1, "Recovery sessionstore file contains the required tab");
+ ok(!(yield File.exists(Paths.clean)), "After first write, clean shutdown sessionstore doesn't exist, since we haven't shutdown yet");
+
+ // Open a second tab, save session, ensure that the correct files exist.
+ info("Testing situation after a second write");
+ let URL2 = URL_BASE + "?second_write";
+ tab.linkedBrowser.loadURI(URL2);
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ yield SessionSaver.run();
+
+ ok((yield File.exists(Paths.recovery)), "After second write, recovery sessionstore file still exists");
+ ok((yield promiseRead(Paths.recovery)).indexOf(URL2) != -1, "Recovery sessionstore file contains the latest url");
+ ok((yield File.exists(Paths.recoveryBackup)), "After write, recoveryBackup sessionstore now exists");
+ let backup = yield promiseRead(Paths.recoveryBackup);
+ ok(backup.indexOf(URL2) == -1, "Recovery backup doesn't contain the latest url");
+ ok(backup.indexOf(URL) != -1, "Recovery backup contains the original url");
+ ok(!(yield File.exists(Paths.clean)), "After first write, clean shutdown sessinstore doesn't exist, since we haven't shutdown yet");
+
+ info("Reinitialize, ensure that we haven't leaked sensitive files");
+ yield SessionFile.read(); // Reinitializes SessionFile
+ yield SessionSaver.run();
+ ok(!(yield File.exists(Paths.clean)), "After second write, clean shutdown sessonstore doesn't exist, since we haven't shutdown yet");
+ ok(!(yield File.exists(Paths.upgradeBackup)), "After second write, clean shutdwn sessionstore doesn't exist, since we haven't shutdown yet");
+ ok(!(yield File.exists(Paths.nextUpgradeBackup)), "After second write, clean sutdown sessionstore doesn't exist, since we haven't shutdown yet");
+
+ gBrowser.removeTab(tab);
+ yield SessionFile.wipe();
+});
+
+var promiseSource = Task.async(function*(name) {
+ let URL = "http://example.com/?atomic_backup_test_recovery=" + Math.random() + "&name=" + name;
+ let tab = gBrowser.addTab(URL);
+
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ yield SessionSaver.run();
+ gBrowser.removeTab(tab);
+
+ let SOURCE = yield promiseRead(Paths.recovery);
+ yield SessionFile.wipe();
+ return SOURCE;
+});
+
+add_task(function* test_recovery() {
+ // Remove all files.
+ yield SessionFile.wipe();
+ info("Attempting to recover from the recovery file");
+
+ // Create Paths.recovery, ensure that we can recover from it.
+ let SOURCE = yield promiseSource("Paths.recovery");
+ yield File.makeDir(Paths.backups);
+ yield File.writeAtomic(Paths.recovery, SOURCE);
+ is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the recovery file");
+ yield SessionFile.wipe();
+
+ info("Corrupting recovery file, attempting to recover from recovery backup");
+ SOURCE = yield promiseSource("Paths.recoveryBackup");
+ yield File.makeDir(Paths.backups);
+ yield File.writeAtomic(Paths.recoveryBackup, SOURCE);
+ yield File.writeAtomic(Paths.recovery, "<Invalid JSON>");
+ is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the recovery file");
+ yield SessionFile.wipe();
+});
+
+add_task(function* test_recovery_inaccessible() {
+ // Can't do chmod() on non-UNIX platforms, we need that for this test.
+ if (AppConstants.platform != "macosx" && AppConstants.platform != "linux") {
+ return;
+ }
+
+ info("Making recovery file inaccessible, attempting to recover from recovery backup");
+ let SOURCE_RECOVERY = yield promiseSource("Paths.recovery");
+ let SOURCE = yield promiseSource("Paths.recoveryBackup");
+ yield File.makeDir(Paths.backups);
+ yield File.writeAtomic(Paths.recoveryBackup, SOURCE);
+
+ // Write a valid recovery file but make it inaccessible.
+ yield File.writeAtomic(Paths.recovery, SOURCE_RECOVERY);
+ yield File.setPermissions(Paths.recovery, { unixMode: 0 });
+
+ is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the recovery file");
+ yield File.setPermissions(Paths.recovery, { unixMode: 0o644 });
+});
+
+add_task(function* test_clean() {
+ yield SessionFile.wipe();
+ let SOURCE = yield promiseSource("Paths.clean");
+ yield File.writeAtomic(Paths.clean, SOURCE);
+ yield SessionFile.read();
+ yield SessionSaver.run();
+ is((yield promiseRead(Paths.cleanBackup)), SOURCE, "After first read/write, clean shutdown file has been moved to cleanBackup");
+});
+
+
+/**
+ * Tests loading of sessionstore when format version is known.
+ */
+add_task(function* test_version() {
+ info("Preparing sessionstore");
+ let SOURCE = yield promiseSource("Paths.clean");
+
+ // Check there's a format version number
+ is(JSON.parse(SOURCE).version[0], "sessionrestore", "Found sessionstore format version");
+
+ // Create Paths.clean file
+ yield File.makeDir(Paths.backups);
+ yield File.writeAtomic(Paths.clean, SOURCE);
+
+ info("Attempting to recover from the clean file");
+ // Ensure that we can recover from Paths.recovery
+ is((yield SessionFile.read()).source, SOURCE, "Recovered the correct source from the clean file");
+});
+
+/**
+ * Tests fallback to previous backups if format version is unknown.
+ */
+add_task(function* test_version_fallback() {
+ info("Preparing data, making sure that it has a version number");
+ let SOURCE = yield promiseSource("Paths.clean");
+ let BACKUP_SOURCE = yield promiseSource("Paths.cleanBackup");
+
+ is(JSON.parse(SOURCE).version[0], "sessionrestore", "Found sessionstore format version");
+ is(JSON.parse(BACKUP_SOURCE).version[0], "sessionrestore", "Found backup sessionstore format version");
+
+ yield File.makeDir(Paths.backups);
+
+ info("Modifying format version number to something incorrect, to make sure that we disregard the file.");
+ let parsedSource = JSON.parse(SOURCE);
+ parsedSource.version[0] = "bookmarks";
+ yield File.writeAtomic(Paths.clean, JSON.stringify(parsedSource));
+ yield File.writeAtomic(Paths.cleanBackup, BACKUP_SOURCE);
+ is((yield SessionFile.read()).source, BACKUP_SOURCE, "Recovered the correct source from the backup recovery file");
+
+ info("Modifying format version number to a future version, to make sure that we disregard the file.");
+ parsedSource = JSON.parse(SOURCE);
+ parsedSource.version[1] = Number.MAX_SAFE_INTEGER;
+ yield File.writeAtomic(Paths.clean, JSON.stringify(parsedSource));
+ yield File.writeAtomic(Paths.cleanBackup, BACKUP_SOURCE);
+ is((yield SessionFile.read()).source, BACKUP_SOURCE, "Recovered the correct source from the backup recovery file");
+});
+
+add_task(function* cleanup() {
+ yield SessionFile.wipe();
+});
diff --git a/browser/components/sessionstore/test/browser_broadcast.js b/browser/components/sessionstore/test/browser_broadcast.js
new file mode 100644
index 000000000..95984d6d0
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const INITIAL_VALUE = "browser_broadcast.js-initial-value-" + Date.now();
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * closing a tab.
+ */
+add_task(function flush_on_tabclose() {
+ let tab = yield createTabWithStorageData(["http://example.com"]);
+ let browser = tab.linkedBrowser;
+
+ yield modifySessionStorage(browser, {test: "on-tab-close"});
+ yield promiseRemoveTab(tab);
+
+ let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+ is(storage["http://example.com"].test, "on-tab-close",
+ "sessionStorage data has been flushed on TabClose");
+});
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * duplicating a tab.
+ */
+add_task(function flush_on_duplicate() {
+ let tab = yield createTabWithStorageData(["http://example.com"]);
+ let browser = tab.linkedBrowser;
+
+ yield modifySessionStorage(browser, {test: "on-duplicate"});
+ let tab2 = ss.duplicateTab(window, tab);
+ yield promiseTabRestored(tab2);
+
+ yield promiseRemoveTab(tab2);
+ let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+ is(storage["http://example.com"].test, "on-duplicate",
+ "sessionStorage data has been flushed when duplicating tabs");
+
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * a window is closed.
+ */
+add_task(function flush_on_windowclose() {
+ let win = yield promiseNewWindow();
+ let tab = yield createTabWithStorageData(["http://example.com"], win);
+ let browser = tab.linkedBrowser;
+
+ yield modifySessionStorage(browser, {test: "on-window-close"});
+ yield BrowserTestUtils.closeWindow(win);
+
+ let [{tabs: [_, {storage}]}] = JSON.parse(ss.getClosedWindowData());
+ is(storage["http://example.com"].test, "on-window-close",
+ "sessionStorage data has been flushed when closing a window");
+});
+
+/**
+ * This test ensures that stale tab data is ignored when reusing a tab
+ * (via e.g. setTabState) and does not overwrite the new data.
+ */
+add_task(function flush_on_settabstate() {
+ let tab = yield createTabWithStorageData(["http://example.com"]);
+ let browser = tab.linkedBrowser;
+
+ // Flush to make sure our tab state is up-to-date.
+ yield TabStateFlusher.flush(browser);
+
+ let state = ss.getTabState(tab);
+ yield modifySessionStorage(browser, {test: "on-set-tab-state"});
+
+ // Flush all data contained in the content script but send it using
+ // asynchronous messages.
+ TabStateFlusher.flush(browser);
+
+ yield promiseTabState(tab, state);
+
+ let {storage} = JSON.parse(ss.getTabState(tab));
+ is(storage["http://example.com"].test, INITIAL_VALUE,
+ "sessionStorage data has not been overwritten");
+
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that we won't lose tab data that has been sent
+ * asynchronously just before closing a tab. Flushing must re-send all data
+ * that hasn't been received by chrome, yet.
+ */
+add_task(function flush_on_tabclose_racy() {
+ let tab = yield createTabWithStorageData(["http://example.com"]);
+ let browser = tab.linkedBrowser;
+
+ // Flush to make sure we start with an empty queue.
+ yield TabStateFlusher.flush(browser);
+
+ yield modifySessionStorage(browser, {test: "on-tab-close-racy"});
+
+ // Flush all data contained in the content script but send it using
+ // asynchronous messages.
+ TabStateFlusher.flush(browser);
+ yield promiseRemoveTab(tab);
+
+ let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+ is(storage["http://example.com"].test, "on-tab-close-racy",
+ "sessionStorage data has been merged correctly to prevent data loss");
+});
+
+function promiseNewWindow() {
+ let deferred = Promise.defer();
+ whenNewWindowLoaded({private: false}, deferred.resolve);
+ return deferred.promise;
+}
+
+function createTabWithStorageData(urls, win = window) {
+ return Task.spawn(function task() {
+ let tab = win.gBrowser.addTab();
+ let browser = tab.linkedBrowser;
+
+ for (let url of urls) {
+ browser.loadURI(url);
+ yield promiseBrowserLoaded(browser);
+ yield modifySessionStorage(browser, {test: INITIAL_VALUE});
+ }
+
+ throw new Task.Result(tab);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_capabilities.js b/browser/components/sessionstore/test/browser_capabilities.js
new file mode 100644
index 000000000..456e41882
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * These tests ensures that disabling features by flipping nsIDocShell.allow*
+ * properties are (re)stored as disabled. Disallowed features must be
+ * re-enabled when the tab is re-used by another tab restoration.
+ */
+add_task(function docshell_capabilities() {
+ let tab = yield createTab();
+ let browser = tab.linkedBrowser;
+ let docShell = browser.docShell;
+
+ // Get the list of capabilities for docShells.
+ let flags = Object.keys(docShell).filter(k => k.startsWith("allow"));
+
+ // Check that everything is allowed by default for new tabs.
+ let state = JSON.parse(ss.getTabState(tab));
+ ok(!("disallow" in state), "everything allowed by default");
+ ok(flags.every(f => docShell[f]), "all flags set to true");
+
+ // Flip a couple of allow* flags.
+ docShell.allowImages = false;
+ docShell.allowMetaRedirects = false;
+
+ // Now reload the document to ensure that these capabilities
+ // are taken into account.
+ browser.reload();
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to make sure chrome received all data.
+ yield TabStateFlusher.flush(browser);
+
+ // Check that we correctly save disallowed features.
+ let disallowedState = JSON.parse(ss.getTabState(tab));
+ let disallow = new Set(disallowedState.disallow.split(","));
+ ok(disallow.has("Images"), "images not allowed");
+ ok(disallow.has("MetaRedirects"), "meta redirects not allowed");
+ is(disallow.size, 2, "two capabilities disallowed");
+
+ // Reuse the tab to restore a new, clean state into it.
+ yield promiseTabState(tab, {entries: [{url: "about:robots"}]});
+
+ // Flush to make sure chrome received all data.
+ yield TabStateFlusher.flush(browser);
+
+ // After restoring disallowed features must be available again.
+ state = JSON.parse(ss.getTabState(tab));
+ ok(!("disallow" in state), "everything allowed again");
+ ok(flags.every(f => docShell[f]), "all flags set to true");
+
+ // Restore the state with disallowed features.
+ yield promiseTabState(tab, disallowedState);
+
+ // Check that docShell flags are set.
+ ok(!docShell.allowImages, "images not allowed");
+ ok(!docShell.allowMetaRedirects, "meta redirects not allowed");
+
+ // Check that we correctly restored features as disabled.
+ state = JSON.parse(ss.getTabState(tab));
+ disallow = new Set(state.disallow.split(","));
+ ok(disallow.has("Images"), "images not allowed anymore");
+ ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
+ is(disallow.size, 2, "two capabilities disallowed");
+
+ // Clean up after ourselves.
+ gBrowser.removeTab(tab);
+});
+
+function createTab() {
+ let tab = gBrowser.addTab("about:mozilla");
+ let browser = tab.linkedBrowser;
+ return promiseBrowserLoaded(browser).then(() => tab);
+}
diff --git a/browser/components/sessionstore/test/browser_cleaner.js b/browser/components/sessionstore/test/browser_cleaner.js
new file mode 100644
index 000000000..921d7d3e4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cleaner.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+/*
+ * This test ensures that Session Restore eventually forgets about
+ * tabs and windows that have been closed a long time ago.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+const LONG_TIME_AGO = 1;
+
+const URL_TAB1 = "http://example.com/browser_cleaner.js?newtab1=" + Math.random();
+const URL_TAB2 = "http://example.com/browser_cleaner.js?newtab2=" + Math.random();
+const URL_NEWWIN = "http://example.com/browser_cleaner.js?newwin=" + Math.random();
+
+function isRecent(stamp) {
+ is(typeof stamp, "number", "This is a timestamp");
+ return Date.now() - stamp <= 60000;
+}
+
+function promiseCleanup () {
+ info("Cleaning up browser");
+
+ return promiseBrowserState(getClosedState());
+};
+
+function getClosedState() {
+ return Cu.cloneInto(CLOSED_STATE, {});
+}
+
+var CLOSED_STATE;
+
+add_task(function* init() {
+ forgetClosedWindows();
+ while (ss.getClosedTabCount(window) > 0) {
+ ss.forgetClosedTab(window, 0);
+ }
+});
+
+add_task(function* test_open_and_close() {
+ let newTab1 = gBrowser.addTab(URL_TAB1);
+ yield promiseBrowserLoaded(newTab1.linkedBrowser);
+
+ let newTab2 = gBrowser.addTab(URL_TAB2);
+ yield promiseBrowserLoaded(newTab2.linkedBrowser);
+
+ let newWin = yield promiseNewWindowLoaded();
+ let tab = newWin.gBrowser.addTab(URL_NEWWIN);
+
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ yield TabStateFlusher.flushWindow(window);
+ yield TabStateFlusher.flushWindow(newWin);
+
+ info("1. Making sure that before closing, we don't have closedAt");
+ // For the moment, no "closedAt"
+ let state = JSON.parse(ss.getBrowserState());
+ is(state.windows[0].closedAt || false, false, "1. Main window doesn't have closedAt");
+ is(state.windows[1].closedAt || false, false, "1. Second window doesn't have closedAt");
+ is(state.windows[0].tabs[0].closedAt || false, false, "1. First tab doesn't have closedAt");
+ is(state.windows[0].tabs[1].closedAt || false, false, "1. Second tab doesn't have closedAt");
+
+ info("2. Making sure that after closing, we have closedAt");
+
+ // Now close stuff, this should add closeAt
+ yield BrowserTestUtils.closeWindow(newWin);
+ yield promiseRemoveTab(newTab1);
+ yield promiseRemoveTab(newTab2);
+
+ state = CLOSED_STATE = JSON.parse(ss.getBrowserState());
+
+ is(state.windows[0].closedAt || false, false, "2. Main window doesn't have closedAt");
+ ok(isRecent(state._closedWindows[0].closedAt), "2. Second window was closed recently");
+ ok(isRecent(state.windows[0]._closedTabs[0].closedAt), "2. First tab was closed recently");
+ ok(isRecent(state.windows[0]._closedTabs[1].closedAt), "2. Second tab was closed recently");
+});
+
+
+add_task(function* test_restore() {
+ info("3. Making sure that after restoring, we don't have closedAt");
+ yield promiseBrowserState(CLOSED_STATE);
+
+ let newWin = ss.undoCloseWindow(0);
+ yield promiseDelayedStartupFinished(newWin);
+
+ let newTab2 = ss.undoCloseTab(window, 0);
+ yield promiseTabRestored(newTab2);
+
+ let newTab1 = ss.undoCloseTab(window, 0);
+ yield promiseTabRestored(newTab1);
+
+ let state = JSON.parse(ss.getBrowserState());
+
+ is(state.windows[0].closedAt || false, false, "3. Main window doesn't have closedAt");
+ is(state.windows[1].closedAt || false, false, "3. Second window doesn't have closedAt");
+ is(state.windows[0].tabs[0].closedAt || false, false, "3. First tab doesn't have closedAt");
+ is(state.windows[0].tabs[1].closedAt || false, false, "3. Second tab doesn't have closedAt");
+
+ yield BrowserTestUtils.closeWindow(newWin);
+ gBrowser.removeTab(newTab1);
+ gBrowser.removeTab(newTab2);
+});
+
+
+add_task(function* test_old_data() {
+ info("4. Removing closedAt from the sessionstore, making sure that it is added upon idle-daily");
+
+ let state = getClosedState();
+ delete state._closedWindows[0].closedAt;
+ delete state.windows[0]._closedTabs[0].closedAt;
+ delete state.windows[0]._closedTabs[1].closedAt;
+ yield promiseBrowserState(state);
+
+ info("Sending idle-daily");
+ Services.obs.notifyObservers(null, "idle-daily", "");
+ info("Sent idle-daily");
+
+ state = JSON.parse(ss.getBrowserState());
+ is(state.windows[0].closedAt || false, false, "4. Main window doesn't have closedAt");
+ ok(isRecent(state._closedWindows[0].closedAt), "4. Second window was closed recently");
+ ok(isRecent(state.windows[0]._closedTabs[0].closedAt), "4. First tab was closed recently");
+ ok(isRecent(state.windows[0]._closedTabs[1].closedAt), "4. Second tab was closed recently");
+ yield promiseCleanup();
+});
+
+
+add_task(function* test_cleanup() {
+
+ info("5. Altering closedAt to an old date, making sure that stuff gets collected, eventually");
+ yield promiseCleanup();
+
+ let state = getClosedState();
+ state._closedWindows[0].closedAt = LONG_TIME_AGO;
+ state.windows[0]._closedTabs[0].closedAt = LONG_TIME_AGO;
+ state.windows[0]._closedTabs[1].closedAt = Date.now();
+ let url = state.windows[0]._closedTabs[1].state.entries[0].url;
+
+ yield promiseBrowserState(state);
+
+ info("Sending idle-daily");
+ Services.obs.notifyObservers(null, "idle-daily", "");
+ info("Sent idle-daily");
+
+ state = JSON.parse(ss.getBrowserState());
+ is(state._closedWindows[0], undefined, "5. Second window was forgotten");
+
+ is(state.windows[0]._closedTabs.length, 1, "5. Only one closed tab left");
+ is(state.windows[0]._closedTabs[0].state.entries[0].url, url, "5. The second tab is still here");
+ yield promiseCleanup();
+});
+
diff --git a/browser/components/sessionstore/test/browser_cookies.js b/browser/components/sessionstore/test/browser_cookies.js
new file mode 100644
index 000000000..cc5b41e4b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies.js
@@ -0,0 +1,173 @@
+"use strict";
+
+const PATH = "/browser/browser/components/sessionstore/test/";
+
+/**
+ * Remove all cookies to start off a clean slate.
+ */
+add_task(function* test_setup() {
+ requestLongerTimeout(2);
+ Services.cookies.removeAll();
+});
+
+/**
+ * Test multiple scenarios with different Set-Cookie header domain= params.
+ */
+add_task(function* test_run() {
+ // Set-Cookie: foobar=random()
+ // The domain of the cookie should be the request domain (www.example.com).
+ // We should collect data only for the request domain, no parent or subdomains.
+ yield testCookieCollection({
+ host: "http://www.example.com",
+ cookieHost: "www.example.com",
+ cookieURIs: ["http://www.example.com" + PATH],
+ noCookieURIs: ["http://example.com/" + PATH]
+ });
+
+ // Set-Cookie: foobar=random()
+ // The domain of the cookie should be the request domain (example.com).
+ // We should collect data only for the request domain, no parent or subdomains.
+ yield testCookieCollection({
+ host: "http://example.com",
+ cookieHost: "example.com",
+ cookieURIs: ["http://example.com" + PATH],
+ noCookieURIs: ["http://www.example.com/" + PATH]
+ });
+
+ // Set-Cookie: foobar=random(); Domain=example.com
+ // The domain of the cookie should be the given one (.example.com).
+ // We should collect data for the given domain and its subdomains.
+ yield testCookieCollection({
+ host: "http://example.com",
+ domain: "example.com",
+ cookieHost: ".example.com",
+ cookieURIs: ["http://example.com" + PATH, "http://www.example.com/" + PATH],
+ noCookieURIs: ["about:robots"]
+ });
+
+ // Set-Cookie: foobar=random(); Domain=.example.com
+ // The domain of the cookie should be the given one (.example.com).
+ // We should collect data for the given domain and its subdomains.
+ yield testCookieCollection({
+ host: "http://example.com",
+ domain: ".example.com",
+ cookieHost: ".example.com",
+ cookieURIs: ["http://example.com" + PATH, "http://www.example.com/" + PATH],
+ noCookieURIs: ["about:robots"]
+ });
+
+ // Set-Cookie: foobar=random(); Domain=www.example.com
+ // The domain of the cookie should be the given one (.www.example.com).
+ // We should collect data for the given domain and its subdomains.
+ yield testCookieCollection({
+ host: "http://www.example.com",
+ domain: "www.example.com",
+ cookieHost: ".www.example.com",
+ cookieURIs: ["http://www.example.com/" + PATH],
+ noCookieURIs: ["http://example.com"]
+ });
+
+ // Set-Cookie: foobar=random(); Domain=.www.example.com
+ // The domain of the cookie should be the given one (.www.example.com).
+ // We should collect data for the given domain and its subdomains.
+ yield testCookieCollection({
+ host: "http://www.example.com",
+ domain: ".www.example.com",
+ cookieHost: ".www.example.com",
+ cookieURIs: ["http://www.example.com/" + PATH],
+ noCookieURIs: ["http://example.com"]
+ });
+});
+
+/**
+ * Generic test function to check sessionstore's cookie collection module with
+ * different cookie domains given in the Set-Cookie header. See above for some
+ * usage examples.
+ */
+var testCookieCollection = Task.async(function (params) {
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+
+ let urlParams = new URLSearchParams();
+ let value = Math.random();
+ urlParams.append("value", value);
+
+ if (params.domain) {
+ urlParams.append("domain", params.domain);
+ }
+
+ // Construct request URI.
+ let uri = `${params.host}${PATH}browser_cookies.sjs?${urlParams}`;
+
+ // Wait for the browser to load and the cookie to be set.
+ // These two events can probably happen in no particular order,
+ // so let's wait for them in parallel.
+ yield Promise.all([
+ waitForNewCookie(),
+ replaceCurrentURI(browser, uri)
+ ]);
+
+ // Check all URIs for which the cookie should be collected.
+ for (let uri of params.cookieURIs || []) {
+ yield replaceCurrentURI(browser, uri);
+
+ // Check the cookie.
+ let cookie = getCookie();
+ is(cookie.host, params.cookieHost, "cookie host is correct");
+ is(cookie.path, PATH, "cookie path is correct");
+ is(cookie.name, "foobar", "cookie name is correct");
+ is(cookie.value, value, "cookie value is correct");
+ }
+
+ // Check all URIs for which the cookie should NOT be collected.
+ for (let uri of params.noCookieURIs || []) {
+ yield replaceCurrentURI(browser, uri);
+
+ // Cookie should be ignored.
+ ok(!getCookie(), "no cookie collected");
+ }
+
+ // Clean up.
+ gBrowser.removeTab(tab);
+ Services.cookies.removeAll();
+});
+
+/**
+ * Replace the current URI of the given browser by loading a new URI. The
+ * browser's session history will be completely replaced. This function ensures
+ * that the parent process has the lastest shistory data before resolving.
+ */
+var replaceCurrentURI = Task.async(function* (browser, uri) {
+ // Replace the tab's current URI with the parent domain.
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ browser.loadURIWithFlags(uri, flags);
+ yield promiseBrowserLoaded(browser);
+
+ // Ensure the tab's session history is up-to-date.
+ yield TabStateFlusher.flush(browser);
+});
+
+/**
+ * Waits for a new "*example.com" cookie to be added.
+ */
+function waitForNewCookie() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subj, topic, data) {
+ let cookie = subj.QueryInterface(Ci.nsICookie2);
+ if (data == "added" && cookie.host.endsWith("example.com")) {
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }
+ }, "cookie-changed", false);
+ });
+}
+
+/**
+ * Retrieves the first cookie in the first window from the current sessionstore
+ * state.
+ */
+function getCookie() {
+ let state = JSON.parse(ss.getWindowState(window));
+ let cookies = state.windows[0].cookies || [];
+ return cookies[0] || null;
+}
diff --git a/browser/components/sessionstore/test/browser_cookies.sjs b/browser/components/sessionstore/test/browser_cookies.sjs
new file mode 100644
index 000000000..bffbd66d9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies.sjs
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+
+function handleRequest(req, resp) {
+ resp.setStatusLine(req.httpVersion, 200);
+
+ let params = new URLSearchParams(req.queryString);
+ let value = params.get("value");
+
+ let domain = "";
+ if (params.has("domain")) {
+ domain = `; Domain=${params.get("domain")}`;
+ }
+
+ resp.setHeader("Set-Cookie", `foobar=${value}${domain}`);
+ resp.write("<meta charset=utf-8>hi");
+}
diff --git a/browser/components/sessionstore/test/browser_crashedTabs.js b/browser/components/sessionstore/test/browser_crashedTabs.js
new file mode 100644
index 000000000..5841d536a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_crashedTabs.js
@@ -0,0 +1,462 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(10);
+
+const PAGE_1 = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const PAGE_2 = "data:text/html,<html><body>Another%20regular,%20everyday,%20normal%20page.";
+
+// Turn off tab animations for testing and use a single content process
+// for these tests since we want to test tabs within the crashing process here.
+add_task(function* test_initialize() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [
+ [ "dom.ipc.processCount", 1 ],
+ [ "browser.tabs.animate", false]
+ ] });
+});
+
+// Allow tabs to restore on demand so we can test pending states
+Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+
+function clickButton(browser, id) {
+ info("Clicking " + id);
+
+ let frame_script = (id) => {
+ let button = content.document.getElementById(id);
+ button.click();
+ };
+
+ let mm = browser.messageManager;
+ mm.loadFrameScript("data:,(" + frame_script.toString() + ")('" + id + "');", false);
+}
+
+/**
+ * Checks the documentURI of the root document of a remote browser
+ * to see if it equals URI. Returns a Promise that resolves if
+ * there is a match, and rejects with an error message if they
+ * do not match.
+ *
+ * @param browser
+ * The remote <xul:browser> to check the root document URI in.
+ * @param URI
+ * A string to match the root document URI against.
+ * @return Promise
+ */
+function promiseContentDocumentURIEquals(browser, URI) {
+ return new Promise((resolve, reject) => {
+ let frame_script = () => {
+ sendAsyncMessage("test:documenturi", {
+ uri: content.document.documentURI,
+ });
+ };
+
+ let mm = browser.messageManager;
+ mm.addMessageListener("test:documenturi", function onMessage(message) {
+ mm.removeMessageListener("test:documenturi", onMessage);
+ let contentURI = message.data.uri;
+ if (contentURI == URI) {
+ resolve();
+ } else {
+ reject(`Content has URI ${contentURI} which does not match ${URI}`);
+ }
+ });
+
+ mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false);
+ });
+}
+
+/**
+ * Checks the window.history.length of the root window of a remote
+ * browser to see if it equals length. Returns a Promise that resolves
+ * if there is a match, and rejects with an error message if they
+ * do not match.
+ *
+ * @param browser
+ * The remote <xul:browser> to check the root window.history.length
+ * @param length
+ * The expected history length
+ * @return Promise
+ */
+function promiseHistoryLength(browser, length) {
+ return new Promise((resolve, reject) => {
+ let frame_script = () => {
+ sendAsyncMessage("test:historylength", {
+ length: content.history.length,
+ });
+ };
+
+ let mm = browser.messageManager;
+ mm.addMessageListener("test:historylength", function onMessage(message) {
+ mm.removeMessageListener("test:historylength", onMessage);
+ let contentLength = message.data.length;
+ if (contentLength == length) {
+ resolve();
+ } else {
+ reject(`Content has window.history.length ${contentLength} which does ` +
+ `not equal expected ${length}`);
+ }
+ });
+
+ mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false);
+ });
+}
+
+/**
+ * Returns a Promise that resolves when a browser has fired the
+ * AboutTabCrashedReady event.
+ *
+ * @param browser
+ * The remote <xul:browser> that will fire the event.
+ * @return Promise
+ */
+function promiseTabCrashedReady(browser) {
+ return new Promise((resolve) => {
+ browser.addEventListener("AboutTabCrashedReady", function ready(e) {
+ browser.removeEventListener("AboutTabCrashedReady", ready, false, true);
+ resolve();
+ }, false, true);
+ });
+}
+
+/**
+ * Checks that if a tab crashes, that information about the tab crashed
+ * page does not get added to the tab history.
+ */
+add_task(function test_crash_page_not_in_history() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+ yield TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ // Check the tab state and make sure the tab crashed page isn't
+ // mentioned.
+ let {entries} = JSON.parse(ss.getTabState(newTab));
+ is(entries.length, 1, "Should have a single history entry");
+ is(entries[0].url, PAGE_1,
+ "Single entry should be the page we visited before crashing");
+
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that if a tab crashes, that when we browse away from that page
+ * to a non-blacklisted site (so the browser becomes remote again), that
+ * we record history for that new visit.
+ */
+add_task(function test_revived_history_from_remote() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+ yield TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ // Browse to a new site that will cause the browser to
+ // become remote again.
+ browser.loadURI(PAGE_2);
+ yield promiseTabRestored(newTab);
+ ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore.");
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield TabStateFlusher.flush(browser);
+
+ // Check the tab state and make sure the tab crashed page isn't
+ // mentioned.
+ let {entries} = JSON.parse(ss.getTabState(newTab));
+ is(entries.length, 2, "Should have two history entries");
+ is(entries[0].url, PAGE_1,
+ "First entry should be the page we visited before crashing");
+ is(entries[1].url, PAGE_2,
+ "Second entry should be the page we visited after crashing");
+
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that if a tab crashes, that when we browse away from that page
+ * to a blacklisted site (so the browser stays non-remote), that
+ * we record history for that new visit.
+ */
+add_task(function test_revived_history_from_non_remote() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+ yield TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ // Browse to a new site that will not cause the browser to
+ // become remote again.
+ browser.loadURI("about:mozilla");
+ yield promiseBrowserLoaded(browser);
+ ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore.");
+ ok(!browser.isRemoteBrowser, "Should not be a remote browser");
+ yield TabStateFlusher.flush(browser);
+
+ // Check the tab state and make sure the tab crashed page isn't
+ // mentioned.
+ let {entries} = JSON.parse(ss.getTabState(newTab));
+ is(entries.length, 2, "Should have two history entries");
+ is(entries[0].url, PAGE_1,
+ "First entry should be the page we visited before crashing");
+ is(entries[1].url, "about:mozilla",
+ "Second entry should be the page we visited after crashing");
+
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that we can revive a crashed tab back to the page that
+ * it was on when it crashed.
+ */
+add_task(function test_revive_tab_from_session_store() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+
+ let newTab2 = gBrowser.addTab();
+ let browser2 = newTab2.linkedBrowser;
+ ok(browser2.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser2);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_2);
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+ // Background tabs should not be crashed, but should be in the "to be restored"
+ // state.
+ ok(!newTab2.hasAttribute("crashed"), "Second tab should not be crashed.");
+ ok(newTab2.hasAttribute("pending"), "Second tab should be pending.");
+
+ // Use SessionStore to revive the first tab
+ clickButton(browser, "restoreTab");
+ yield promiseTabRestored(newTab);
+ ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore.");
+ ok(newTab2.hasAttribute("pending"), "Second tab should still be pending.");
+
+ // We can't just check browser.currentURI.spec, because from
+ // the outside, a crashed tab has the same URI as the page
+ // it crashed on (much like an about:neterror page). Instead,
+ // we have to use the documentURI on the content.
+ yield promiseContentDocumentURIEquals(browser, PAGE_2);
+
+ // We should also have two entries in the browser history.
+ yield promiseHistoryLength(browser, 2);
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(newTab2);
+});
+
+/**
+ * Checks that we can revive multiple crashed tabs back to the pages
+ * that they were on when they crashed.
+ */
+add_task(function test_revive_all_tabs_from_session_store() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+
+ // In order to see a second about:tabcrashed page, we'll need
+ // a second window, since only selected tabs will show
+ // about:tabcrashed.
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
+ let newTab2 = win2.gBrowser.addTab(PAGE_1);
+ win2.gBrowser.selectedTab = newTab2;
+ let browser2 = newTab2.linkedBrowser;
+ ok(browser2.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser2);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_2);
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+ yield TabStateFlusher.flush(browser2);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+ // Both tabs should now be crashed.
+ is(newTab.getAttribute("crashed"), "true", "First tab should be crashed");
+ is(newTab2.getAttribute("crashed"), "true", "Second window tab should be crashed");
+
+ // Use SessionStore to revive all the tabs
+ clickButton(browser, "restoreAll");
+ yield promiseTabRestored(newTab);
+ ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore.");
+ ok(!newTab.hasAttribute("pending"), "Tab shouldn't be pending.");
+ ok(!newTab2.hasAttribute("crashed"), "Second tab shouldn't be marked as crashed anymore.");
+ ok(!newTab2.hasAttribute("pending"), "Second tab shouldn't be pending.");
+
+ // We can't just check browser.currentURI.spec, because from
+ // the outside, a crashed tab has the same URI as the page
+ // it crashed on (much like an about:neterror page). Instead,
+ // we have to use the documentURI on the content.
+ yield promiseContentDocumentURIEquals(browser, PAGE_2);
+ yield promiseContentDocumentURIEquals(browser2, PAGE_1);
+
+ // We should also have two entries in the browser history.
+ yield promiseHistoryLength(browser, 2);
+
+ yield BrowserTestUtils.closeWindow(win2);
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that about:tabcrashed can close the current tab
+ */
+add_task(function test_close_tab_after_crash() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ let promise = promiseEvent(gBrowser.tabContainer, "TabClose");
+
+ // Click the close tab button
+ clickButton(browser, "closeTab");
+ yield promise;
+
+ is(gBrowser.tabs.length, 1, "Should have closed the tab");
+});
+
+
+/**
+ * Checks that "restore all" button is only shown if more than one tab
+ * is showing about:tabcrashed
+ */
+add_task(function* test_hide_restore_all_button() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ let doc = browser.contentDocument;
+ let restoreAllButton = doc.getElementById("restoreAll");
+ let restoreOneButton = doc.getElementById("restoreTab");
+
+ let restoreAllStyles = window.getComputedStyle(restoreAllButton);
+ is(restoreAllStyles.display, "none", "Restore All button should be hidden");
+ ok(restoreOneButton.classList.contains("primary"), "Restore Tab button should have the primary class");
+
+ let newTab2 = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+
+ browser.loadURI(PAGE_2);
+ yield promiseBrowserLoaded(browser);
+
+ // Load up a second window so we can get another tab to show
+ // about:tabcrashed
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
+ let newTab3 = win2.gBrowser.addTab(PAGE_2);
+ win2.gBrowser.selectedTab = newTab3;
+ let otherWinBrowser = newTab3.linkedBrowser;
+ yield promiseBrowserLoaded(otherWinBrowser);
+ // We'll need to make sure the second tab's browser has finished
+ // sending its AboutTabCrashedReady event before we know for
+ // sure whether or not we're showing the right Restore buttons.
+ let otherBrowserReady = promiseTabCrashedReady(otherWinBrowser);
+
+ // Crash the first tab.
+ yield BrowserTestUtils.crashBrowser(browser);
+ yield otherBrowserReady;
+
+ doc = browser.contentDocument;
+ restoreAllButton = doc.getElementById("restoreAll");
+ restoreOneButton = doc.getElementById("restoreTab");
+
+ restoreAllStyles = window.getComputedStyle(restoreAllButton);
+ isnot(restoreAllStyles.display, "none", "Restore All button should not be hidden");
+ ok(!(restoreOneButton.classList.contains("primary")), "Restore Tab button should not have the primary class");
+
+ yield BrowserTestUtils.closeWindow(win2);
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(newTab2);
+});
+
+add_task(function* test_aboutcrashedtabzoom() {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ yield promiseBrowserLoaded(browser);
+
+ browser.loadURI(PAGE_1);
+ yield promiseBrowserLoaded(browser);
+
+ FullZoom.enlarge();
+ let zoomLevel = ZoomManager.getZoomForBrowser(browser);
+ ok(zoomLevel !== 1, "should have enlarged");
+
+ yield TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ ok(ZoomManager.getZoomForBrowser(browser) === 1, "zoom should have reset on crash");
+
+ clickButton(browser, "restoreTab");
+ yield promiseTabRestored(newTab);
+
+ ok(ZoomManager.getZoomForBrowser(browser) === zoomLevel, "zoom should have gone back to enlarged");
+ FullZoom.reset();
+
+ gBrowser.removeTab(newTab);
+});
diff --git a/browser/components/sessionstore/test/browser_dying_cache.js b/browser/components/sessionstore/test/browser_dying_cache.js
new file mode 100644
index 000000000..c573aa5d4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_dying_cache.js
@@ -0,0 +1,66 @@
+"use strict";
+
+/**
+ * This test ensures that after closing a window we keep its state data around
+ * as long as something keeps a reference to it. It should only be possible to
+ * read data after closing - writing should fail.
+ */
+
+add_task(function* test() {
+ // Open a new window.
+ let win = yield promiseNewWindowLoaded();
+
+ // Load some URL in the current tab.
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ win.gBrowser.selectedBrowser.loadURIWithFlags("about:robots", flags);
+ yield promiseBrowserLoaded(win.gBrowser.selectedBrowser);
+
+ // Open a second tab and close the first one.
+ let tab = win.gBrowser.addTab("about:mozilla");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ yield promiseRemoveTab(win.gBrowser.tabs[0]);
+
+ // Make sure our window is still tracked by sessionstore
+ // and the window state is as expected.
+ ok("__SSi" in win, "window is being tracked by sessionstore");
+ ss.setWindowValue(win, "foo", "bar");
+ checkWindowState(win);
+
+ let state = ss.getWindowState(win);
+ let closedTabData = ss.getClosedTabData(win);
+
+ // Close our window.
+ yield BrowserTestUtils.closeWindow(win);
+
+ // SessionStore should no longer track our window
+ // but it should still report the same state.
+ ok(!("__SSi" in win), "sessionstore does no longer track our window");
+ checkWindowState(win);
+
+ // Make sure we're not allowed to modify state data.
+ Assert.throws(() => ss.setWindowState(win, {}),
+ "we're not allowed to modify state data anymore");
+ Assert.throws(() => ss.setWindowValue(win, "foo", "baz"),
+ "we're not allowed to modify state data anymore");
+});
+
+function checkWindowState(window) {
+ let {windows: [{tabs}]} = JSON.parse(ss.getWindowState(window));
+ is(tabs.length, 1, "the window has a single tab");
+ is(tabs[0].entries[0].url, "about:mozilla", "the tab is about:mozilla");
+
+ is(ss.getClosedTabCount(window), 1, "the window has one closed tab");
+ let [{state: {entries: [{url}]}}] = JSON.parse(ss.getClosedTabData(window));
+ is(url, "about:robots", "the closed tab is about:robots");
+
+ is(ss.getWindowValue(window, "foo"), "bar", "correct extData value");
+}
+
+function shouldThrow(f) {
+ try {
+ f();
+ } catch (e) {
+ return true;
+ }
+}
diff --git a/browser/components/sessionstore/test/browser_dynamic_frames.js b/browser/components/sessionstore/test/browser_dynamic_frames.js
new file mode 100644
index 000000000..e4355fee3
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_dynamic_frames.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that static frames of framesets are serialized but dynamically
+ * inserted iframes are ignored.
+ */
+add_task(function () {
+ // This URL has the following frames:
+ // + data:text/html,A (static)
+ // + data:text/html,B (static)
+ // + data:text/html,C (dynamic iframe)
+ const URL = "data:text/html;charset=utf-8," +
+ "<frameset cols=50%25,50%25><frame src='data:text/html,A'>" +
+ "<frame src='data:text/html,B'></frameset>" +
+ "<script>var i=document.createElement('iframe');" +
+ "i.setAttribute('src', 'data:text/html,C');" +
+ "document.body.appendChild(i);</script>";
+
+ // Add a new tab with two "static" and one "dynamic" frame.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+
+ // Check URLs.
+ ok(entries[0].url.startsWith("data:text/html"), "correct root url");
+ is(entries[0].children[0].url, "data:text/html,A", "correct url for 1st frame");
+ is(entries[0].children[1].url, "data:text/html,B", "correct url for 2nd frame");
+
+ // Check the number of children.
+ is(entries.length, 1, "there is one root entry ...");
+ is(entries[0].children.length, 2, "... with two child entries");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that iframes created by the network parser are serialized but
+ * dynamically inserted iframes are ignored. Navigating a subframe should
+ * create a second root entry that doesn't contain any dynamic children either.
+ */
+add_task(function () {
+ // This URL has the following frames:
+ // + data:text/html,A (static)
+ // + data:text/html,C (dynamic iframe)
+ const URL = "data:text/html;charset=utf-8," +
+ "<iframe name=t src='data:text/html,A'></iframe>" +
+ "<a id=lnk href='data:text/html,B' target=t>clickme</a>" +
+ "<script>var i=document.createElement('iframe');" +
+ "i.setAttribute('src', 'data:text/html,C');" +
+ "document.body.appendChild(i);</script>";
+
+ // Add a new tab with one "static" and one "dynamic" frame.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+
+ // Check URLs.
+ ok(entries[0].url.startsWith("data:text/html"), "correct root url");
+ ok(!entries[0].children, "no children collected");
+
+ // Navigate the subframe.
+ browser.messageManager.sendAsyncMessage("ss-test:click", {id: "lnk"});
+ yield promiseBrowserLoaded(browser, false /* don't ignore subframes */);
+
+ yield TabStateFlusher.flush(browser);
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+
+ // Check URLs.
+ ok(entries[0].url.startsWith("data:text/html"), "correct 1st root url");
+ ok(entries[1].url.startsWith("data:text/html"), "correct 2nd root url");
+ ok(!entries.children, "no children collected");
+ ok(!entries[0].children, "no children collected");
+ ok(!entries[1].children, "no children collected");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_forget_async_closings.js b/browser/components/sessionstore/test/browser_forget_async_closings.js
new file mode 100644
index 000000000..c130ec5ad
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_forget_async_closings.js
@@ -0,0 +1,144 @@
+"use strict";
+
+const PAGE = "http://example.com/";
+
+/**
+ * Creates a tab in the current window worth storing in the
+ * closedTabs array, and then closes it. Runs a synchronous
+ * forgetFn passed in that should cause us to forget the tab,
+ * and then ensures that after the tab has sent its final
+ * update message that we didn't accidentally store it in
+ * the closedTabs array.
+ *
+ * @param forgetFn (function)
+ * A synchronous function that should cause the tab
+ * to be forgotten.
+ * @returns Promise
+ */
+let forgetTabHelper = Task.async(function*(forgetFn) {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ yield pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Forget any previous closed tabs from other tests that may have
+ // run in the same session.
+ Services.obs.notifyObservers(null, "browser:purge-session-history", 0);
+
+ is(ss.getClosedTabCount(window), 0,
+ "We should have 0 closed tabs being stored.");
+
+ // Create a tab worth remembering.
+ let tab = gBrowser.addTab(PAGE);
+ let browser = tab.linkedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ yield TabStateFlusher.flush(browser);
+
+ // Now close the tab, and immediately choose to forget it.
+ let promise = BrowserTestUtils.removeTab(tab);
+
+ // At this point, the tab will have closed, but the final update
+ // to SessionStore hasn't come up yet. Now do the operation that
+ // should cause us to forget the tab.
+ forgetFn();
+
+ is(ss.getClosedTabCount(window), 0, "Should have forgotten the closed tab");
+
+ // Now wait for the final update to come up.
+ yield promise;
+
+ is(ss.getClosedTabCount(window), 0,
+ "Should not have stored the forgotten closed tab");
+});
+
+/**
+ * Creates a new window worth storing in the closeWIndows array,
+ * and then closes it. Runs a synchronous forgetFn passed in that
+ * should cause us to forget the window, and then ensures that after
+ * the window has sent its final update message that we didn't
+ * accidentally store it in the closedWindows array.
+ *
+ * @param forgetFn (function)
+ * A synchronous function that should cause the window
+ * to be forgotten.
+ * @returns Promise
+ */
+let forgetWinHelper = Task.async(function*(forgetFn) {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ yield pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Forget any previous closed windows from other tests that may have
+ // run in the same session.
+ Services.obs.notifyObservers(null, "browser:purge-session-history", 0);
+
+ is(ss.getClosedWindowCount(), 0, "We should have 0 closed windows being stored.");
+
+ let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+
+ // Create a tab worth remembering.
+ let tab = newWin.gBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+ browser.loadURI(PAGE);
+ yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ yield TabStateFlusher.flush(browser);
+
+ // Now close the window and immediately choose to forget it.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+ let domWindowClosed = BrowserTestUtils.domWindowClosed(newWin);
+
+ newWin.close();
+ yield domWindowClosed;
+
+ // At this point, the window will have closed and the onClose handler
+ // has run, but the final update to SessionStore hasn't come up yet.
+ // Now do the oepration that should cause us to forget the window.
+ forgetFn();
+
+ is(ss.getClosedWindowCount(), 0, "Should have forgotten the closed window");
+
+ // Now wait for the final update to come up.
+ yield windowClosed;
+
+ is(ss.getClosedWindowCount(), 0, "Should not have stored the closed window");
+});
+
+/**
+ * Tests that if we choose to forget a tab while waiting for its
+ * final flush to complete, we don't accidentally store it.
+ */
+add_task(function* test_forget_closed_tab() {
+ yield forgetTabHelper(() => {
+ ss.forgetClosedTab(window, 0);
+ });
+});
+
+/**
+ * Tests that if we choose to forget a tab while waiting for its
+ * final flush to complete, we don't accidentally store it.
+ */
+add_task(function* test_forget_closed_window() {
+ yield forgetWinHelper(() => {
+ ss.forgetClosedWindow(0);
+ });
+});
+
+/**
+ * Tests that if we choose to purge history while waiting for a
+ * final flush of a tab to complete, we don't accidentally store it.
+ */
+add_task(function* test_forget_purged_tab() {
+ yield forgetTabHelper(() => {
+ Services.obs.notifyObservers(null, "browser:purge-session-history", 0);
+ });
+});
+
+/**
+ * Tests that if we choose to purge history while waiting for a
+ * final flush of a window to complete, we don't accidentally
+ * store it.
+ */
+add_task(function* test_forget_purged_window() {
+ yield forgetWinHelper(() => {
+ Services.obs.notifyObservers(null, "browser:purge-session-history", 0);
+ });
+});
diff --git a/browser/components/sessionstore/test/browser_form_restore_events.js b/browser/components/sessionstore/test/browser_form_restore_events.js
new file mode 100644
index 000000000..3fc2e0fd4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_form_restore_events.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_form_restore_events_sample.html";
+
+/**
+ * Originally a test for Bug 476161, but then expanded to include all input
+ * types in bug 640136.
+ */
+add_task(function () {
+ // Load a page with some form elements.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // text fields
+ yield setInputValue(browser, {id: "modify01", value: Math.random()});
+ yield setInputValue(browser, {id: "modify02", value: Date.now()});
+
+ // textareas
+ yield setInputValue(browser, {id: "modify03", value: Math.random()});
+ yield setInputValue(browser, {id: "modify04", value: Date.now()});
+
+ // file
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ yield setInputValue(browser, {id: "modify05", value: file.path});
+
+ // select
+ yield setSelectedIndex(browser, {id: "modify06", index: 1});
+ yield setMultipleSelected(browser, {id: "modify07", indices: [0,1,2]});
+
+ // checkbox
+ yield setInputChecked(browser, {id: "modify08", checked: true});
+ yield setInputChecked(browser, {id: "modify09", checked: false});
+
+ // radio
+ yield setInputChecked(browser, {id: "modify10", checked: true});
+ yield setInputChecked(browser, {id: "modify11", checked: true});
+
+ // Duplicate the tab and check that restoring form data yields the expected
+ // input and change events for modified form fields.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2);
+
+ let inputFired = yield getTextContent(browser2, {id: "inputFired"});
+ inputFired = inputFired.trim().split().sort().join(" ");
+
+ let changeFired = yield getTextContent(browser2, {id: "changeFired"});
+ changeFired = changeFired.trim().split().sort().join(" ");
+
+ is(inputFired, "modify01 modify02 modify03 modify04 modify05",
+ "input events were only dispatched for modified input, textarea fields");
+
+ is(changeFired, "modify06 modify07 modify08 modify09 modify11",
+ "change events were only dispatched for modified select, checkbox, radio fields");
+
+ // Cleanup.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_form_restore_events_sample.html b/browser/components/sessionstore/test/browser_form_restore_events_sample.html
new file mode 100644
index 000000000..1d46d4040
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_form_restore_events_sample.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for form restore events (originally bug 476161)</title>
+
+<script>
+
+document.addEventListener("input", function(aEvent) {
+ var inputEl = aEvent.originalTarget;
+ var changedEl = document.getElementById("inputFired");
+ changedEl.textContent += " " + inputEl.id;
+}, false);
+
+document.addEventListener("change", function(aEvent) {
+ var inputEl = aEvent.originalTarget;
+ var changedEl = document.getElementById("changeFired");
+ changedEl.textContent += " " + inputEl.id;
+}, false);
+
+</script>
+
+<!-- input events -->
+<h3>Text fields with changed text</h3>
+<input type="text" id="modify1">
+<input type="text" id="modify2" value="preset value">
+<input type="text" id="modify01">
+<input type="text" id="modify02" value="preset value">
+
+<h3>Text fields with unchanged text</h3>
+<input type="text" id="unchanged1">
+<input type="text" id="unchanged2" value="preset value">
+<input type="text" id="unchanged01">
+<input type="text" id="unchanged02" value="preset value">
+
+<h3>Textarea with changed text</h3>
+<textarea id="modify03"></textarea>
+<textarea id="modify04">preset value</textarea>
+
+<h3>Textarea with unchanged text</h3>
+<textarea id="unchanged03"></textarea>
+<textarea id="unchanged04">preset value</textarea>
+
+<h3>file field with changed value</h3>
+<input type="file" id="modify05">
+
+<h3>file field with unchanged value</h3>
+<input type="file" id="unchanged05">
+
+<!-- change events -->
+
+<h3>Select menu with changed selection</h3>
+<select id="modify06">
+ <option value="one">one</option>
+ <option value="two">two</option>
+ <option value="three">three</option>
+</select>
+
+<h3>Select menu with unchanged selection (change event still fires)</h3>
+<select id="unchanged06">
+ <option value="one">one</option>
+ <option value="two" selected>two</option>
+ <option value="three">three</option>
+</select>
+
+<h3>Multiple Select menu with changed selection</h3>
+<select id="modify07" multiple>
+ <option value="one">one</option>
+ <option value="two" selected>two</option>
+ <option value="three">three</option>
+</select>
+
+<h3>Select menu with unchanged selection</h3>
+<select id="unchanged07" multiple>
+ <option value="one">one</option>
+ <option value="two" selected>two</option>
+ <option value="three" selected>three</option>
+</select>
+
+<h3>checkbox with changed value</h3>
+<input type="checkbox" id="modify08">
+<input type="checkbox" id="modify09" checked>
+
+<h3>checkbox with unchanged value</h3>
+<input type="checkbox" id="unchanged08">
+<input type="checkbox" id="unchanged09" checked>
+
+<h3>radio with changed value</h3>
+<input type="radio" id="modify10" name="group">Radio 1</input>
+<input type="radio" id="modify11" name="group">Radio 2</input>
+<input type="radio" id="modify12" name="group" checked>Radio 3</input>
+
+<h3>radio with unchanged value</h3>
+<input type="radio" id="unchanged10" name="group2">Radio 4</input>
+<input type="radio" id="unchanged11" name="group2">Radio 5</input>
+<input type="radio" id="unchanged12" name="group2" checked>Radio 6</input>
+
+<h3>Changed field IDs</h3>
+<div id="changed"></div>
+<div id="inputFired"></div>
+<div id="changeFired"></div>
diff --git a/browser/components/sessionstore/test/browser_formdata.js b/browser/components/sessionstore/test/browser_formdata.js
new file mode 100644
index 000000000..ce1272888
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+/**
+ * This test ensures that form data collection respects the privacy level as
+ * set by the user.
+ */
+add_task(function test_formdata() {
+ const URL = "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_formdata_sample.html";
+
+ const OUTER_VALUE = "browser_formdata_" + Math.random();
+ const INNER_VALUE = "browser_formdata_" + Math.random();
+
+ // Creates a tab, loads a page with some form fields,
+ // modifies their values and closes the tab.
+ function createAndRemoveTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Modify form data.
+ yield setInputValue(browser, {id: "txt", value: OUTER_VALUE});
+ yield setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
+
+ // Remove the tab.
+ yield promiseRemoveTab(tab);
+ });
+ }
+
+ yield createAndRemoveTab();
+ let [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
+
+ // Disable saving data for encrypted sites.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
+
+ yield createAndRemoveTab();
+ [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ ok(!formdata.children, "inner value was *not* stored");
+
+ // Disable saving data for any site.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ yield createAndRemoveTab();
+ [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
+ ok(!formdata, "form data has *not* been stored");
+
+ // Restore the default privacy level.
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+});
+
+/**
+ * This test ensures that a malicious website can't trick us into restoring
+ * form data into a wrong website and that we always check the stored URL
+ * before doing so.
+ */
+add_task(function test_url_check() {
+ const URL = "data:text/html;charset=utf-8,<input%20id=input>";
+ const VALUE = "value-" + Math.random();
+
+ // Create a tab with an iframe containing an input field.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Restore a tab state with a given form data url.
+ function restoreStateWithURL(url) {
+ let state = {entries: [{url: URL}], formdata: {id: {input: VALUE}}};
+
+ if (url) {
+ state.formdata.url = url;
+ }
+
+ return promiseTabState(tab, state).then(() => getInputValue(browser, "input"));
+ }
+
+ // Check that the form value is restored with the correct URL.
+ is((yield restoreStateWithURL(URL)), VALUE, "form data restored");
+
+ // Check that the form value is *not* restored with the wrong URL.
+ is((yield restoreStateWithURL(URL + "?")), "", "form data not restored");
+ is((yield restoreStateWithURL()), "", "form data not restored");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that collecting form data works as expected when having
+ * nested frame sets.
+ */
+add_task(function test_nested() {
+ const URL = "data:text/html;charset=utf-8," +
+ "<iframe src='data:text/html;charset=utf-8," +
+ "<input autofocus=true>'/>";
+
+ const FORM_DATA = {
+ children: [{
+ xpath: {"/xhtml:html/xhtml:body/xhtml:input": "M"},
+ url: "data:text/html;charset=utf-8,<input%20autofocus=true>"
+ }]
+ };
+
+ // Create a tab with an iframe containing an input field.
+ let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Modify the input field's value.
+ yield sendMessage(browser, "ss-test:sendKeyEvent", {key: "m", frame: 0});
+
+ // Remove the tab and check that we stored form data correctly.
+ yield promiseRemoveTab(tab);
+ let [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
+ is(JSON.stringify(formdata), JSON.stringify(FORM_DATA),
+ "formdata for iframe stored correctly");
+
+ // Restore the closed tab.
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ yield promiseTabRestored(tab);
+
+ // Check that the input field has the right value.
+ yield TabStateFlusher.flush(browser);
+ ({formdata} = JSON.parse(ss.getTabState(tab)));
+ is(JSON.stringify(formdata), JSON.stringify(FORM_DATA),
+ "formdata for iframe restored correctly");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that collecting form data for documents with
+ * designMode=on works as expected.
+ */
+add_task(function test_design_mode() {
+ const URL = "data:text/html;charset=utf-8,<h1>mozilla</h1>" +
+ "<script>document.designMode='on'</script>";
+
+ // Load a tab with an editable document.
+ let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Modify the document content.
+ yield sendMessage(browser, "ss-test:sendKeyEvent", {key: "m"});
+
+ // Close and restore the tab.
+ yield promiseRemoveTab(tab);
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ yield promiseTabRestored(tab);
+
+ // Check that the innerHTML value was restored.
+ let html = yield getInnerHTML(browser);
+ let expected = "<h1>Mmozilla</h1><script>document.designMode='on'</script>";
+ is(html, expected, "editable document has been restored correctly");
+
+ // Close and restore the tab.
+ yield promiseRemoveTab(tab);
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ yield promiseTabRestored(tab);
+
+ // Check that the innerHTML value was restored.
+ html = yield getInnerHTML(browser);
+ expected = "<h1>Mmozilla</h1><script>document.designMode='on'</script>";
+ is(html, expected, "editable document has been restored correctly");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+function getInputValue(browser, id) {
+ return sendMessage(browser, "ss-test:getInputValue", {id: id});
+}
+
+function setInputValue(browser, data) {
+ return sendMessage(browser, "ss-test:setInputValue", data);
+}
+
+function getInnerHTML(browser) {
+ return sendMessage(browser, "ss-test:getInnerHTML", {selector: "body"});
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_cc.js b/browser/components/sessionstore/test/browser_formdata_cc.js
new file mode 100644
index 000000000..6e27ca970
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_cc.js
@@ -0,0 +1,79 @@
+"use strict";
+
+const URL = "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_formdata_sample.html";
+
+requestLongerTimeout(3);
+
+/**
+ * This test ensures that credit card numbers in form data will not be
+ * collected, while numbers that don't look like credit card numbers will
+ * still be collected.
+ */
+add_task(function* () {
+ const validCCNumbers = [
+ // 15 digits
+ "930771457288760", "474915027480942",
+ "924894781317325", "714816113937185",
+ "790466087343106", "474320195408363",
+ "219211148122351", "633038472250799",
+ "354236732906484", "095347810189325",
+ // 16 digits
+ "3091269135815020", "5471839082338112",
+ "0580828863575793", "5015290610002932",
+ "9465714503078607", "4302068493801686",
+ "2721398408985465", "6160334316984331",
+ "8643619970075142", "0218246069710785"
+ ];
+
+ const invalidCCNumbers = [
+ // 15 digits
+ "526931005800649", "724952425140686",
+ "379761391174135", "030551436468583",
+ "947377014076746", "254848023655752",
+ "226871580283345", "708025346034339",
+ "917585839076788", "918632588027666",
+ // 16 digits
+ "9946177098017064", "4081194386488872",
+ "3095975979578034", "3662215692222536",
+ "6723210018630429", "4411962856225025",
+ "8276996369036686", "4449796938248871",
+ "3350852696538147", "5011802870046957"
+ ];
+
+ // Creates a tab, loads a page with a form field, sets the value of the
+ // field, and then removes the tab to trigger data collection.
+ function* createAndRemoveTab(formValue) {
+ // Create a new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Set form value.
+ yield setInputValue(browser, formValue);
+
+ // Remove the tab.
+ yield promiseRemoveTab(tab);
+ }
+
+ // Test that valid CC numbers are not collected.
+ for (let number of validCCNumbers) {
+ yield createAndRemoveTab(number);
+ let [{state}] = JSON.parse(ss.getClosedTabData(window));
+ ok(!("formdata" in state), "valid CC numbers are not collected");
+ }
+
+ // Test that non-CC numbers are still collected.
+ for (let number of invalidCCNumbers) {
+ yield createAndRemoveTab(number);
+ let [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
+ is(formdata.id.txt, number,
+ "numbers that are not valid CC numbers are still collected");
+ }
+});
+
+function setInputValue(browser, formValue) {
+ return ContentTask.spawn(browser, formValue, function* (formValue) {
+ content.document.getElementById("txt").setUserInput(formValue);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_format.js b/browser/components/sessionstore/test/browser_formdata_format.js
new file mode 100644
index 000000000..6a1b5975d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_format.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ /** Tests formdata format **/
+ waitForExplicitFinish();
+
+ let formData = [
+ { },
+ // old format
+ { "#input1" : "value0" },
+ { "#input1" : "value1", "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value2" },
+ { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value3" },
+ // new format
+ { id: { "input1" : "value4" } },
+ { id: { "input1" : "value5" }, xpath: {} },
+ { id: { "input1" : "value6" }, xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value7" } },
+ { id: {}, xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value8" } },
+ { xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value9" } },
+ // combinations
+ { "#input1" : "value10", id: { "input1" : "value11" } },
+ { "#input1" : "value12", id: { "input1" : "value13" }, xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value14" } },
+ { "#input1" : "value15", xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value16" } },
+ { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value17", id: { "input1" : "value18" } },
+ { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value19", id: { "input1" : "value20" }, xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value21" } },
+ { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value22", xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value23" } },
+ { "#input1" : "value24", "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value25", id: { "input1" : "value26" } },
+ { "#input1" : "value27", "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value28", id: { "input1" : "value29" }, xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value30" } },
+ { "#input1" : "value31", "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value32", xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value33" } }
+ ]
+ let expectedValues = [
+ [ "" , "" ],
+ // old format
+ [ "value0", "" ],
+ [ "value1", "value2" ],
+ [ "", "value3" ],
+ // new format
+ [ "value4", "" ],
+ [ "value5", "" ],
+ [ "value6", "value7" ],
+ [ "", "value8" ],
+ [ "", "value9" ],
+ // combinations
+ [ "value11", "" ],
+ [ "value13", "value14" ],
+ [ "", "value16" ],
+ [ "value18", "" ],
+ [ "value20", "value21" ],
+ [ "", "value23" ],
+ [ "value26", "" ],
+ [ "value29", "value30" ],
+ [ "", "value33" ]
+ ];
+ let testTabCount = 0;
+ let callback = function() {
+ testTabCount--;
+ if (testTabCount == 0) {
+ finish();
+ }
+ };
+
+ for (let i = 0; i < formData.length; i++) {
+ testTabCount++;
+ testTabRestoreData(formData[i], expectedValues[i], callback);
+ }
+}
+
+function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
+ let URL = ROOT + "browser_formdata_format_sample.html";
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+
+ aFormData.url = URL;
+ let tabState = { entries: [{ url: URL }], formdata: aFormData };
+
+ Task.spawn(function () {
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield promiseTabState(tab, tabState);
+
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ let restoredTabState = JSON.parse(ss.getTabState(tab));
+ let restoredFormData = restoredTabState.formdata;
+
+ if (restoredFormData) {
+ let doc = tab.linkedBrowser.contentDocument;
+ let input1 = doc.getElementById("input1");
+ let input2 = doc.querySelector("input[name=input2]");
+
+ // test format
+ ok("id" in restoredFormData || "xpath" in restoredFormData,
+ "FormData format is valid: " + restoredFormData);
+ // validate that there are no old keys
+ for (let key of Object.keys(restoredFormData)) {
+ if (["id", "xpath", "url"].indexOf(key) === -1) {
+ ok(false, "FormData format is invalid.");
+ }
+ }
+ // test id
+ is(input1.value, aExpectedValue[0],
+ "FormData by 'id' has been restored correctly");
+ // test xpath
+ is(input2.value, aExpectedValue[1],
+ "FormData by 'xpath' has been restored correctly");
+ }
+
+ // clean up
+ gBrowser.removeTab(tab);
+
+ // This test might time out if the task fails.
+ }).then(aCallback);
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_format_sample.html b/browser/components/sessionstore/test/browser_formdata_format_sample.html
new file mode 100644
index 000000000..f991e3657
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_format_sample.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<title>Test formdata format</title>
+
+<!-- input events -->
+<h3>Input fields</h3>
+<input type="text" id="input1">
+<input type="text" name="input2"> \ No newline at end of file
diff --git a/browser/components/sessionstore/test/browser_formdata_sample.html b/browser/components/sessionstore/test/browser_formdata_sample.html
new file mode 100644
index 000000000..6cbb54fb5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_sample.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_formdata_sample.html</title>
+ </head>
+ <body>
+ <input id="txt" />
+
+ <script type="text/javascript;version=1.8">
+ let isOuter = window == window.top;
+
+ if (isOuter) {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "https://example.com" + location.pathname);
+ document.body.appendChild(iframe);
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_formdata_xpath.js b/browser/components/sessionstore/test/browser_formdata_xpath.js
new file mode 100644
index 000000000..d69feb546
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_xpath.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_formdata_xpath_sample.html";
+
+/**
+ * Bug 346337 - Generic form data restoration tests.
+ */
+add_task(function setup() {
+ // make sure we don't save form data at all (except for tab duplication)
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+ });
+});
+
+const FILE1 = createFilePath("346337_test1.file");
+const FILE2 = createFilePath("346337_test2.file");
+
+const FIELDS = {
+ "//input[@name='input']": Date.now().toString(),
+ "//input[@name='spaced 1']": Math.random().toString(),
+ "//input[3]": "three",
+ "//input[@type='checkbox']": true,
+ "//input[@name='uncheck']": false,
+ "//input[@type='radio'][1]": false,
+ "//input[@type='radio'][2]": true,
+ "//input[@type='radio'][3]": false,
+ "//select": 2,
+ "//select[@multiple]": [1, 3],
+ "//textarea[1]": "",
+ "//textarea[2]": "Some text... " + Math.random(),
+ "//textarea[3]": "Some more text\n" + new Date(),
+ "//input[@type='file'][1]": [FILE1],
+ "//input[@type='file'][2]": [FILE1, FILE2]
+};
+
+add_task(function test_form_data_restoration() {
+ // Load page with some input fields.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Fill in some values.
+ for (let xpath of Object.keys(FIELDS)) {
+ yield setFormValue(browser, xpath);
+ }
+
+ // Duplicate the tab.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2);
+
+ // Check that all form values have been duplicated.
+ for (let xpath of Object.keys(FIELDS)) {
+ let expected = JSON.stringify(FIELDS[xpath]);
+ let actual = JSON.stringify(yield getFormValue(browser2, xpath));
+ is(actual, expected, "The value for \"" + xpath + "\" was correctly restored");
+ }
+
+ // Remove all tabs.
+ yield promiseRemoveTab(tab2);
+ yield promiseRemoveTab(tab);
+
+ // Restore one of the tabs again.
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ yield promiseTabRestored(tab);
+
+ // Check that none of the form values have been restored due to the privacy
+ // level settings.
+ for (let xpath of Object.keys(FIELDS)) {
+ let expected = FIELDS[xpath];
+ if (expected) {
+ let actual = yield getFormValue(browser, xpath, expected);
+ isnot(actual, expected, "The value for \"" + xpath + "\" was correctly discarded");
+ }
+ }
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+});
+
+function createFilePath(leaf) {
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(leaf);
+ return file.path;
+}
+
+function isArrayOfNumbers(value) {
+ return Array.isArray(value) && value.every(n => typeof(n) === "number");
+}
+
+function isArrayOfStrings(value) {
+ return Array.isArray(value) && value.every(n => typeof(n) === "string");
+}
+
+function getFormValue(browser, xpath) {
+ let value = FIELDS[xpath];
+
+ if (typeof value == "string") {
+ return getInputValue(browser, {xpath: xpath});
+ }
+
+ if (typeof value == "boolean") {
+ return getInputChecked(browser, {xpath: xpath});
+ }
+
+ if (typeof value == "number") {
+ return getSelectedIndex(browser, {xpath: xpath});
+ }
+
+ if (isArrayOfNumbers(value)) {
+ return getMultipleSelected(browser, {xpath: xpath});
+ }
+
+ if (isArrayOfStrings(value)) {
+ return getFileNameArray(browser, {xpath: xpath});
+ }
+
+ throw new Error("unknown input type");
+}
+
+function setFormValue(browser, xpath) {
+ let value = FIELDS[xpath];
+
+ if (typeof value == "string") {
+ return setInputValue(browser, {xpath: xpath, value: value});
+ }
+
+ if (typeof value == "boolean") {
+ return setInputChecked(browser, {xpath: xpath, checked: value});
+ }
+
+ if (typeof value == "number") {
+ return setSelectedIndex(browser, {xpath: xpath, index: value});
+ }
+
+ if (isArrayOfNumbers(value)) {
+ return setMultipleSelected(browser, {xpath: xpath, indices: value});
+ }
+
+ if (isArrayOfStrings(value)) {
+ return setFileNameArray(browser, {xpath: xpath, names: value});
+ }
+
+ throw new Error("unknown input type");
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_xpath_sample.html b/browser/components/sessionstore/test/browser_formdata_xpath_sample.html
new file mode 100644
index 000000000..682162d6a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_xpath_sample.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<title>Test for bug 346337</title>
+
+<h3>Text Fields</h3>
+<input type="text" name="input">
+<input type="text" name="spaced 1">
+<input>
+
+<h3>Checkboxes and Radio buttons</h3>
+<input type="checkbox" name="check"> Check 1
+<input type="checkbox" name="uncheck" checked> Check 2
+<p>
+<input type="radio" name="group" value="1"> Radio 1
+<input type="radio" name="group" value="some"> Radio 2
+<input type="radio" name="group" checked> Radio 3
+
+<h3>Selects</h3>
+<select name="any">
+ <option value="1"> Select 1
+ <option value="some"> Select 2
+ <option>Select 3
+</select>
+<select multiple="multiple">
+ <option value=1> Multi-select 1
+ <option value=2> Multi-select 2
+ <option value=3> Multi-select 3
+ <option value=4> Multi-select 4
+</select>
+
+<h3>Text Areas</h3>
+<textarea name="testarea"></textarea>
+<textarea name="sized one" rows="5" cols="25"></textarea>
+<textarea></textarea>
+
+<h3>File Selector</h3>
+<input type="file">
+<input type="file" multiple>
diff --git a/browser/components/sessionstore/test/browser_frame_history.js b/browser/components/sessionstore/test/browser_frame_history.js
new file mode 100644
index 000000000..e0d152f77
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history.js
@@ -0,0 +1,170 @@
+/* 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/. */
+
+/**
+ Ensure that frameset history works properly when restoring a tab,
+ provided that the frameset is static.
+ */
+
+// Loading a toplevel frameset
+add_task(function() {
+ let testURL = getRootDirectory(gTestPath) + "browser_frame_history_index.html";
+ let tab = gBrowser.addTab(testURL);
+ gBrowser.selectedTab = tab;
+
+ info("Opening a page with three frames, 4 loads should take place");
+ yield waitForLoadsInBrowser(tab.linkedBrowser, 4);
+
+ let browser_b = tab.linkedBrowser.contentDocument.getElementsByTagName("frame")[1];
+ let document_b = browser_b.contentDocument;
+ let links = document_b.getElementsByTagName("a");
+
+ // We're going to click on the first link, so listen for another load event
+ info("Clicking on link 1, 1 load should take place");
+ let promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
+ EventUtils.sendMouseEvent({type:"click"}, links[0], browser_b.contentWindow);
+ yield promise;
+
+ info("Clicking on link 2, 1 load should take place");
+ promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
+ EventUtils.sendMouseEvent({type:"click"}, links[1], browser_b.contentWindow);
+ yield promise;
+
+ info("Close then un-close page, 4 loads should take place");
+ yield promiseRemoveTab(tab);
+ let newTab = ss.undoCloseTab(window, 0);
+ yield waitForLoadsInBrowser(newTab.linkedBrowser, 4);
+
+ info("Go back in time, 1 load should take place");
+ gBrowser.goBack();
+ yield waitForLoadsInBrowser(newTab.linkedBrowser, 1);
+
+ let expectedURLEnds = ["a.html", "b.html", "c1.html"];
+ let frames = newTab.linkedBrowser.contentDocument.getElementsByTagName("frame");
+ for (let i = 0; i < frames.length; i++) {
+ is(frames[i].contentDocument.location,
+ getRootDirectory(gTestPath) + "browser_frame_history_" + expectedURLEnds[i],
+ "frame " + i + " has the right url");
+ }
+ gBrowser.removeTab(newTab);
+});
+
+// Loading the frameset inside an iframe
+add_task(function() {
+ let testURL = getRootDirectory(gTestPath) + "browser_frame_history_index2.html";
+ let tab = gBrowser.addTab(testURL);
+ gBrowser.selectedTab = tab;
+
+ info("iframe: Opening a page with an iframe containing three frames, 5 loads should take place");
+ yield waitForLoadsInBrowser(tab.linkedBrowser, 5);
+
+ let browser_b = tab.linkedBrowser.contentDocument.
+ getElementById("iframe").contentDocument.
+ getElementsByTagName("frame")[1];
+ let document_b = browser_b.contentDocument;
+ let links = document_b.getElementsByTagName("a");
+
+ // We're going to click on the first link, so listen for another load event
+ info("iframe: Clicking on link 1, 1 load should take place");
+ let promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
+ EventUtils.sendMouseEvent({type:"click"}, links[0], browser_b.contentWindow);
+ yield promise;
+
+ info("iframe: Clicking on link 2, 1 load should take place");
+ promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
+ EventUtils.sendMouseEvent({type:"click"}, links[1], browser_b.contentWindow);
+ yield promise;
+
+ info("iframe: Close then un-close page, 5 loads should take place");
+ yield promiseRemoveTab(tab);
+ let newTab = ss.undoCloseTab(window, 0);
+ yield waitForLoadsInBrowser(newTab.linkedBrowser, 5);
+
+ info("iframe: Go back in time, 1 load should take place");
+ gBrowser.goBack();
+ yield waitForLoadsInBrowser(newTab.linkedBrowser, 1);
+
+ let expectedURLEnds = ["a.html", "b.html", "c1.html"];
+ let frames = newTab.linkedBrowser.contentDocument.
+ getElementById("iframe").contentDocument.
+ getElementsByTagName("frame");
+ for (let i = 0; i < frames.length; i++) {
+ is(frames[i].contentDocument.location,
+ getRootDirectory(gTestPath) + "browser_frame_history_" + expectedURLEnds[i],
+ "frame " + i + " has the right url");
+ }
+ gBrowser.removeTab(newTab);
+});
+
+// Now, test that we don't record history if the iframe is added dynamically
+add_task(function() {
+ // Start with an empty history
+ let blankState = JSON.stringify({
+ windows: [{
+ tabs: [{ entries: [{ url: "about:blank" }] }],
+ _closedTabs: []
+ }],
+ _closedWindows: []
+ });
+ ss.setBrowserState(blankState);
+
+ let testURL = getRootDirectory(gTestPath) + "browser_frame_history_index_blank.html";
+ let tab = gBrowser.addTab(testURL);
+ gBrowser.selectedTab = tab;
+ yield waitForLoadsInBrowser(tab.linkedBrowser, 1);
+
+ info("dynamic: Opening a page with an iframe containing three frames, 4 dynamic loads should take place");
+ let doc = tab.linkedBrowser.contentDocument;
+ let iframe = doc.createElement("iframe");
+ iframe.id = "iframe";
+ iframe.src="browser_frame_history_index.html";
+ doc.body.appendChild(iframe);
+ yield waitForLoadsInBrowser(tab.linkedBrowser, 4);
+
+ let browser_b = tab.linkedBrowser.contentDocument.
+ getElementById("iframe").contentDocument.
+ getElementsByTagName("frame")[1];
+ let document_b = browser_b.contentDocument;
+ let links = document_b.getElementsByTagName("a");
+
+ // We're going to click on the first link, so listen for another load event
+ info("dynamic: Clicking on link 1, 1 load should take place");
+ let promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
+ EventUtils.sendMouseEvent({type:"click"}, links[0], browser_b.contentWindow);
+ yield promise;
+
+ info("dynamic: Clicking on link 2, 1 load should take place");
+ promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
+ EventUtils.sendMouseEvent({type:"click"}, links[1], browser_b.contentWindow);
+ yield promise;
+
+ info("Check in the state that we have not stored this history");
+ let state = ss.getBrowserState();
+ info(JSON.stringify(JSON.parse(state), null, "\t"));
+ is(state.indexOf("c1.html"), -1, "History entry was not stored in the session state");;
+ gBrowser.removeTab(tab);
+});
+
+// helper functions
+function waitForLoadsInBrowser(aBrowser, aLoadCount) {
+ let deferred = Promise.defer();
+ let loadCount = 0;
+ aBrowser.addEventListener("load", function(aEvent) {
+ if (++loadCount < aLoadCount) {
+ info("Got " + loadCount + " loads, waiting until we have " + aLoadCount);
+ return;
+ }
+
+ aBrowser.removeEventListener("load", arguments.callee, true);
+ deferred.resolve();
+ }, true);
+ return deferred.promise;
+}
+
+function timeout(delay, task) {
+ let deferred = Promise.defer();
+ setTimeout(() => deferred.resolve(true), delay);
+ task.then(() => deferred.resolve(false), deferred.reject);
+ return deferred.promise;
+}
diff --git a/browser/components/sessionstore/test/browser_frame_history_a.html b/browser/components/sessionstore/test/browser_frame_history_a.html
new file mode 100755
index 000000000..8e7b35d7a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_a.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm A!
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frame_history_b.html b/browser/components/sessionstore/test/browser_frame_history_b.html
new file mode 100755
index 000000000..38b43da21
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_b.html
@@ -0,0 +1,10 @@
+<html>
+ <body>
+ I'm B!<br/>
+ <a target="c" href="browser_frame_history_c1.html">click me first</a><br/>
+ <a target="c" href="browser_frame_history_c2.html">then click me</a><br/>
+ Close this tab.<br/>
+ Restore this tab.<br/>
+ Click back.<br/>
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frame_history_c.html b/browser/components/sessionstore/test/browser_frame_history_c.html
new file mode 100755
index 000000000..0efd7d902
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_c.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm C!
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frame_history_c1.html b/browser/components/sessionstore/test/browser_frame_history_c1.html
new file mode 100755
index 000000000..b55c1d45a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_c1.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm C1!
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frame_history_c2.html b/browser/components/sessionstore/test/browser_frame_history_c2.html
new file mode 100755
index 000000000..aec504141
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_c2.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ I'm C2!
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frame_history_index.html b/browser/components/sessionstore/test/browser_frame_history_index.html
new file mode 100644
index 000000000..76eeb4c4d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_index.html
@@ -0,0 +1,10 @@
+<html>
+ <frameset cols="20%,80%">
+ <frameset rows="30%,70%">
+ <frame src="browser_frame_history_a.html"/>
+ <frame src="browser_frame_history_b.html"/>
+ </frameset>
+ <frame src="browser_frame_history_c.html" name="c"/>
+ </frameset>
+</html>
+
diff --git a/browser/components/sessionstore/test/browser_frame_history_index2.html b/browser/components/sessionstore/test/browser_frame_history_index2.html
new file mode 100644
index 000000000..e4dfb4083
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_index2.html
@@ -0,0 +1,4 @@
+<html>
+ <iframe src="browser_frame_history_index.html" id="iframe" />
+</html>
+
diff --git a/browser/components/sessionstore/test/browser_frame_history_index_blank.html b/browser/components/sessionstore/test/browser_frame_history_index_blank.html
new file mode 100644
index 000000000..30fd1f58c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_index_blank.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ </body>
+</html>
+
diff --git a/browser/components/sessionstore/test/browser_frametree.js b/browser/components/sessionstore/test/browser_frametree.js
new file mode 100644
index 000000000..a342f8c66
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frametree.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = HTTPROOT + "browser_frametree_sample.html";
+const URL_FRAMESET = HTTPROOT + "browser_frametree_sample_frameset.html";
+
+/**
+ * This ensures that loading a page normally, aborting a page load, reloading
+ * a page, navigating using the bfcache, and ignoring frames that were
+ * created dynamically work as expect. We expect the frame tree to be reset
+ * when a page starts loading and we also expect a valid frame tree to exist
+ * when it has stopped loading.
+ */
+add_task(function test_frametree() {
+ const FRAME_TREE_SINGLE = { href: URL };
+ const FRAME_TREE_FRAMESET = {
+ href: URL_FRAMESET,
+ children: [{href: URL}, {href: URL}, {href: URL}]
+ };
+
+ // Create a tab with a single frame.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseNewFrameTree(browser);
+ yield checkFrameTree(browser, FRAME_TREE_SINGLE,
+ "loading a page resets and creates the frame tree correctly");
+
+ // Load the frameset and create two frames dynamically, the first on
+ // DOMContentLoaded and the second on load.
+ yield sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
+ browser.loadURI(URL_FRAMESET);
+ yield promiseNewFrameTree(browser);
+ yield checkFrameTree(browser, FRAME_TREE_FRAMESET,
+ "dynamic frames created on or after the load event are ignored");
+
+ // Go back to the previous single-frame page. There will be no load event as
+ // the page is still in the bfcache. We thus make sure this type of navigation
+ // resets the frame tree.
+ browser.goBack();
+ yield promiseNewFrameTree(browser);
+ yield checkFrameTree(browser, FRAME_TREE_SINGLE,
+ "loading from bfache resets and creates the frame tree correctly");
+
+ // Load the frameset again but abort the load early.
+ // The frame tree should still be reset and created.
+ browser.loadURI(URL_FRAMESET);
+ executeSoon(() => browser.stop());
+ yield promiseNewFrameTree(browser);
+
+ // Load the frameset and check the tree again.
+ yield sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
+ browser.loadURI(URL_FRAMESET);
+ yield promiseNewFrameTree(browser);
+ yield checkFrameTree(browser, FRAME_TREE_FRAMESET,
+ "reloading a page resets and creates the frame tree correctly");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that we ignore frames that were created dynamically at or
+ * after the load event. SessionStore can't handle these and will not restore
+ * or collect any data for them.
+ */
+add_task(function test_frametree_dynamic() {
+ // The frame tree as expected. The first two frames are static
+ // and the third one was created on DOMContentLoaded.
+ const FRAME_TREE = {
+ href: URL_FRAMESET,
+ children: [{href: URL}, {href: URL}, {href: URL}]
+ };
+ const FRAME_TREE_REMOVED = {
+ href: URL_FRAMESET,
+ children: [{href: URL}, {href: URL}]
+ };
+
+ // Add an empty tab for a start.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Create dynamic frames on "DOMContentLoaded" and on "load".
+ yield sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
+ browser.loadURI(URL_FRAMESET);
+ yield promiseNewFrameTree(browser);
+
+ // Check that the frame tree does not contain the frame created on "load".
+ // The two static frames and the one created on DOMContentLoaded must be in
+ // the tree.
+ yield checkFrameTree(browser, FRAME_TREE,
+ "frame tree contains first four frames");
+
+ // Remove the last frame in the frameset.
+ yield sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
+ // Check that the frame tree didn't change.
+ yield checkFrameTree(browser, FRAME_TREE,
+ "frame tree contains first four frames");
+
+ // Remove the last frame in the frameset.
+ yield sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
+ // Check that the frame tree excludes the removed frame.
+ yield checkFrameTree(browser, FRAME_TREE_REMOVED,
+ "frame tree contains first three frames");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Checks whether the current frame hierarchy of a given |browser| matches the
+ * |expected| frame hierarchy.
+ */
+function checkFrameTree(browser, expected, msg) {
+ return sendMessage(browser, "ss-test:mapFrameTree").then(tree => {
+ is(JSON.stringify(tree), JSON.stringify(expected), msg);
+ });
+}
+
+/**
+ * Returns a promise that will be resolved when the given |browser| has loaded
+ * and we received messages saying that its frame tree has been reset and
+ * recollected.
+ */
+function promiseNewFrameTree(browser) {
+ let reset = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
+ let collect = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
+ return Promise.all([reset, collect]);
+}
diff --git a/browser/components/sessionstore/test/browser_frametree_sample.html b/browser/components/sessionstore/test/browser_frametree_sample.html
new file mode 100644
index 000000000..dda129448
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frametree_sample.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_frametree_sample.html</title>
+ </head>
+ <body style='width: 100000px; height: 100000px;'>top</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frametree_sample_frameset.html b/browser/components/sessionstore/test/browser_frametree_sample_frameset.html
new file mode 100644
index 000000000..e1cd08735
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frametree_sample_frameset.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_frametree_sample_frameset.html</title>
+ </head>
+ <frameset id="frames" rows="50%, 50%">
+ <frame src="browser_frametree_sample.html">
+ <frame src="browser_frametree_sample.html">
+ </frameset>
+</html>
diff --git a/browser/components/sessionstore/test/browser_global_store.js b/browser/components/sessionstore/test/browser_global_store.js
new file mode 100644
index 000000000..792154830
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_global_store.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the API for saving global session data.
+add_task(function* () {
+ const key1 = "Unique name 1: " + Date.now();
+ const key2 = "Unique name 2: " + Date.now();
+ const value1 = "Unique value 1: " + Math.random();
+ const value2 = "Unique value 2: " + Math.random();
+
+ let global = {};
+ global[key1] = value1;
+
+ const testState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank" }] },
+ ]
+ }
+ ],
+ global: global
+ };
+
+ function testRestoredState() {
+ is(ss.getGlobalValue(key1), value1, "restored state has global value");
+ }
+
+ function testGlobalStore() {
+ is(ss.getGlobalValue(key2), "", "global value initially not set");
+
+ ss.setGlobalValue(key2, value1);
+ is(ss.getGlobalValue(key2), value1, "retreived value matches stored");
+
+ ss.setGlobalValue(key2, value2);
+ is(ss.getGlobalValue(key2), value2, "previously stored value was overwritten");
+
+ ss.deleteGlobalValue(key2);
+ is(ss.getGlobalValue(key2), "", "global value was deleted");
+ }
+
+ yield promiseBrowserState(testState);
+ testRestoredState();
+ testGlobalStore();
+});
diff --git a/browser/components/sessionstore/test/browser_history_persist.js b/browser/components/sessionstore/test/browser_history_persist.js
new file mode 100644
index 000000000..6b9e62abc
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_history_persist.js
@@ -0,0 +1,93 @@
+/* eslint-env mozilla/frame-script */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that history entries that should not be persisted are restored in the
+ * same state.
+ */
+add_task(function check_history_not_persisted() {
+ // Create an about:blank tab
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Retrieve the tab state.
+ yield TabStateFlusher.flush(browser);
+ let state = JSON.parse(ss.getTabState(tab));
+ ok(!state.entries[0].persist, "Should have collected the persistence state");
+ yield promiseRemoveTab(tab);
+ browser = null;
+
+ // Open a new tab to restore into.
+ tab = gBrowser.addTab("about:blank");
+ browser = tab.linkedBrowser;
+ yield promiseTabState(tab, state);
+
+ yield ContentTask.spawn(browser, null, function() {
+ let sessionHistory = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+
+ is(sessionHistory.count, 1, "Should be a single history entry");
+ is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:blank", "Should be the right URL");
+ });
+
+ // Load a new URL into the tab, it should replace the about:blank history entry
+ browser.loadURI("about:robots");
+ yield promiseBrowserLoaded(browser);
+ yield ContentTask.spawn(browser, null, function() {
+ let sessionHistory = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+ is(sessionHistory.count, 1, "Should be a single history entry");
+ is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:robots", "Should be the right URL");
+ });
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+});
+
+/**
+ * Check that entries default to being persisted when the attribute doesn't
+ * exist
+ */
+add_task(function check_history_default_persisted() {
+ // Create an about:blank tab
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Retrieve the tab state.
+ yield TabStateFlusher.flush(browser);
+ let state = JSON.parse(ss.getTabState(tab));
+ delete state.entries[0].persist;
+ yield promiseRemoveTab(tab);
+ browser = null;
+
+ // Open a new tab to restore into.
+ tab = gBrowser.addTab("about:blank");
+ browser = tab.linkedBrowser;
+ yield promiseTabState(tab, state);
+ yield ContentTask.spawn(browser, null, function() {
+ let sessionHistory = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+
+ is(sessionHistory.count, 1, "Should be a single history entry");
+ is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:blank", "Should be the right URL");
+ });
+
+ // Load a new URL into the tab, it should replace the about:blank history entry
+ browser.loadURI("about:robots");
+ yield promiseBrowserLoaded(browser);
+ yield ContentTask.spawn(browser, null, function() {
+ let sessionHistory = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+ is(sessionHistory.count, 2, "Should be two history entries");
+ is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:blank", "Should be the right URL");
+ is(sessionHistory.getEntryAtIndex(1, false).URI.spec, "about:robots", "Should be the right URL");
+ });
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_label_and_icon.js b/browser/components/sessionstore/test/browser_label_and_icon.js
new file mode 100644
index 000000000..db68eb042
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_label_and_icon.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci} = Components;
+
+/**
+ * Make sure that tabs are restored on demand as otherwise the tab will start
+ * loading immediately and we can't check its icon and label.
+ */
+add_task(function setup() {
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+ });
+});
+
+/**
+ * Ensure that a pending tab has label and icon correctly set.
+ */
+add_task(function test_label_and_icon() {
+ // Create a new tab.
+ let tab = gBrowser.addTab("about:robots");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Retrieve the tab state.
+ yield TabStateFlusher.flush(browser);
+ let state = ss.getTabState(tab);
+ yield promiseRemoveTab(tab);
+ browser = null;
+
+ // Open a new tab to restore into.
+ tab = gBrowser.addTab("about:blank");
+ ss.setTabState(tab, state);
+ yield promiseTabRestoring(tab);
+
+ // Check that label and icon are set for the restoring tab.
+ ok(gBrowser.getIcon(tab).startsWith("data:image/png;"), "icon is set");
+ is(tab.label, "Gort! Klaatu barada nikto!", "label is set");
+
+ let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let serializedPrincipal = tab.getAttribute("iconLoadingPrincipal");
+ let iconLoadingPrincipal = serhelper.deserializeObject(serializedPrincipal)
+ .QueryInterface(Ci.nsIPrincipal);
+ is(iconLoadingPrincipal.origin, "about:robots", "correct loadingPrincipal used");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_merge_closed_tabs.js b/browser/components/sessionstore/test/browser_merge_closed_tabs.js
new file mode 100644
index 000000000..b26e86f22
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_merge_closed_tabs.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that closed tabs are merged when restoring
+ * a window state without overwriting tabs.
+ */
+add_task(function () {
+ const initialState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:blank" }] }
+ ],
+ _closedTabs: [
+ { state: { entries: [{ ID: 1000, url: "about:blank" }]} },
+ { state: { entries: [{ ID: 1001, url: "about:blank" }]} }
+ ]
+ }]
+ }
+
+ const restoreState = {
+ windows: [{
+ tabs: [
+ { entries: [{ url: "about:robots" }] }
+ ],
+ _closedTabs: [
+ { state: { entries: [{ ID: 1002, url: "about:robots" }]} },
+ { state: { entries: [{ ID: 1003, url: "about:robots" }]} },
+ { state: { entries: [{ ID: 1004, url: "about:robots" }]} }
+ ]
+ }]
+ }
+
+ const maxTabsUndo = 4;
+ gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", maxTabsUndo);
+
+ // Open a new window and restore it to an initial state.
+ let win = yield promiseNewWindowLoaded({private: false});
+ SessionStore.setWindowState(win, JSON.stringify(initialState), true);
+ is(SessionStore.getClosedTabCount(win), 2, "2 closed tabs after restoring initial state");
+
+ // Restore the new state but do not overwrite existing tabs (this should
+ // cause the closed tabs to be merged).
+ SessionStore.setWindowState(win, JSON.stringify(restoreState), false);
+
+ // Verify the windows closed tab data is correct.
+ let iClosed = initialState.windows[0]._closedTabs;
+ let rClosed = restoreState.windows[0]._closedTabs;
+ let cData = JSON.parse(SessionStore.getClosedTabData(win));
+
+ is(cData.length, Math.min(iClosed.length + rClosed.length, maxTabsUndo),
+ "Number of closed tabs is correct");
+
+ // When the closed tabs are merged the restored tabs are considered to be
+ // closed more recently.
+ for (let i = 0; i < cData.length; i++) {
+ if (i < rClosed.length) {
+ is(cData[i].state.entries[0].ID, rClosed[i].state.entries[0].ID,
+ "Closed tab entry matches");
+ } else {
+ is(cData[i].state.entries[0].ID, iClosed[i - rClosed.length].state.entries[0].ID,
+ "Closed tab entry matches");
+ }
+ }
+
+ // Clean up.
+ gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+
diff --git a/browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js b/browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js
new file mode 100644
index 000000000..fc958b293
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js
@@ -0,0 +1,36 @@
+"use strict";
+
+const PAGE_1 = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const PAGE_2 = "data:text/html,<html><body>Another%20regular,%20everyday,%20normal%20page.";
+
+add_task(function*() {
+ // Load an empty, non-remote tab at about:blank...
+ let tab = gBrowser.addTab("about:blank", {
+ forceNotRemote: true,
+ });
+ gBrowser.selectedTab = tab;
+ let browser = gBrowser.selectedBrowser;
+ ok(!browser.isRemoteBrowser, "Ensure browser is not remote");
+ // Load a remote page, and then another remote page immediately
+ // after.
+ browser.loadURI(PAGE_1);
+ browser.stop();
+ browser.loadURI(PAGE_2);
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ ok(browser.isRemoteBrowser, "Should have switched remoteness");
+ yield TabStateFlusher.flush(browser);
+ let state = JSON.parse(ss.getTabState(tab));
+ let entries = state.entries;
+ is(entries.length, 1, "There should only be one entry");
+ is(entries[0].url, PAGE_2, "Should have PAGE_2 as the sole history entry");
+ is(browser.currentURI.spec, PAGE_2, "Should have PAGE_2 as the browser currentURI");
+
+ yield ContentTask.spawn(browser, PAGE_2, function*(PAGE_2) {
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+ Assert.equal(docShell.currentURI.spec, PAGE_2,
+ "Content should have PAGE_2 as the browser currentURI");
+ });
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_newtab_userTypedValue.js b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js
new file mode 100644
index 000000000..66dc93380
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js
@@ -0,0 +1,72 @@
+"use strict";
+
+requestLongerTimeout(4);
+
+/**
+ * Test that when restoring an 'initial page' with session restore, it
+ * produces an empty URL bar, rather than leaving its URL explicitly
+ * there as a 'user typed value'.
+ */
+add_task(function* () {
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+ yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:logo");
+ let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab(win.gBrowser, () => {});
+
+ // This opens about:newtab:
+ win.BrowserOpenTab();
+ let tab = yield tabOpenedAndSwitchedTo;
+ is(win.gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+ let state = JSON.parse(SessionStore.getTabState(tab));
+ ok(!state.userTypedValue, "userTypedValue should be undefined on the tab's state");
+ tab = null;
+
+ yield BrowserTestUtils.closeWindow(win);
+
+ ok(SessionStore.getClosedWindowCount(), "Should have a closed window");
+
+ win = SessionStore.undoCloseWindow(0);
+ yield TestUtils.topicObserved("sessionstore-single-window-restored",
+ subject => subject == win);
+ // Don't wait for load here because it's about:newtab and we may have swapped in
+ // a preloaded browser.
+ yield TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+
+ is(win.gURLBar.value, "", "URL bar should be empty");
+ tab = win.gBrowser.selectedTab;
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+ state = JSON.parse(SessionStore.getTabState(tab));
+ ok(!state.userTypedValue, "userTypedValue should be undefined on the tab's state");
+
+ yield BrowserTestUtils.removeTab(tab);
+
+ for (let url of gInitialPages) {
+ if (url == BROWSER_NEW_TAB_URL) {
+ continue; // We tested about:newtab using BrowserOpenTab() above.
+ }
+ info("Testing " + url + " - " + new Date());
+ yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ yield BrowserTestUtils.closeWindow(win);
+
+ ok(SessionStore.getClosedWindowCount(), "Should have a closed window");
+
+ win = SessionStore.undoCloseWindow(0);
+ yield TestUtils.topicObserved("sessionstore-single-window-restored",
+ subject => subject == win);
+ yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ yield TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+
+ is(win.gURLBar.value, "", "URL bar should be empty");
+ tab = win.gBrowser.selectedTab;
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+ state = JSON.parse(SessionStore.getTabState(tab));
+ ok(!state.userTypedValue, "userTypedValue should be undefined on the tab's state");
+
+ info("Removing tab - " + new Date());
+ yield BrowserTestUtils.removeTab(tab);
+ info("Finished removing tab - " + new Date());
+ }
+ info("Removing window - " + new Date());
+ yield BrowserTestUtils.closeWindow(win);
+ info("Finished removing window - " + new Date());
+});
diff --git a/browser/components/sessionstore/test/browser_pageStyle.js b/browser/components/sessionstore/test/browser_pageStyle.js
new file mode 100644
index 000000000..7abee5d9d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_pageStyle.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = getRootDirectory(gTestPath) + "browser_pageStyle_sample.html";
+const URL_NESTED = getRootDirectory(gTestPath) + "browser_pageStyle_sample_nested.html";
+
+/**
+ * This test ensures that page style information is correctly persisted.
+ */
+add_task(function page_style() {
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+ let sheets = yield getStyleSheets(browser);
+
+ // Enable all style sheets one by one.
+ for (let [title, disabled] of sheets) {
+ yield enableStyleSheetsForSet(browser, title);
+
+ let tab2 = gBrowser.duplicateTab(tab);
+ yield promiseTabRestored(tab2);
+
+ let sheets = yield getStyleSheets(tab2.linkedBrowser);
+ let enabled = sheets.filter(([title, disabled]) => !disabled);
+
+ if (title.startsWith("fail_")) {
+ ok(!enabled.length, "didn't restore " + title);
+ } else {
+ is(enabled.length, 1, "restored one style sheet");
+ is(enabled[0][0], title, "restored correct sheet");
+ }
+
+ gBrowser.removeTab(tab2);
+ }
+
+ // Disable all styles and verify that this is correctly persisted.
+ yield setAuthorStyleDisabled(browser, true);
+
+ let tab2 = gBrowser.duplicateTab(tab);
+ yield promiseTabRestored(tab2);
+
+ let authorStyleDisabled = yield getAuthorStyleDisabled(tab2.linkedBrowser);
+ ok(authorStyleDisabled, "disabled all stylesheets");
+
+ // Clean up.
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(tab2);
+});
+
+/**
+ * This test ensures that page style notification from nested documents are
+ * received and the page style is persisted correctly.
+ */
+add_task(function nested_page_style() {
+ let tab = gBrowser.addTab(URL_NESTED);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ yield enableSubDocumentStyleSheetsForSet(browser, "alternate");
+ yield promiseRemoveTab(tab);
+
+ let [{state: {pageStyle}}] = JSON.parse(ss.getClosedTabData(window));
+ let expected = JSON.stringify({children: [{pageStyle: "alternate"}]});
+ is(JSON.stringify(pageStyle), expected, "correct pageStyle persisted");
+});
+
+function getStyleSheets(browser) {
+ return sendMessage(browser, "ss-test:getStyleSheets");
+}
+
+function enableStyleSheetsForSet(browser, name) {
+ return sendMessage(browser, "ss-test:enableStyleSheetsForSet", name);
+}
+
+function enableSubDocumentStyleSheetsForSet(browser, name) {
+ return sendMessage(browser, "ss-test:enableSubDocumentStyleSheetsForSet", {
+ id: "iframe", set: name
+ });
+}
+
+function getAuthorStyleDisabled(browser) {
+ return sendMessage(browser, "ss-test:getAuthorStyleDisabled");
+}
+
+function setAuthorStyleDisabled(browser, val) {
+ return sendMessage(browser, "ss-test:setAuthorStyleDisabled", val)
+}
diff --git a/browser/components/sessionstore/test/browser_pageStyle_sample.html b/browser/components/sessionstore/test/browser_pageStyle_sample.html
new file mode 100644
index 000000000..810054049
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_pageStyle_sample.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>pageStyle sample</title>
+
+ <link href="404.css" title="default" rel="stylesheet">
+ <link href="404.css" title="alternate" rel="alternate stylesheet">
+ <link href="404.css" title="altERnate" rel=" styLEsheet altERnate ">
+ <link href="404.css" title="media_empty" rel="alternate stylesheet" media="">
+ <link href="404.css" title="media_all" rel="alternate stylesheet" media="all">
+ <link href="404.css" title="media_ALL" rel="alternate stylesheet" media=" ALL ">
+ <link href="404.css" title="media_screen" rel="alternate stylesheet" media="screen">
+ <link href="404.css" title="media_print_screen" rel="alternate stylesheet" media="print,screen">
+</head>
+<body></body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_pageStyle_sample_nested.html b/browser/components/sessionstore/test/browser_pageStyle_sample_nested.html
new file mode 100644
index 000000000..157609fa6
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_pageStyle_sample_nested.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>pageStyle sample (nested)</title>
+</head>
+<body>
+ <iframe id="iframe" src="browser_pageStyle_sample.html"/>
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_page_title.js b/browser/components/sessionstore/test/browser_page_title.js
new file mode 100644
index 000000000..9bbb1ca76
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_page_title.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const URL = "data:text/html,<title>initial title</title>";
+
+add_task(function* () {
+ // Create a new tab.
+ let tab = gBrowser.addTab(URL);
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ // Remove the tab.
+ yield promiseRemoveTab(tab);
+
+ // Check the title.
+ let [{state: {entries}}] = JSON.parse(ss.getClosedTabData(window));
+ is(entries[0].title, "initial title", "correct title");
+});
+
+add_task(function* () {
+ // Create a new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to ensure we collected the initial title.
+ yield TabStateFlusher.flush(browser);
+
+ // Set a new title.
+ yield ContentTask.spawn(browser, null, function* () {
+ return new Promise(resolve => {
+ addEventListener("DOMTitleChanged", function onTitleChanged() {
+ removeEventListener("DOMTitleChanged", onTitleChanged);
+ resolve();
+ });
+
+ content.document.title = "new title";
+ });
+ });
+
+ // Remove the tab.
+ yield promiseRemoveTab(tab);
+
+ // Check the title.
+ let [{state: {entries}}] = JSON.parse(ss.getClosedTabData(window));
+ is(entries[0].title, "new title", "correct title");
+});
diff --git a/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js
new file mode 100644
index 000000000..1deb461c8
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js
@@ -0,0 +1,95 @@
+"use strict";
+
+const SELFCHROMEURL =
+ "chrome://mochitests/content/browser/browser/" +
+ "components/sessionstore/test/browser_parentProcessRestoreHash.js";
+
+const Cm = Components.manager;
+
+const TESTCLASSID = "78742c04-3630-448c-9be3-6c5070f062de";
+
+const TESTURL = "about:testpageforsessionrestore#foo";
+
+
+let TestAboutPage = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+ getURIFlags: function(aURI) {
+ // No CAN_ or MUST_LOAD_IN_CHILD means this loads in the parent:
+ return Ci.nsIAboutModule.ALLOW_SCRIPT |
+ Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+ },
+
+ newChannel: function(aURI, aLoadInfo) {
+ // about: page inception!
+ let newURI = Services.io.newURI(SELFCHROMEURL, null, null);
+ let channel = Services.io.newChannelFromURIWithLoadInfo(newURI,
+ aLoadInfo);
+ channel.originalURI = aURI;
+ return channel;
+ },
+
+ createInstance: function(outer, iid) {
+ if (outer != null) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(iid);
+ },
+
+ register: function() {
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory(
+ Components.ID(TESTCLASSID), "Only here for a test",
+ "@mozilla.org/network/protocol/about;1?what=testpageforsessionrestore", this);
+ },
+
+ unregister: function() {
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory(
+ Components.ID(TESTCLASSID), this);
+ }
+};
+
+
+/**
+ * Test that switching from a remote to a parent process browser
+ * correctly clears the userTypedValue
+ */
+add_task(function* () {
+ TestAboutPage.register();
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/", true, true);
+ ok(tab.linkedBrowser.isRemoteBrowser, "Browser should be remote");
+
+ let resolveLocationChangePromise;
+ let locationChangePromise = new Promise(r => resolveLocationChangePromise = r);
+ let wpl = {
+ onStateChange(wpl, request, state, status) {
+ let location = request.QueryInterface(Ci.nsIChannel).originalURI;
+ // Ignore about:blank loads.
+ let docStop = Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+ if (location.spec == "about:blank" || (state & docStop == docStop)) {
+ return;
+ }
+ is(location.spec, TESTURL, "Got the expected URL");
+ resolveLocationChangePromise();
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ gURLBar.value = TESTURL;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ yield locationChangePromise;
+
+ ok(!tab.linkedBrowser.isRemoteBrowser, "Browser should no longer be remote");
+
+ is(gURLBar.textValue, TESTURL, "URL bar visible value should be correct.");
+ is(gURLBar.value, TESTURL, "URL bar value should be correct.");
+ is(gURLBar.getAttribute("pageproxystate"), "valid", "URL bar is in valid page proxy state");
+
+ ok(!tab.linkedBrowser.userTypedValue, "No userTypedValue should be on the browser.");
+
+ yield BrowserTestUtils.removeTab(tab);
+ gBrowser.removeProgressListener(wpl);
+ TestAboutPage.unregister();
+});
diff --git a/browser/components/sessionstore/test/browser_pending_tabs.js b/browser/components/sessionstore/test/browser_pending_tabs.js
new file mode 100644
index 000000000..e734e55c9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_pending_tabs.js
@@ -0,0 +1,35 @@
+"use strict";
+
+const TAB_STATE = {
+ entries: [{ url: "about:mozilla" }, { url: "about:robots" }],
+ index: 1,
+};
+
+add_task(function* () {
+ // Create a background tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // The tab shouldn't be restored right away.
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ // Prepare the tab state.
+ let promise = promiseTabRestoring(tab);
+ ss.setTabState(tab, JSON.stringify(TAB_STATE));
+ ok(tab.hasAttribute("pending"), "tab is pending");
+ yield promise;
+
+ // Flush to ensure the parent has all data.
+ yield TabStateFlusher.flush(browser);
+
+ // Check that the shistory index is the one we restored.
+ let tabState = TabState.collect(tab);
+ is(tabState.index, TAB_STATE.index, "correct shistory index");
+
+ // Check we don't collect userTypedValue when we shouldn't.
+ ok(!tabState.userTypedValue, "tab didn't have a userTypedValue");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_privatetabs.js b/browser/components/sessionstore/test/browser_privatetabs.js
new file mode 100644
index 000000000..cc02e56cf
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_privatetabs.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function cleanup() {
+ info("Forgetting closed tabs");
+ while (ss.getClosedTabCount(window)) {
+ ss.forgetClosedTab(window, 0);
+ }
+});
+
+add_task(function() {
+ let URL_PUBLIC = "http://example.com/public/" + Math.random();
+ let URL_PRIVATE = "http://example.com/private/" + Math.random();
+ let tab1, tab2;
+ try {
+ // Setup a public tab and a private tab
+ info("Setting up public tab");
+ tab1 = gBrowser.addTab(URL_PUBLIC);
+ yield promiseBrowserLoaded(tab1.linkedBrowser);
+
+ info("Setting up private tab");
+ tab2 = gBrowser.addTab();
+ yield promiseBrowserLoaded(tab2.linkedBrowser);
+ yield setUsePrivateBrowsing(tab2.linkedBrowser, true);
+ tab2.linkedBrowser.loadURI(URL_PRIVATE);
+ yield promiseBrowserLoaded(tab2.linkedBrowser);
+
+ info("Flush to make sure chrome received all data.");
+ yield TabStateFlusher.flush(tab1.linkedBrowser);
+ yield TabStateFlusher.flush(tab2.linkedBrowser);
+
+ info("Checking out state");
+ let state = yield promiseRecoveryFileContents();
+
+ info("State: " + state);
+ // Ensure that sessionstore.js only knows about the public tab
+ ok(state.indexOf(URL_PUBLIC) != -1, "State contains public tab");
+ ok(state.indexOf(URL_PRIVATE) == -1, "State does not contain private tab");
+
+ // Ensure that we can close and undo close the public tab but not the private tab
+ gBrowser.removeTab(tab2);
+ tab2 = null;
+
+ gBrowser.removeTab(tab1);
+ tab1 = null;
+
+ tab1 = ss.undoCloseTab(window, 0);
+ ok(true, "Public tab supports undo close");
+
+ is(ss.getClosedTabCount(window), 0, "Private tab does not support undo close");
+
+ } finally {
+ if (tab1) {
+ gBrowser.removeTab(tab1);
+ }
+ if (tab2) {
+ gBrowser.removeTab(tab2);
+ }
+ }
+});
+
+add_task(function () {
+ const FRAME_SCRIPT = "data:," +
+ "docShell.QueryInterface%28Components.interfaces.nsILoadContext%29.usePrivateBrowsing%3Dtrue";
+
+ // Clear the list of closed windows.
+ forgetClosedWindows();
+
+ // Create a new window to attach our frame script to.
+ let win = yield promiseNewWindowLoaded();
+ let mm = win.getGroupMessageManager("browsers");
+ mm.loadFrameScript(FRAME_SCRIPT, true);
+
+ // Create a new tab in the new window that will load the frame script.
+ let tab = win.gBrowser.addTab("about:mozilla");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+ yield TabStateFlusher.flush(browser);
+
+ // Check that we consider the tab as private.
+ let state = JSON.parse(ss.getTabState(tab));
+ ok(state.isPrivate, "tab considered private");
+
+ // Ensure we don't allow restoring closed private tabs in non-private windows.
+ win.gBrowser.removeTab(tab);
+ is(ss.getClosedTabCount(win), 0, "no tabs to restore");
+
+ // Create a new tab in the new window that will load the frame script.
+ tab = win.gBrowser.addTab("about:mozilla");
+ browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+ yield TabStateFlusher.flush(browser);
+
+ // Check that we consider the tab as private.
+ state = JSON.parse(ss.getTabState(tab));
+ ok(state.isPrivate, "tab considered private");
+
+ // Check that all private tabs are removed when the non-private
+ // window is closed and we don't save windows without any tabs.
+ yield BrowserTestUtils.closeWindow(win);
+ is(ss.getClosedWindowCount(), 0, "no windows to restore");
+});
+
+add_task(function () {
+ // Clear the list of closed windows.
+ forgetClosedWindows();
+
+ // Create a new window to attach our frame script to.
+ let win = yield promiseNewWindowLoaded({private: true});
+
+ // Create a new tab in the new window that will load the frame script.
+ let tab = win.gBrowser.addTab("about:mozilla");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+ yield TabStateFlusher.flush(browser);
+
+ // Check that we consider the tab as private.
+ let state = JSON.parse(ss.getTabState(tab));
+ ok(state.isPrivate, "tab considered private");
+
+ // Ensure that closed tabs in a private windows can be restored.
+ win.gBrowser.removeTab(tab);
+ is(ss.getClosedTabCount(win), 1, "there is a single tab to restore");
+
+ // Ensure that closed private windows can never be restored.
+ yield BrowserTestUtils.closeWindow(win);
+ is(ss.getClosedWindowCount(), 0, "no windows to restore");
+});
+
+function setUsePrivateBrowsing(browser, val) {
+ return sendMessage(browser, "ss-test:setUsePrivateBrowsing", val);
+}
+
diff --git a/browser/components/sessionstore/test/browser_purge_shistory.js b/browser/components/sessionstore/test/browser_purge_shistory.js
new file mode 100644
index 000000000..28c6f6f24
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_purge_shistory.js
@@ -0,0 +1,59 @@
+"use strict";
+
+/**
+ * This test checks that pending tabs are treated like fully loaded tabs when
+ * purging session history. Just like for fully loaded tabs we want to remove
+ * every but the current shistory entry.
+ */
+
+const TAB_STATE = {
+ entries: [{url: "about:mozilla"}, {url: "about:robots"}],
+ index: 1,
+};
+
+function checkTabContents(browser) {
+ return ContentTask.spawn(browser, null, function* () {
+ let Ci = Components.interfaces;
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
+ Assert.ok(history && history.count == 1 && content.document.documentURI == "about:mozilla",
+ "expected tab contents found");
+ });
+}
+
+add_task(function* () {
+ // Create a new tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+ yield promiseTabState(tab, TAB_STATE);
+
+ // Create another new tab.
+ let tab2 = gBrowser.addTab("about:blank");
+ let browser2 = tab2.linkedBrowser;
+ yield promiseBrowserLoaded(browser2);
+
+ // The tab shouldn't be restored right away.
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ // Prepare the tab state.
+ let promise = promiseTabRestoring(tab2);
+ ss.setTabState(tab2, JSON.stringify(TAB_STATE));
+ ok(tab2.hasAttribute("pending"), "tab is pending");
+ yield promise;
+
+ // Purge session history.
+ Services.obs.notifyObservers(null, "browser:purge-session-history", "");
+ yield checkTabContents(browser);
+ ok(tab2.hasAttribute("pending"), "tab is still pending");
+
+ // Kick off tab restoration.
+ gBrowser.selectedTab = tab2;
+ yield promiseTabRestored(tab2);
+ yield checkTabContents(browser2);
+ ok(!tab2.hasAttribute("pending"), "tab is not pending anymore");
+
+ // Cleanup.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js b/browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js
new file mode 100644
index 000000000..7dbee03fd
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js
@@ -0,0 +1,342 @@
+"use strict";
+
+/**
+ * This set of tests checks that the remoteness is properly
+ * set for each browser in a window when that window has
+ * session state loaded into it.
+ */
+
+/**
+ * Takes a SessionStore window state object for a single
+ * window, sets the selected tab for it, and then returns
+ * the object to be passed to SessionStore.setWindowState.
+ *
+ * @param state (object)
+ * The state to prepare to be sent to a window. This is
+ * state should just be for a single window.
+ * @param selected (int)
+ * The 1-based index of the selected tab. Note that
+ * If this is 0, then the selected tab will not change
+ * from what's already selected in the window that we're
+ * sending state to.
+ * @returns (object)
+ * The JSON encoded string to call
+ * SessionStore.setWindowState with.
+ */
+function prepareState(state, selected) {
+ // We'll create a copy so that we don't accidentally
+ // modify the caller's selected property.
+ let copy = {};
+ Object.assign(copy, state);
+ copy.selected = selected;
+
+ return {
+ windows: [ copy ],
+ };
+}
+
+const SIMPLE_STATE = {
+ tabs: [
+ { entries: [{ url: "http://example.com/", title: "title" }] },
+ { entries: [{ url: "http://example.com/", title: "title" }] },
+ { entries: [{ url: "http://example.com/", title: "title" }] },
+ ],
+ title: "",
+ _closedTabs: [],
+};
+
+const PINNED_STATE = {
+ tabs: [
+ { entries: [{ url: "http://example.com/", title: "title" }], pinned: true },
+ { entries: [{ url: "http://example.com/", title: "title" }], pinned: true },
+ { entries: [{ url: "http://example.com/", title: "title" }] },
+ ],
+ title: "",
+ _closedTabs: [],
+};
+
+/**
+ * This is where most of the action is happening. This function takes
+ * an Array of "test scenario" Objects and runs them. For each scenario, a
+ * window is opened, put into some state, and then a new state is
+ * loaded into that window. We then check to make sure that the
+ * right things have happened in that window wrt remoteness flips.
+ *
+ * The schema for a testing scenario Object is as follows:
+ *
+ * initialRemoteness:
+ * an Array that represents the starting window. Each bool
+ * in the Array represents the window tabs in order. A "true"
+ * indicates that that tab should be remote. "false" if the tab
+ * should be non-remote.
+ *
+ * initialSelectedTab:
+ * The 1-based index of the tab that we want to select for the
+ * restored window. This is 1-based to avoid confusion with the
+ * selectedTab property described down below, though you probably
+ * want to set this to be greater than 0, since the initial window
+ * needs to have a defined initial selected tab. Because of this,
+ * the test will throw if initialSelectedTab is 0.
+ *
+ * stateToRestore:
+ * A JS Object for the state to send down to the window.
+ *
+ * selectedTab:
+ * The 1-based index of the tab that we want to select for the
+ * restored window. Leave this at 0 if you don't want to change
+ * the selection from the initial window state.
+ *
+ * expectedFlips:
+ * an Array that represents the window that we end up with after
+ * restoring state. Each bool in the Array represents the window tabs,
+ * in order. A "true" indicates that the tab should have flipped
+ * its remoteness once. "false" indicates that the tab should never
+ * have flipped remoteness. Note that any tab that flips its remoteness
+ * more than once will cause a test failure.
+ *
+ * expectedRemoteness:
+ * an Array that represents the window that we end up with after
+ * restoring state. Each bool in the Array represents the window
+ * tabs in order. A "true" indicates that the tab be remote, and
+ * a "false" indicates that the tab should be "non-remote". We
+ * need this Array in order to test pinned tabs which will also
+ * be loaded by default, and therefore should end up remote.
+ *
+ */
+function* runScenarios(scenarios) {
+ for (let scenario of scenarios) {
+ // Let's make sure our scenario is sane first.
+ Assert.equal(scenario.expectedFlips.length,
+ scenario.expectedRemoteness.length,
+ "All expected flips and remoteness needs to be supplied");
+ Assert.ok(scenario.initialSelectedTab > 0,
+ "You must define an initially selected tab");
+
+ // First, we need to create the initial conditions, so we
+ // open a new window to put into our starting state...
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+ let tabbrowser = win.gBrowser;
+ Assert.ok(tabbrowser.selectedBrowser.isRemoteBrowser,
+ "The initial browser should be remote.");
+ // Now put the window into the expected initial state.
+ for (let i = 0; i < scenario.initialRemoteness.length; ++i) {
+ let tab;
+ if (i > 0) {
+ // The window starts with one tab, so we need to create
+ // any of the additional ones required by this test.
+ info("Opening a new tab");
+ tab = yield BrowserTestUtils.openNewForegroundTab(tabbrowser)
+ } else {
+ info("Using the selected tab");
+ tab = tabbrowser.selectedTab;
+ }
+ let browser = tab.linkedBrowser;
+ let remotenessState = scenario.initialRemoteness[i];
+ tabbrowser.updateBrowserRemoteness(browser, remotenessState);
+ }
+
+ // And select the requested tab.
+ let tabToSelect = tabbrowser.tabs[scenario.initialSelectedTab - 1];
+ if (tabbrowser.selectedTab != tabToSelect) {
+ yield BrowserTestUtils.switchTab(tabbrowser, tabToSelect);
+ }
+
+ // Hook up an event listener to make sure that the right
+ // tabs flip remoteness, and only once.
+ let flipListener = {
+ seenBeforeTabs: new Set(),
+ seenAfterTabs: new Set(),
+ handleEvent(e) {
+ let index = Array.from(tabbrowser.tabs).indexOf(e.target);
+ switch (e.type) {
+ case "BeforeTabRemotenessChange":
+ info(`Saw tab at index ${index} before remoteness flip`);
+ if (this.seenBeforeTabs.has(e.target)) {
+ Assert.ok(false, "Saw tab before remoteness flip more than once");
+ }
+ this.seenBeforeTabs.add(e.target);
+ break;
+ case "TabRemotenessChange":
+ info(`Saw tab at index ${index} after remoteness flip`);
+ if (this.seenAfterTabs.has(e.target)) {
+ Assert.ok(false, "Saw tab after remoteness flip more than once");
+ }
+ this.seenAfterTabs.add(e.target);
+ break;
+ }
+ },
+ };
+
+ win.addEventListener("BeforeTabRemotenessChange", flipListener);
+ win.addEventListener("TabRemotenessChange", flipListener);
+
+ // Okay, time to test!
+ let state = prepareState(scenario.stateToRestore,
+ scenario.selectedTab);
+
+ SessionStore.setWindowState(win, state, true);
+
+ win.removeEventListener("BeforeTabRemotenessChange", flipListener);
+ win.removeEventListener("TabRemotenessChange", flipListener);
+
+ // Because we know that scenario.expectedFlips and
+ // scenario.expectedRemoteness have the same length, we
+ // can check that we satisfied both with the same loop.
+ for (let i = 0; i < scenario.expectedFlips.length; ++i) {
+ let expectedToFlip = scenario.expectedFlips[i];
+ let expectedRemoteness = scenario.expectedRemoteness[i];
+ let tab = tabbrowser.tabs[i];
+ if (expectedToFlip) {
+ Assert.ok(flipListener.seenBeforeTabs.has(tab),
+ `We should have seen tab at index ${i} before remoteness flip`);
+ Assert.ok(flipListener.seenAfterTabs.has(tab),
+ `We should have seen tab at index ${i} after remoteness flip`);
+ } else {
+ Assert.ok(!flipListener.seenBeforeTabs.has(tab),
+ `We should not have seen tab at index ${i} before remoteness flip`);
+ Assert.ok(!flipListener.seenAfterTabs.has(tab),
+ `We should not have seen tab at index ${i} after remoteness flip`);
+ }
+
+ Assert.equal(tab.linkedBrowser.isRemoteBrowser, expectedRemoteness,
+ "Should have gotten the expected remoteness " +
+ `for the tab at index ${i}`);
+ }
+
+ yield BrowserTestUtils.closeWindow(win);
+ }
+}
+
+/**
+ * Tests that if we restore state to browser windows with
+ * a variety of initial remoteness states, that we only flip
+ * the remoteness on the necessary tabs. For this particular
+ * set of tests, we assume that tabs are restoring on demand.
+ */
+add_task(function*() {
+ // This test opens and closes windows, which might bog down
+ // a debug build long enough to time out the test, so we
+ // extend the tolerance on timeouts.
+ requestLongerTimeout(5);
+
+ yield SpecialPowers.pushPrefEnv({
+ "set": [["browser.sessionstore.restore_on_demand", true]],
+ });
+
+ const TEST_SCENARIOS = [
+ // Only one tab in the new window, and it's remote. This
+ // is the common case, since this is how restoration occurs
+ // when the restored window is being opened.
+ {
+ initialRemoteness: [true],
+ initialSelectedTab: 1,
+ stateToRestore: SIMPLE_STATE,
+ selectedTab: 3,
+ // The initial tab is remote and should go into
+ // the background state. The second and third tabs
+ // are new and should be initialized non-remote.
+ expectedFlips: [true, false, true],
+ // Only the selected tab should be remote.
+ expectedRemoteness: [false, false, true],
+ },
+
+ // A single remote tab, and this is the one that's going
+ // to be selected once state is restored.
+ {
+ initialRemoteness: [true],
+ initialSelectedTab: 1,
+ stateToRestore: SIMPLE_STATE,
+ selectedTab: 1,
+ // The initial tab is remote and selected, so it should
+ // not flip remoteness. The other two new tabs should
+ // be non-remote by default.
+ expectedFlips: [false, false, false],
+ // Only the selected tab should be remote.
+ expectedRemoteness: [true, false, false],
+ },
+
+ // A single remote tab which starts selected. We set the
+ // selectedTab to 0 which is equivalent to "don't change
+ // the tab selection in the window".
+ {
+ initialRemoteness: [true],
+ initialSelectedTab: 1,
+ stateToRestore: SIMPLE_STATE,
+ selectedTab: 0,
+ // The initial tab is remote and selected, so it should
+ // not flip remoteness. The other two new tabs should
+ // be non-remote by default.
+ expectedFlips: [false, false, false],
+ // Only the selected tab should be remote.
+ expectedRemoteness: [true, false, false],
+ },
+
+ // An initially remote tab, but we're going to load
+ // some pinned tabs now, and the pinned tabs should load
+ // right away.
+ {
+ initialRemoteness: [true],
+ initialSelectedTab: 1,
+ stateToRestore: PINNED_STATE,
+ selectedTab: 3,
+ // The initial tab is pinned and will load right away,
+ // so it should stay remote. The second tab is new
+ // and pinned, so it should start remote and not flip.
+ // The third tab is not pinned, but it is selected,
+ // so it will start non-remote, and then flip remoteness.
+ expectedFlips: [false, false, true],
+ // Both pinned tabs and the selected tabs should all
+ // end up being remote.
+ expectedRemoteness: [true, true, true],
+ },
+
+ // A single non-remote tab.
+ {
+ initialRemoteness: [false],
+ initialSelectedTab: 1,
+ stateToRestore: SIMPLE_STATE,
+ selectedTab: 2,
+ // The initial tab is non-remote and should stay
+ // that way. The second and third tabs are new and
+ // should be initialized non-remote.
+ expectedFlips: [false, true, false],
+ // Only the selected tab should be remote.
+ expectedRemoteness: [false, true, false],
+ },
+
+ // A mixture of remote and non-remote tabs.
+ {
+ initialRemoteness: [true, false, true],
+ initialSelectedTab: 1,
+ stateToRestore: SIMPLE_STATE,
+ selectedTab: 3,
+ // The initial tab is remote and should flip to non-remote
+ // as it is put into the background. The second tab should
+ // stay non-remote, and the third one should stay remote.
+ expectedFlips: [true, false, false],
+ // Only the selected tab should be remote.
+ expectedRemoteness: [false, false, true],
+ },
+
+ // An initially non-remote tab, but we're going to load
+ // some pinned tabs now, and the pinned tabs should load
+ // right away.
+ {
+ initialRemoteness: [false],
+ initialSelectedTab: 1,
+ stateToRestore: PINNED_STATE,
+ selectedTab: 3,
+ // The initial tab is pinned and will load right away,
+ // so it should flip remoteness. The second tab is new
+ // and pinned, so it should start remote and not flip.
+ // The third tab is not pinned, but it is selected,
+ // so it will start non-remote, and then flip remoteness.
+ expectedFlips: [true, false, true],
+ // Both pinned tabs and the selected tabs should all
+ // end up being remote.
+ expectedRemoteness: [true, true, true],
+ },
+ ];
+
+ yield* runScenarios(TEST_SCENARIOS);
+});
diff --git a/browser/components/sessionstore/test/browser_replace_load.js b/browser/components/sessionstore/test/browser_replace_load.js
new file mode 100644
index 000000000..5464a0874
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_replace_load.js
@@ -0,0 +1,52 @@
+"use strict";
+
+const STATE = {
+ entries: [{url: "about:robots"}, {url: "about:mozilla"}],
+ selected: 2
+};
+
+/**
+ * Bug 1100223. Calling browser.loadURI() while a tab is loading causes
+ * sessionstore to override the desired target URL. This test ensures that
+ * calling loadURI() on a pending tab causes the tab to no longer be marked
+ * as pending and correctly finish the instructed load while keeping the
+ * restored history around.
+ */
+add_task(function* () {
+ yield testSwitchToTab("about:mozilla#fooobar", {ignoreFragment: "whenComparingAndReplace"});
+ yield testSwitchToTab("about:mozilla?foo=bar", {replaceQueryString: true});
+});
+
+var testSwitchToTab = Task.async(function* (url, options) {
+ // Create a background tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // The tab shouldn't be restored right away.
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ // Prepare the tab state.
+ let promise = promiseTabRestoring(tab);
+ ss.setTabState(tab, JSON.stringify(STATE));
+ ok(tab.hasAttribute("pending"), "tab is pending");
+ yield promise;
+
+ // Switch-to-tab with a similar URI.
+ switchToTabHavingURI(url, false, options);
+
+ // Tab should now restore
+ yield promiseTabRestored(tab);
+ is(browser.currentURI.spec, url, "correct URL loaded");
+
+ // Check that we didn't lose any history entries.
+ yield ContentTask.spawn(browser, null, function* () {
+ let Ci = Components.interfaces;
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
+ Assert.equal(history && history.count, 3, "three history entries");
+ });
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js b/browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js
new file mode 100644
index 000000000..5767c6c0f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js
@@ -0,0 +1,171 @@
+/*
+ * Bug 1267910 - The regression test case for session cookies.
+ */
+
+"use strict";
+
+const TEST_HOST = "www.example.com";
+const COOKIE =
+{
+ name: "test1",
+ value: "yes1",
+ path: "/browser/browser/components/sessionstore/test/"
+};
+const SESSION_DATA = `
+{
+ "version": ["sessionrestore", 1],
+ "windows": [{
+ "tabs": [{
+ "entries": [],
+ "lastAccessed": 1463893009797,
+ "hidden": false,
+ "attributes": {},
+ "image": null
+ }, {
+ "entries": [{
+ "url": "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html",
+ \"charset": "UTF-8",
+ "ID": 0,
+ "docshellID": 2,
+ "originalURI": "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html",
+ \"docIdentifier": 0,
+ "persist": true
+ }],
+ "lastAccessed": 1463893009321,
+ "hidden": false,
+ "attributes": {},
+ "userContextId": 0,
+ "index": 1,
+ "image": "http://www.example.com/favicon.ico"
+ }],
+ "selected": 1,
+ "_closedTabs": [],
+ "busy": false,
+ "width": 1024,
+ "height": 768,
+ "screenX": 4,
+ "screenY": 23,
+ "sizemode": "normal",
+ "cookies": [{
+ "host": "www.example.com",
+ "value": "yes1",
+ "path": "/browser/browser/components/sessionstore/test/",
+ "name": "test1"
+ }]
+ }],
+ "selectedWindow": 1,
+ "_closedWindows": [],
+ "session": {
+ "lastUpdate": 1463893009801,
+ "startTime": 1463893007134,
+ "recentCrashes": 0
+ },
+ "global": {}
+}`;
+const SESSION_DATA_OA = `
+{
+ "version": ["sessionrestore", 1],
+ "windows": [{
+ "tabs": [{
+ "entries": [],
+ "lastAccessed": 1463893009797,
+ "hidden": false,
+ "attributes": {},
+ "image": null
+ }, {
+ "entries": [{
+ "url": "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html",
+ \"charset": "UTF-8",
+ "ID": 0,
+ "docshellID": 2,
+ "originalURI": "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html",
+ \"docIdentifier": 0,
+ "persist": true
+ }],
+ "lastAccessed": 1463893009321,
+ "hidden": false,
+ "attributes": {},
+ "userContextId": 0,
+ "index": 1,
+ "image": "http://www.example.com/favicon.ico"
+ }],
+ "selected": 1,
+ "_closedTabs": [],
+ "busy": false,
+ "width": 1024,
+ "height": 768,
+ "screenX": 4,
+ "screenY": 23,
+ "sizemode": "normal",
+ "cookies": [{
+ "host": "www.example.com",
+ "value": "yes1",
+ "path": "/browser/browser/components/sessionstore/test/",
+ "name": "test1",
+ "originAttributes": {
+ "addonId": "",
+ "appId": 0,
+ "inIsolatedMozBrowser": false,
+ "userContextId": 0
+ }
+ }]
+ }],
+ "selectedWindow": 1,
+ "_closedWindows": [],
+ "session": {
+ "lastUpdate": 1463893009801,
+ "startTime": 1463893007134,
+ "recentCrashes": 0
+ },
+ "global": {}
+}`;
+
+add_task(function* run_test() {
+ // Wait until initialization is complete.
+ yield SessionStore.promiseInitialized;
+
+ // Clear cookies.
+ Services.cookies.removeAll();
+
+ // Open a new window.
+ let win = yield promiseNewWindowLoaded();
+
+ // Restore window with session cookies that have no originAttributes.
+ ss.setWindowState(win, SESSION_DATA, true);
+
+ let enumerator = Services.cookies.getCookiesFromHost(TEST_HOST, {});
+ let cookie;
+ let cookieCount = 0;
+ while (enumerator.hasMoreElements()) {
+ cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
+ cookieCount++;
+ }
+
+ // Check that the cookie is restored successfully.
+ is(cookieCount, 1, "expected one cookie");
+ is(cookie.name, COOKIE.name, "cookie name successfully restored");
+ is(cookie.value, COOKIE.value, "cookie value successfully restored");
+ is(cookie.path, COOKIE.path, "cookie path successfully restored");
+
+ // Clear cookies.
+ Services.cookies.removeAll();
+
+ // Restore window with session cookies that have originAttributes within.
+ ss.setWindowState(win, SESSION_DATA_OA, true);
+
+ enumerator = Services.cookies.getCookiesFromHost(TEST_HOST, {});
+ cookieCount = 0;
+ while (enumerator.hasMoreElements()) {
+ cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
+ cookieCount++;
+ }
+
+ // Check that the cookie is restored successfully.
+ is(cookieCount, 1, "expected one cookie");
+ is(cookie.name, COOKIE.name, "cookie name successfully restored");
+ is(cookie.value, COOKIE.value, "cookie value successfully restored");
+ is(cookie.path, COOKIE.path, "cookie path successfully restored");
+
+ // Close our window.
+ yield BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/sessionstore/test/browser_restore_redirect.js b/browser/components/sessionstore/test/browser_restore_redirect.js
new file mode 100644
index 000000000..bea6e9f47
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_restore_redirect.js
@@ -0,0 +1,69 @@
+"use strict";
+
+const BASE = "http://example.com/browser/browser/components/sessionstore/test/";
+const TARGET = BASE + "restore_redirect_target.html";
+
+/**
+ * Ensure that a http redirect leaves a working tab.
+ */
+add_task(function check_http_redirect() {
+ let state = {
+ entries: [{ url: BASE + "restore_redirect_http.html" }]
+ };
+
+ // Open a new tab to restore into.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseTabState(tab, state);
+
+ info("Restored tab");
+
+ yield TabStateFlusher.flush(browser);
+ let data = TabState.collect(tab);
+ is(data.entries.length, 1, "Should be one entry in session history");
+ is(data.entries[0].url, TARGET, "Should be the right session history entry");
+
+ ok(!("__SS_data" in browser), "Temporary restore data should have been cleared");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+});
+
+/**
+ * Ensure that a js redirect leaves a working tab.
+ */
+add_task(function check_js_redirect() {
+ let state = {
+ entries: [{ url: BASE + "restore_redirect_js.html" }]
+ };
+
+ let loadPromise = new Promise(resolve => {
+ function listener(msg) {
+ if (msg.data.url.endsWith("restore_redirect_target.html")) {
+ window.messageManager.removeMessageListener("ss-test:loadEvent", listener);
+ resolve();
+ }
+ }
+
+ window.messageManager.addMessageListener("ss-test:loadEvent", listener);
+ });
+
+ // Open a new tab to restore into.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseTabState(tab, state);
+
+ info("Restored tab");
+
+ yield loadPromise;
+
+ yield TabStateFlusher.flush(browser);
+ let data = TabState.collect(tab);
+ is(data.entries.length, 1, "Should be one entry in session history");
+ is(data.entries[0].url, TARGET, "Should be the right session history entry");
+
+ ok(!("__SS_data" in browser), "Temporary restore data should have been cleared");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js b/browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js
new file mode 100644
index 000000000..e29cd5e49
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that even if the user has set their tabs to restore
+ * immediately on session start, that background tabs after a
+ * content process crash restore on demand.
+ */
+
+"use strict";
+
+const PAGE_1 = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const PAGE_2 = "data:text/html,<html><body>Another%20regular,%20everyday,%20normal%20page.";
+
+add_task(function* setup() {
+ yield pushPrefs(["dom.ipc.processCount", 1],
+ ["browser.tabs.animate", false],
+ ["browser.sessionstore.restore_on_demand", false]);
+});
+
+add_task(function* test_revive_bg_tabs_on_demand() {
+ let newTab1 = gBrowser.addTab(PAGE_1);
+ let browser1 = newTab1.linkedBrowser;
+ gBrowser.selectedTab = newTab1;
+
+ let newTab2 = gBrowser.addTab(PAGE_2);
+ let browser2 = newTab2.linkedBrowser;
+
+ yield BrowserTestUtils.browserLoaded(browser1);
+ yield BrowserTestUtils.browserLoaded(browser2);
+
+ yield TabStateFlusher.flush(browser2);
+
+ // Now crash the selected tab
+ let windowReady = BrowserTestUtils.waitForEvent(window, "SSWindowStateReady");
+ yield BrowserTestUtils.crashBrowser(browser1);
+
+ ok(newTab1.hasAttribute("crashed"), "Selected tab should be crashed");
+ ok(!newTab2.hasAttribute("crashed"), "Background tab should not be crashed");
+
+ // Wait until we've had a chance to restore all tabs immediately
+ yield windowReady;
+
+ // But we should not have restored the background tab
+ ok(newTab2.hasAttribute("pending"), "Background tab should be pending");
+
+ // Now select newTab2 to make sure it restores.
+ let newTab2Restored = promiseTabRestored(newTab2);
+ gBrowser.selectedTab = newTab2;
+ yield newTab2Restored;
+
+ ok(browser2.isRemoteBrowser, "Restored browser should be remote");
+
+ yield BrowserTestUtils.removeTab(newTab1);
+ yield BrowserTestUtils.removeTab(newTab2);
+});
diff --git a/browser/components/sessionstore/test/browser_scrollPositions.js b/browser/components/sessionstore/test/browser_scrollPositions.js
new file mode 100644
index 000000000..865520772
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BASE = "http://example.com/browser/browser/components/sessionstore/test/"
+const URL = BASE + "browser_scrollPositions_sample.html";
+const URL_FRAMESET = BASE + "browser_scrollPositions_sample_frameset.html";
+
+// Randomized set of scroll positions we will use in this test.
+const SCROLL_X = Math.round(100 * (1 + Math.random()));
+const SCROLL_Y = Math.round(200 * (1 + Math.random()));
+const SCROLL_STR = SCROLL_X + "," + SCROLL_Y;
+
+const SCROLL2_X = Math.round(300 * (1 + Math.random()));
+const SCROLL2_Y = Math.round(400 * (1 + Math.random()));
+const SCROLL2_STR = SCROLL2_X + "," + SCROLL2_Y;
+
+requestLongerTimeout(2);
+
+/**
+ * This test ensures that we properly serialize and restore scroll positions
+ * for an average page without any frames.
+ */
+add_task(function test_scroll() {
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Scroll down a little.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL_X, y: SCROLL_Y});
+ yield checkScroll(tab, {scroll: SCROLL_STR}, "scroll is fine");
+
+ // Duplicate and check that the scroll position is restored.
+ let tab2 = ss.duplicateTab(window, tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2);
+
+ let scroll = yield sendMessage(browser2, "ss-test:getScrollPosition");
+ is(JSON.stringify(scroll), JSON.stringify({x: SCROLL_X, y: SCROLL_Y}),
+ "scroll position has been duplicated correctly");
+
+ // Check that reloading retains the scroll positions.
+ browser2.reload();
+ yield promiseBrowserLoaded(browser2);
+ yield checkScroll(tab2, {scroll: SCROLL_STR}, "reloading retains scroll positions");
+
+ // Check that a force-reload resets scroll positions.
+ browser2.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ yield promiseBrowserLoaded(browser2);
+ yield checkScroll(tab2, null, "force-reload resets scroll positions");
+
+ // Scroll back to the top and check that the position has been reset. We
+ // expect the scroll position to be "null" here because there is no data to
+ // be stored if the frame is in its default scroll position.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: 0, y: 0});
+ yield checkScroll(tab, null, "no scroll stored");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+ yield promiseRemoveTab(tab2);
+});
+
+/**
+ * This tests ensures that we properly serialize and restore scroll positions
+ * for multiple frames of pages with framesets.
+ */
+add_task(function test_scroll_nested() {
+ let tab = gBrowser.addTab(URL_FRAMESET);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Scroll the first child frame down a little.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL_X, y: SCROLL_Y, frame: 0});
+ yield checkScroll(tab, {children: [{scroll: SCROLL_STR}]}, "scroll is fine");
+
+ // Scroll the second child frame down a little.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL2_X, y: SCROLL2_Y, frame: 1});
+ yield checkScroll(tab, {children: [{scroll: SCROLL_STR}, {scroll: SCROLL2_STR}]}, "scroll is fine");
+
+ // Duplicate and check that the scroll position is restored.
+ let tab2 = ss.duplicateTab(window, tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2);
+
+ let scroll = yield sendMessage(browser2, "ss-test:getScrollPosition", {frame: 0});
+ is(JSON.stringify(scroll), JSON.stringify({x: SCROLL_X, y: SCROLL_Y}),
+ "scroll position #1 has been duplicated correctly");
+
+ scroll = yield sendMessage(browser2, "ss-test:getScrollPosition", {frame: 1});
+ is(JSON.stringify(scroll), JSON.stringify({x: SCROLL2_X, y: SCROLL2_Y}),
+ "scroll position #2 has been duplicated correctly");
+
+ // Check that resetting one frame's scroll position removes it from the
+ // serialized value.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: 0, y: 0, frame: 0});
+ yield checkScroll(tab, {children: [null, {scroll: SCROLL2_STR}]}, "scroll is fine");
+
+ // Check the resetting all frames' scroll positions nulls the stored value.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: 0, y: 0, frame: 1});
+ yield checkScroll(tab, null, "no scroll stored");
+
+ // Cleanup.
+ yield promiseRemoveTab(tab);
+ yield promiseRemoveTab(tab2);
+});
+
+/**
+ * Test that scroll positions persist after restoring background tabs in
+ * a restored window (bug 1228518).
+ */
+add_task(function test_scroll_background_tabs() {
+ pushPrefs(["browser.sessionstore.restore_on_demand", true]);
+
+ let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+ let tab = newWin.gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ // Scroll down a little.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL_X, y: SCROLL_Y});
+ yield checkScroll(tab, {scroll: SCROLL_STR}, "scroll is fine");
+
+ // Close the window
+ yield BrowserTestUtils.closeWindow(newWin);
+
+ // Now restore the window
+ newWin = ss.undoCloseWindow(0);
+
+ // Make sure to wait for the window to be restored.
+ yield BrowserTestUtils.waitForEvent(newWin, "SSWindowStateReady");
+
+ is(newWin.gBrowser.tabs.length, 2, "There should be two tabs");
+
+ // The second tab should be the one we loaded URL at still
+ tab = newWin.gBrowser.tabs[1];
+ yield promiseTabRestoring(tab);
+
+ ok(tab.hasAttribute("pending"), "Tab should be pending");
+ browser = tab.linkedBrowser;
+
+ // Ensure there are no pending queued messages in the child.
+ yield TabStateFlusher.flush(browser);
+
+ // Now check to see if the background tab remembers where it
+ // should be scrolled to.
+ newWin.gBrowser.selectedTab = tab;
+ yield promiseTabRestored(tab);
+
+ yield checkScroll(tab, {scroll: SCROLL_STR}, "scroll is still fine");
+
+ yield BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js b/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js
new file mode 100644
index 000000000..735a87634
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BASE = "http://example.com/browser/browser/components/sessionstore/test/"
+const READER_MODE_URL = "about:reader?url=" +
+ encodeURIComponent(BASE + "browser_scrollPositions_readerModeArticle.html");
+
+// Randomized set of scroll positions we will use in this test.
+const SCROLL_READER_MODE_Y = Math.round(400 * (1 + Math.random()));
+const SCROLL_READER_MODE_STR = "0," + SCROLL_READER_MODE_Y;
+
+requestLongerTimeout(2);
+
+/**
+ * Test that scroll positions of about reader page after restoring background
+ * tabs in a restored window (bug 1153393).
+ */
+add_task(function test_scroll_background_about_reader_tabs() {
+ pushPrefs(["browser.sessionstore.restore_on_demand", true]);
+
+ let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+ let tab = newWin.gBrowser.addTab(READER_MODE_URL);
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.browserLoaded(browser),
+ BrowserTestUtils.waitForContentEvent(browser, "AboutReaderContentReady")
+ ]);
+
+ // Scroll down a little.
+ yield sendMessage(browser, "ss-test:setScrollPosition", {x: 0, y: SCROLL_READER_MODE_Y});
+ yield checkScroll(tab, {scroll: SCROLL_READER_MODE_STR}, "scroll is fine");
+
+ // Close the window
+ yield BrowserTestUtils.closeWindow(newWin);
+
+ // Now restore the window
+ newWin = ss.undoCloseWindow(0);
+
+ // Make sure to wait for the window to be restored.
+ yield BrowserTestUtils.waitForEvent(newWin, "SSWindowStateReady");
+
+ is(newWin.gBrowser.tabs.length, 2, "There should be two tabs");
+
+ // The second tab should be the one we loaded URL at still
+ tab = newWin.gBrowser.tabs[1];
+ yield promiseTabRestoring(tab);
+
+ ok(tab.hasAttribute("pending"), "Tab should be pending");
+ browser = tab.linkedBrowser;
+
+ // Ensure there are no pending queued messages in the child.
+ yield TabStateFlusher.flush(browser);
+
+ // Now check to see if the background tab remembers where it
+ // should be scrolled to.
+ newWin.gBrowser.selectedTab = tab;
+ yield Promise.all([
+ promiseTabRestored(tab),
+ BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "AboutReaderContentReady")
+ ]);
+
+ yield checkScroll(tab, {scroll: SCROLL_READER_MODE_STR}, "scroll is still fine");
+
+ yield BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html b/browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html
new file mode 100644
index 000000000..55452e043
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_scrollPositions_sample.html b/browser/components/sessionstore/test/browser_scrollPositions_sample.html
new file mode 100644
index 000000000..0182783db
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions_sample.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_scrollPositions_sample.html</title>
+ </head>
+ <body style='width: 100000px; height: 100000px;'>top</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html b/browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html
new file mode 100644
index 000000000..c7e363fa1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_scrollPositions_sample_frameset.html</title>
+ </head>
+ <frameset id="frames" rows="50%, 50%">
+ <frame src="browser_scrollPositions_sample.html">
+ <frame src="browser_scrollPositions_sample.html">
+ </frameset>
+</html>
diff --git a/browser/components/sessionstore/test/browser_send_async_message_oom.js b/browser/components/sessionstore/test/browser_send_async_message_oom.js
new file mode 100644
index 000000000..6afd771db
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_send_async_message_oom.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const HISTOGRAM_NAME = "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM";
+
+/**
+ * Test that an OOM in sendAsyncMessage in a framescript will be reported
+ * to Telemetry.
+ */
+
+add_task(function* init() {
+ Services.telemetry.canRecordExtended = true;
+});
+
+function frameScript() {
+ // Make send[A]syncMessage("SessionStore:update", ...) simulate OOM.
+ // Other operations are unaffected.
+ let mm = docShell.sameTypeRootTreeItem.
+ QueryInterface(Ci.nsIDocShell).
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIContentFrameMessageManager);
+
+ let wrap = function(original) {
+ return function(name, ...args) {
+ if (name != "SessionStore:update") {
+ return original(name, ...args);
+ }
+ throw new Components.Exception("Simulated OOM", Cr.NS_ERROR_OUT_OF_MEMORY);
+ }
+ }
+
+ mm.sendAsyncMessage = wrap(mm.sendAsyncMessage);
+ mm.sendSyncMessage = wrap(mm.sendSyncMessage);
+}
+
+add_task(function*() {
+ // Capture original state.
+ let snapshot = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot();
+
+ // Open a browser, configure it to cause OOM.
+ let newTab = gBrowser.addTab("about:robots");
+ let browser = newTab.linkedBrowser;
+ yield ContentTask.spawn(browser, null, frameScript);
+
+
+ let promiseReported = new Promise(resolve => {
+ browser.messageManager.addMessageListener("SessionStore:error", resolve);
+ });
+
+ // Attempt to flush. This should fail.
+ let promiseFlushed = TabStateFlusher.flush(browser);
+ promiseFlushed.then((success) => {
+ if (success) {
+ throw new Error("Flush should have failed")
+ }
+ });
+
+ // The frame script should report an error.
+ yield promiseReported;
+
+ // Give us some time to handle that error.
+ yield new Promise(resolve => setTimeout(resolve, 10));
+
+ // By now, Telemetry should have been updated.
+ let snapshot2 = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot();
+ gBrowser.removeTab(newTab);
+
+ Assert.ok(snapshot2.sum > snapshot.sum);
+});
+
+add_task(function* cleanup() {
+ Services.telemetry.canRecordExtended = false;
+});
diff --git a/browser/components/sessionstore/test/browser_sessionHistory.js b/browser/components/sessionstore/test/browser_sessionHistory.js
new file mode 100644
index 000000000..f4523e06a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionHistory.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+/**
+ * Ensure that starting a load invalidates shistory.
+ */
+add_task(function test_load_start() {
+ // Create a new tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Load a new URI.
+ yield BrowserTestUtils.loadURI(browser, "about:mozilla");
+
+ // Remove the tab before it has finished loading.
+ yield promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry");
+ yield promiseRemoveTab(tab);
+
+ // Undo close the tab.
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ yield promiseTabRestored(tab);
+
+ // Check that the correct URL was restored.
+ is(browser.currentURI.spec, "about:mozilla", "url is correct");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that anchor navigation invalidates shistory.
+ */
+add_task(function test_hashchange() {
+ const URL = "data:text/html;charset=utf-8,<a id=a href=%23>clickme</a>";
+
+ // Create a new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Check that we start with a single shistory entry.
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is one shistory entry");
+
+ // Click the link and wait for a hashchange event.
+ browser.messageManager.sendAsyncMessage("ss-test:click", {id: "a"});
+ yield promiseContentMessage(browser, "ss-test:hashchange");
+
+ // Check that we now have two shistory entries.
+ yield TabStateFlusher.flush(browser);
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that loading pages from the bfcache invalidates shistory.
+ */
+add_task(function test_pageshow() {
+ const URL = "data:text/html;charset=utf-8,<h1>first</h1>";
+ const URL2 = "data:text/html;charset=utf-8,<h1>second</h1>";
+
+ // Create a new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Create a second shistory entry.
+ browser.loadURI(URL2);
+ yield promiseBrowserLoaded(browser);
+
+ // Go back to the previous url which is loaded from the bfcache.
+ browser.goBack();
+ yield promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
+ is(browser.currentURI.spec, URL, "correct url after going back");
+
+ // Check that loading from bfcache did invalidate shistory.
+ yield TabStateFlusher.flush(browser);
+ let {index} = JSON.parse(ss.getTabState(tab));
+ is(index, 1, "first history entry is selected");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that subframe navigation invalidates shistory.
+ */
+add_task(function test_subframes() {
+ const URL = "data:text/html;charset=utf-8," +
+ "<iframe src=http%3A//example.com/ name=t></iframe>" +
+ "<a id=a1 href=http%3A//example.com/1 target=t>clickme</a>" +
+ "<a id=a2 href=http%3A//example.com/%23 target=t>clickme</a>";
+
+ // Create a new tab.
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Check that we have a single shistory entry.
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is one shistory entry");
+ is(entries[0].children.length, 1, "the entry has one child");
+
+ // Navigate the subframe.
+ browser.messageManager.sendAsyncMessage("ss-test:click", {id: "a1"});
+ yield promiseBrowserLoaded(browser, false /* don't ignore subframes */);
+
+ // Check shistory.
+ yield TabStateFlusher.flush(browser);
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there now are two shistory entries");
+ is(entries[1].children.length, 1, "the second entry has one child");
+
+ // Go back in history.
+ browser.goBack();
+ yield promiseBrowserLoaded(browser, false /* don't ignore subframes */);
+
+ // Navigate the subframe again.
+ browser.messageManager.sendAsyncMessage("ss-test:click", {id: "a2"});
+ yield promiseContentMessage(browser, "ss-test:hashchange");
+
+ // Check shistory.
+ yield TabStateFlusher.flush(browser);
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there now are two shistory entries");
+ is(entries[1].children.length, 1, "the second entry has one child");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that navigating from an about page invalidates shistory.
+ */
+add_task(function test_about_page_navigate() {
+ // Create a new tab.
+ let tab = gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Check that we have a single shistory entry.
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is one shistory entry");
+ is(entries[0].url, "about:blank", "url is correct");
+
+ browser.loadURI("about:robots");
+ yield promiseBrowserLoaded(browser);
+
+ // Check that we have changed the history entry.
+ yield TabStateFlusher.flush(browser);
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 1, "there is one shistory entry");
+ is(entries[0].url, "about:robots", "url is correct");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that history.pushState and history.replaceState invalidate shistory.
+ */
+add_task(function test_pushstate_replacestate() {
+ // Create a new tab.
+ let tab = gBrowser.addTab("http://example.com/1");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Check that we have a single shistory entry.
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is one shistory entry");
+ is(entries[0].url, "http://example.com/1", "url is correct");
+
+ yield ContentTask.spawn(browser, {}, function* () {
+ content.window.history.pushState({}, "", 'test-entry/');
+ });
+
+ // Check that we have added the history entry.
+ yield TabStateFlusher.flush(browser);
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there is another shistory entry");
+ is(entries[1].url, "http://example.com/test-entry/", "url is correct");
+
+ yield ContentTask.spawn(browser, {}, function* () {
+ content.window.history.replaceState({}, "", "test-entry2/");
+ });
+
+ // Check that we have modified the history entry.
+ yield TabStateFlusher.flush(browser);
+ ({entries} = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there is still two shistory entries");
+ is(entries[1].url, "http://example.com/test-entry/test-entry2/", "url is correct");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that slow loading subframes will invalidate shistory.
+ */
+add_task(function test_slow_subframe_load() {
+ const SLOW_URL = "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_sessionHistory_slow.sjs";
+
+ const URL = "data:text/html;charset=utf-8," +
+ "<frameset cols=50%25,50%25>" +
+ "<frame src='" + SLOW_URL + "'>" +
+ "</frameset>";
+
+ // Add a new tab with a slow loading subframe
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+
+ // Check the number of children.
+ is(entries.length, 1, "there is one root entry ...");
+ is(entries[0].children.length, 1, "... with one child entries");
+
+ // Check URLs.
+ ok(entries[0].url.startsWith("data:text/html"), "correct root url");
+ is(entries[0].children[0].url, SLOW_URL, "correct url for subframe");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_sessionHistory_slow.sjs b/browser/components/sessionstore/test/browser_sessionHistory_slow.sjs
new file mode 100644
index 000000000..41da3c2ad
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionHistory_slow.sjs
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const DELAY_MS = "2000";
+
+let timer;
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+ resp.setHeader("Cache-Control", "no-cache", false);
+ resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(() => {
+ resp.write("hi");
+ resp.finish();
+ }, DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+}
diff --git a/browser/components/sessionstore/test/browser_sessionStorage.html b/browser/components/sessionstore/test/browser_sessionStorage.html
new file mode 100644
index 000000000..7e2dccf4a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStorage.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_sessionStorage.html</title>
+ </head>
+ <body>
+ <script type="text/javascript;version=1.8">
+ let isOuter = window == window.top;
+ let args = window.location.search.slice(1).split("&");
+ let rand = args[0];
+
+ if (isOuter) {
+ let iframe = document.createElement("iframe");
+ let isSecure = args.indexOf("secure") > -1;
+ let scheme = isSecure ? "https" : "http";
+ iframe.setAttribute("src", scheme + "://example.com" + location.pathname + "?" + rand);
+ document.body.appendChild(iframe);
+ }
+
+ if (sessionStorage.length === 0) {
+ sessionStorage.test = (isOuter ? "outer" : "inner") + "-value-" + rand;
+ document.title = sessionStorage.test;
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_sessionStorage.js b/browser/components/sessionstore/test/browser_sessionStorage.js
new file mode 100644
index 000000000..b580c5cc2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStorage.js
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RAND = Math.random();
+const URL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_sessionStorage.html" +
+ "?" + RAND;
+
+const OUTER_VALUE = "outer-value-" + RAND;
+const INNER_VALUE = "inner-value-" + RAND;
+
+/**
+ * This test ensures that setting, modifying and restoring sessionStorage data
+ * works as expected.
+ */
+add_task(function session_storage() {
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to make sure chrome received all data.
+ yield TabStateFlusher.flush(browser);
+
+ let {storage} = JSON.parse(ss.getTabState(tab));
+ is(storage["http://example.com"].test, INNER_VALUE,
+ "sessionStorage data for example.com has been serialized correctly");
+ is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
+ "sessionStorage data for mochi.test has been serialized correctly");
+
+ // Ensure that modifying sessionStore values works for the inner frame only.
+ yield modifySessionStorage(browser, {test: "modified1"}, {frameIndex: 0});
+ yield TabStateFlusher.flush(browser);
+
+ ({storage} = JSON.parse(ss.getTabState(tab)));
+ is(storage["http://example.com"].test, "modified1",
+ "sessionStorage data for example.com has been serialized correctly");
+ is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
+ "sessionStorage data for mochi.test has been serialized correctly");
+
+ // Ensure that modifying sessionStore values works for both frames.
+ yield modifySessionStorage(browser, {test: "modified"});
+ yield modifySessionStorage(browser, {test: "modified2"}, {frameIndex: 0});
+ yield TabStateFlusher.flush(browser);
+
+ ({storage} = JSON.parse(ss.getTabState(tab)));
+ is(storage["http://example.com"].test, "modified2",
+ "sessionStorage data for example.com has been serialized correctly");
+ is(storage["http://mochi.test:8888"].test, "modified",
+ "sessionStorage data for mochi.test has been serialized correctly");
+
+ // Test that duplicating a tab works.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2);
+
+ // Flush to make sure chrome received all data.
+ yield TabStateFlusher.flush(browser2);
+
+ ({storage} = JSON.parse(ss.getTabState(tab2)));
+ is(storage["http://example.com"].test, "modified2",
+ "sessionStorage data for example.com has been duplicated correctly");
+ is(storage["http://mochi.test:8888"].test, "modified",
+ "sessionStorage data for mochi.test has been duplicated correctly");
+
+ // Ensure that the content script retains restored data
+ // (by e.g. duplicateTab) and sends it along with new data.
+ yield modifySessionStorage(browser2, {test: "modified3"});
+ yield TabStateFlusher.flush(browser2);
+
+ ({storage} = JSON.parse(ss.getTabState(tab2)));
+ is(storage["http://example.com"].test, "modified2",
+ "sessionStorage data for example.com has been duplicated correctly");
+ is(storage["http://mochi.test:8888"].test, "modified3",
+ "sessionStorage data for mochi.test has been duplicated correctly");
+
+ // Check that loading a new URL discards data.
+ browser2.loadURI("http://mochi.test:8888/");
+ yield promiseBrowserLoaded(browser2);
+ yield TabStateFlusher.flush(browser2);
+
+ ({storage} = JSON.parse(ss.getTabState(tab2)));
+ is(storage["http://mochi.test:8888"].test, "modified3",
+ "navigating retains correct storage data");
+ ok(!storage["http://example.com"], "storage data was discarded");
+
+ // Check that loading a new URL discards data.
+ browser2.loadURI("about:mozilla");
+ yield promiseBrowserLoaded(browser2);
+ yield TabStateFlusher.flush(browser2);
+
+ let state = JSON.parse(ss.getTabState(tab2));
+ ok(!state.hasOwnProperty("storage"), "storage data was discarded");
+
+ // Clean up.
+ yield promiseRemoveTab(tab);
+ yield promiseRemoveTab(tab2);
+});
+
+/**
+ * This test ensures that purging domain data also purges data from the
+ * sessionStorage data collected for tabs.
+ */
+add_task(function purge_domain() {
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Purge data for "mochi.test".
+ yield purgeDomainData(browser, "mochi.test");
+
+ // Flush to make sure chrome received all data.
+ yield TabStateFlusher.flush(browser);
+
+ let {storage} = JSON.parse(ss.getTabState(tab));
+ ok(!storage["http://mochi.test:8888"],
+ "sessionStorage data for mochi.test has been purged");
+ is(storage["http://example.com"].test, INNER_VALUE,
+ "sessionStorage data for example.com has been preserved");
+
+ yield promiseRemoveTab(tab);
+});
+
+/**
+ * This test ensures that collecting sessionStorage data respects the privacy
+ * levels as set by the user.
+ */
+add_task(function respect_privacy_level() {
+ let tab = gBrowser.addTab(URL + "&secure");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield promiseRemoveTab(tab);
+
+ let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+ is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
+ "http sessionStorage data has been saved");
+ is(storage["https://example.com"].test, INNER_VALUE,
+ "https sessionStorage data has been saved");
+
+ // Disable saving data for encrypted sites.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
+
+ tab = gBrowser.addTab(URL + "&secure");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield promiseRemoveTab(tab);
+
+ [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+ is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
+ "http sessionStorage data has been saved");
+ ok(!storage["https://example.com"],
+ "https sessionStorage data has *not* been saved");
+
+ // Disable saving data for any site.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ // Check that duplicating a tab copies all private data.
+ tab = gBrowser.addTab(URL + "&secure");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ let tab2 = gBrowser.duplicateTab(tab);
+ yield promiseTabRestored(tab2);
+ yield promiseRemoveTab(tab);
+
+ // With privacy_level=2 the |tab| shouldn't have any sessionStorage data.
+ [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+ ok(!storage, "sessionStorage data has *not* been saved");
+
+ // Remove all closed tabs before continuing with the next test.
+ // As Date.now() isn't monotonic we might sometimes check
+ // the wrong closedTabData entry.
+ while (ss.getClosedTabCount(window) > 0) {
+ ss.forgetClosedTab(window, 0);
+ }
+
+ // Restore the default privacy level and close the duplicated tab.
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+ yield promiseRemoveTab(tab2);
+
+ // With privacy_level=0 the duplicated |tab2| should persist all data.
+ [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+ is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
+ "http sessionStorage data has been saved");
+ is(storage["https://example.com"].test, INNER_VALUE,
+ "https sessionStorage data has been saved");
+});
+
+function purgeDomainData(browser, domain) {
+ return sendMessage(browser, "ss-test:purgeDomainData", domain);
+}
diff --git a/browser/components/sessionstore/test/browser_sessionStorage_size.js b/browser/components/sessionstore/test/browser_sessionStorage_size.js
new file mode 100644
index 000000000..d1d894611
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStorage_size.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const RAND = Math.random();
+const URL = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_sessionStorage.html" +
+ "?" + RAND;
+
+const OUTER_VALUE = "outer-value-" + RAND;
+
+// Test that we record the size of messages.
+add_task(function* test_telemetry() {
+ Services.telemetry.canRecordExtended = true;
+ let histogram = Services.telemetry.getHistogramById("FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS");
+ let snap1 = histogram.snapshot();
+
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to make sure chrome received all data.
+ yield TabStateFlusher.flush(browser);
+ let snap2 = histogram.snapshot();
+
+ Assert.ok(snap2.counts[5] > snap1.counts[5]);
+ yield promiseRemoveTab(tab);
+ Services.telemetry.canRecordExtended = false;
+});
+
+// Lower the size limit for DOM Storage content. Check that DOM Storage
+// is not updated, but that other things remain updated.
+add_task(function* test_large_content() {
+ Services.prefs.setIntPref("browser.sessionstore.dom_storage_limit", 5);
+
+ let tab = gBrowser.addTab(URL);
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+
+ // Flush to make sure chrome received all data.
+ yield TabStateFlusher.flush(browser);
+
+ let state = JSON.parse(ss.getTabState(tab));
+ info(JSON.stringify(state, null, "\t"));
+ Assert.equal(state.storage, null, "We have no storage for the tab");
+ Assert.equal(state.entries[0].title, OUTER_VALUE);
+ yield promiseRemoveTab(tab);
+
+ Services.prefs.clearUserPref("browser.sessionstore.dom_storage_limit");
+});
diff --git a/browser/components/sessionstore/test/browser_sessionStoreContainer.js b/browser/components/sessionstore/test/browser_sessionStoreContainer.js
new file mode 100644
index 000000000..1bc9537e2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStoreContainer.js
@@ -0,0 +1,141 @@
+/* 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/. */
+
+"use strict";
+
+add_task(function* () {
+ for (let i = 0; i < 3; ++i) {
+ let tab = gBrowser.addTab("http://example.com/", { userContextId: i });
+ let browser = tab.linkedBrowser;
+
+ yield promiseBrowserLoaded(browser);
+
+ let tab2 = gBrowser.duplicateTab(tab);
+ Assert.equal(tab2.getAttribute("usercontextid"), i);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2)
+
+ yield ContentTask.spawn(browser2, { expectedId: i }, function* (args) {
+ let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
+ Assert.equal(loadContext.originAttributes.userContextId,
+ args.expectedId, "The docShell has the correct userContextId");
+ });
+
+ yield promiseRemoveTab(tab);
+ yield promiseRemoveTab(tab2);
+ }
+});
+
+add_task(function* () {
+ let tab = gBrowser.addTab("http://example.com/", { userContextId: 1 });
+ let browser = tab.linkedBrowser;
+
+ yield promiseBrowserLoaded(browser);
+
+ gBrowser.selectedTab = tab;
+
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ yield promiseTabRestored(tab2)
+
+ yield ContentTask.spawn(browser2, { expectedId: 1 }, function* (args) {
+ Assert.equal(docShell.getOriginAttributes().userContextId,
+ args.expectedId,
+ "The docShell has the correct userContextId");
+ });
+
+ yield promiseRemoveTab(tab);
+ yield promiseRemoveTab(tab2);
+});
+
+add_task(function* () {
+ let tab = gBrowser.addTab("http://example.com/", { userContextId: 1 });
+ let browser = tab.linkedBrowser;
+
+ yield promiseBrowserLoaded(browser);
+
+ gBrowser.removeTab(tab);
+
+ let tab2 = ss.undoCloseTab(window, 0);
+ Assert.equal(tab2.getAttribute("usercontextid"), 1);
+ yield promiseTabRestored(tab2);
+ yield ContentTask.spawn(tab2.linkedBrowser, { expectedId: 1 }, function* (args) {
+ Assert.equal(docShell.getOriginAttributes().userContextId,
+ args.expectedId,
+ "The docShell has the correct userContextId");
+ });
+
+ yield promiseRemoveTab(tab2);
+});
+
+// Opens "uri" in a new tab with the provided userContextId and focuses it.
+// Returns the newly opened tab.
+function* openTabInUserContext(userContextId) {
+ // Open the tab in the correct userContextId.
+ let tab = gBrowser.addTab("http://example.com", { userContextId });
+
+ // Select tab and make sure its browser is focused.
+ gBrowser.selectedTab = tab;
+ tab.ownerGlobal.focus();
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ yield BrowserTestUtils.browserLoaded(browser);
+ return { tab, browser };
+}
+
+function waitForNewCookie() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subj, topic, data) {
+ let cookie = subj.QueryInterface(Ci.nsICookie2);
+ if (data == "added") {
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }
+ }, "cookie-changed", false);
+ });
+}
+
+add_task(function* test() {
+ const USER_CONTEXTS = [
+ "default",
+ "personal",
+ "work",
+ ];
+
+ const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ const { TabStateFlusher } = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+
+ // Make sure userContext is enabled.
+ yield SpecialPowers.pushPrefEnv({
+ "set": [ [ "privacy.userContext.enabled", true ] ]
+ });
+
+ let lastSessionRestore;
+ for (let userContextId of Object.keys(USER_CONTEXTS)) {
+ // Load the page in 3 different contexts and set a cookie
+ // which should only be visible in that context.
+ let cookie = USER_CONTEXTS[userContextId];
+
+ // Open our tab in the given user context.
+ let { tab, browser } = yield* openTabInUserContext(userContextId);
+
+ yield Promise.all([
+ waitForNewCookie(),
+ ContentTask.spawn(browser, cookie, cookie => content.document.cookie = cookie)
+ ]);
+
+ // Ensure the tab's session history is up-to-date.
+ yield TabStateFlusher.flush(browser);
+
+ lastSessionRestore = ss.getWindowState(window);
+
+ // Remove the tab.
+ gBrowser.removeTab(tab);
+ }
+
+ let state = JSON.parse(lastSessionRestore);
+ is(state.windows[0].cookies.length, USER_CONTEXTS.length,
+ "session restore should have each container's cookie");
+});
+
diff --git a/browser/components/sessionstore/test/browser_swapDocShells.js b/browser/components/sessionstore/test/browser_swapDocShells.js
new file mode 100644
index 000000000..839f060e7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_swapDocShells.js
@@ -0,0 +1,35 @@
+"use strict";
+
+add_task(function* () {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
+ yield promiseBrowserLoaded(gBrowser.selectedBrowser);
+
+ let win = gBrowser.replaceTabWithWindow(tab);
+ yield promiseDelayedStartupFinished(win);
+ yield promiseBrowserHasURL(win.gBrowser.browsers[0], "about:mozilla");
+
+ win.duplicateTabIn(win.gBrowser.selectedTab, "tab");
+ yield promiseTabRestored(win.gBrowser.tabs[1]);
+
+ let browser = win.gBrowser.browsers[1];
+ is(browser.currentURI.spec, "about:mozilla", "tab was duplicated");
+
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+function promiseDelayedStartupFinished(win) {
+ let deferred = Promise.defer();
+ whenDelayedStartupFinished(win, deferred.resolve);
+ return deferred.promise;
+}
+
+function promiseBrowserHasURL(browser, url) {
+ let promise = Promise.resolve();
+
+ if (browser.contentDocument.readyState === "complete" &&
+ browser.currentURI.spec === url) {
+ return promise;
+ }
+
+ return promise.then(() => promiseBrowserHasURL(browser, url));
+}
diff --git a/browser/components/sessionstore/test/browser_switch_remoteness.js b/browser/components/sessionstore/test/browser_switch_remoteness.js
new file mode 100644
index 000000000..9eb8c260a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_switch_remoteness.js
@@ -0,0 +1,49 @@
+"use strict";
+
+const URL = "http://example.com/browser_switch_remoteness_";
+
+function countHistoryEntries(browser, expected) {
+ return ContentTask.spawn(browser, { expected }, function* (args) {
+ let Ci = Components.interfaces;
+ let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
+ Assert.equal(history && history.count, args.expected,
+ "correct number of shistory entries");
+ });
+}
+
+add_task(function* () {
+ // Open a new window.
+ let win = yield promiseNewWindowLoaded();
+
+ // Add a new tab.
+ let tab = win.gBrowser.addTab("about:blank");
+ let browser = tab.linkedBrowser;
+ yield promiseBrowserLoaded(browser);
+ ok(browser.isRemoteBrowser, "browser is remote");
+
+ // Get the maximum number of preceding entries to save.
+ const MAX_BACK = Services.prefs.getIntPref("browser.sessionstore.max_serialize_back");
+ ok(MAX_BACK > -1, "check that the default has a value that caps data");
+
+ // Load more pages than we would save to disk on a clean shutdown.
+ for (let i = 0; i < MAX_BACK + 2; i++) {
+ browser.loadURI(URL + i);
+ yield promiseBrowserLoaded(browser);
+ ok(browser.isRemoteBrowser, "browser is still remote");
+ }
+
+ // Check we have the right number of shistory entries.
+ yield countHistoryEntries(browser, MAX_BACK + 2);
+
+ // Load a non-remote page.
+ browser.loadURI("about:robots");
+ yield promiseTabRestored(tab);
+ ok(!browser.isRemoteBrowser, "browser is not remote anymore");
+
+ // Check that we didn't lose any shistory entries.
+ yield countHistoryEntries(browser, MAX_BACK + 3);
+
+ // Cleanup.
+ yield BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/sessionstore/test/browser_undoCloseById.js b/browser/components/sessionstore/test/browser_undoCloseById.js
new file mode 100644
index 000000000..f2f0f919c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_undoCloseById.js
@@ -0,0 +1,118 @@
+"use strict";
+
+/**
+ * This test is for the undoCloseById function.
+ */
+
+Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
+
+function openAndCloseTab(window, url) {
+ let tab = window.gBrowser.addTab(url);
+ yield promiseBrowserLoaded(tab.linkedBrowser, true, url);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ yield promiseRemoveTab(tab);
+}
+
+function* openWindow(url) {
+ let win = yield promiseNewWindowLoaded();
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ win.gBrowser.selectedBrowser.loadURIWithFlags(url, flags);
+ yield promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, url);
+ return win;
+}
+
+function closeWindow(win) {
+ yield BrowserTestUtils.closeWindow(win);
+ // Wait 20 ms to allow SessionStorage a chance to register the closed window.
+ yield new Promise(resolve => setTimeout(resolve, 20));
+}
+
+add_task(function* test_undoCloseById() {
+ // Clear the lists of closed windows and tabs.
+ forgetClosedWindows();
+ while (SessionStore.getClosedTabCount(window)) {
+ SessionStore.forgetClosedTab(window, 0);
+ }
+
+ // Open a new window.
+ let win = yield openWindow("about:robots");
+
+ // Open and close a tab.
+ yield openAndCloseTab(win, "about:mozilla");
+ is(SessionStore.lastClosedObjectType, "tab", "The last closed object is a tab");
+
+ // Record the first closedId created.
+ let initialClosedId = SessionStore.getClosedTabData(win, false)[0].closedId;
+
+ // Open and close another window.
+ let win2 = yield openWindow("about:mozilla");
+ yield closeWindow(win2); // closedId == initialClosedId + 1
+ is(SessionStore.lastClosedObjectType, "window", "The last closed object is a window");
+
+ // Open and close another tab in the first window.
+ yield openAndCloseTab(win, "about:robots"); // closedId == initialClosedId + 2
+ is(SessionStore.lastClosedObjectType, "tab", "The last closed object is a tab");
+
+ // Undo closing the second tab.
+ let tab = SessionStore.undoCloseById(initialClosedId + 2);
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ is(tab.linkedBrowser.currentURI.spec, "about:robots", "The expected tab was re-opened");
+
+ let notTab = SessionStore.undoCloseById(initialClosedId + 2);
+ is(notTab, undefined, "Re-opened tab cannot be unClosed again by closedId");
+
+ // Now the last closed object should be a window again.
+ is(SessionStore.lastClosedObjectType, "window", "The last closed object is a window");
+
+ // Undo closing the first tab.
+ let tab2 = SessionStore.undoCloseById(initialClosedId);
+ yield promiseBrowserLoaded(tab2.linkedBrowser);
+ is(tab2.linkedBrowser.currentURI.spec, "about:mozilla", "The expected tab was re-opened");
+
+ // Close the two tabs we re-opened.
+ yield promiseRemoveTab(tab); // closedId == initialClosedId + 3
+ is(SessionStore.lastClosedObjectType, "tab", "The last closed object is a tab");
+ yield promiseRemoveTab(tab2); // closedId == initialClosedId + 4
+ is(SessionStore.lastClosedObjectType, "tab", "The last closed object is a tab");
+
+ // Open another new window.
+ let win3 = yield openWindow("about:mozilla");
+
+ // Close both windows.
+ yield closeWindow(win); // closedId == initialClosedId + 5
+ is(SessionStore.lastClosedObjectType, "window", "The last closed object is a window");
+ yield closeWindow(win3); // closedId == initialClosedId + 6
+ is(SessionStore.lastClosedObjectType, "window", "The last closed object is a window");
+
+ // Undo closing the second window.
+ win = SessionStore.undoCloseById(initialClosedId + 6);
+ yield BrowserTestUtils.waitForEvent(win, "load");
+
+ // Make sure we wait until this window is restored.
+ yield BrowserTestUtils.waitForEvent(win.gBrowser.tabContainer,
+ "SSTabRestored");
+
+ is(win.gBrowser.selectedBrowser.currentURI.spec, "about:mozilla", "The expected window was re-opened");
+
+ let notWin = SessionStore.undoCloseById(initialClosedId + 6);
+ is(notWin, undefined, "Re-opened window cannot be unClosed again by closedId");
+
+ // Close the window again.
+ yield closeWindow(win);
+ is(SessionStore.lastClosedObjectType, "window", "The last closed object is a window");
+
+ // Undo closing the first window.
+ win = SessionStore.undoCloseById(initialClosedId + 5);
+
+ yield BrowserTestUtils.waitForEvent(win, "load");
+
+ // Make sure we wait until this window is restored.
+ yield BrowserTestUtils.waitForEvent(win.gBrowser.tabContainer,
+ "SSTabRestored");
+
+ is(win.gBrowser.selectedBrowser.currentURI.spec, "about:robots", "The expected window was re-opened");
+
+ // Close the window again.
+ yield closeWindow(win);
+ is(SessionStore.lastClosedObjectType, "window", "The last closed object is a window");
+});
diff --git a/browser/components/sessionstore/test/browser_unrestored_crashedTabs.js b/browser/components/sessionstore/test/browser_unrestored_crashedTabs.js
new file mode 100644
index 000000000..e46348e59
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_unrestored_crashedTabs.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that if we have tabs that are still in the "click to
+ * restore" state, that if their browsers crash, that we don't
+ * show the crashed state for those tabs (since selecting them
+ * should restore them anyway).
+ */
+
+const PREF = "browser.sessionstore.restore_on_demand";
+const PAGE = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+add_task(function* test() {
+ yield pushPrefs([PREF, true]);
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ yield TabStateFlusher.flush(browser);
+
+ // We'll create a second "pending" tab. This is the one we'll
+ // ensure doesn't go to about:tabcrashed. We start it non-remote
+ // since this is how SessionStore creates all browsers before
+ // they are restored.
+ let unrestoredTab = gBrowser.addTab("about:blank", {
+ skipAnimation: true,
+ forceNotRemote: true,
+ });
+
+ let state = {
+ entries: [{url: PAGE}],
+ };
+
+ ss.setTabState(unrestoredTab, JSON.stringify(state));
+
+ ok(!unrestoredTab.hasAttribute("crashed"), "tab is not crashed");
+ ok(unrestoredTab.hasAttribute("pending"), "tab is pending");
+
+ // Now crash the selected browser.
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ ok(!unrestoredTab.hasAttribute("crashed"), "tab is still not crashed");
+ ok(unrestoredTab.hasAttribute("pending"), "tab is still pending");
+
+ // Selecting the tab should now restore it.
+ gBrowser.selectedTab = unrestoredTab;
+ yield promiseTabRestored(unrestoredTab);
+
+ ok(!unrestoredTab.hasAttribute("crashed"), "tab is still not crashed");
+ ok(!unrestoredTab.hasAttribute("pending"), "tab is no longer pending");
+
+ // The original tab should still be crashed
+ let originalTab = gBrowser.getTabForBrowser(browser);
+ ok(originalTab.hasAttribute("crashed"), "original tab is crashed");
+ ok(!originalTab.isRemoteBrowser, "Should not be remote");
+
+ // We'd better be able to restore it still.
+ gBrowser.selectedTab = originalTab;
+ SessionStore.reviveCrashedTab(originalTab);
+ yield promiseTabRestored(originalTab);
+
+ // Clean up.
+ yield BrowserTestUtils.removeTab(unrestoredTab);
+ });
+});
diff --git a/browser/components/sessionstore/test/browser_upgrade_backup.js b/browser/components/sessionstore/test/browser_upgrade_backup.js
new file mode 100644
index 000000000..768671051
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_upgrade_backup.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+
+const Paths = SessionFile.Paths;
+const PREF_UPGRADE = "browser.sessionstore.upgradeBackup.latestBuildID";
+const PREF_MAX_UPGRADE_BACKUPS = "browser.sessionstore.upgradeBackup.maxUpgradeBackups";
+
+/**
+ * Prepares tests by retrieving the current platform's build ID, clearing the
+ * build where the last backup was created and creating arbitrary JSON data
+ * for a new backup.
+ */
+var prepareTest = Task.async(function* () {
+ let result = {};
+
+ result.buildID = Services.appinfo.platformBuildID;
+ Services.prefs.setCharPref(PREF_UPGRADE, "");
+ result.contents = JSON.stringify({"browser_upgrade_backup.js": Math.random()});
+
+ return result;
+});
+
+/**
+ * Retrieves all upgrade backups and returns them in an array.
+ */
+var getUpgradeBackups = Task.async(function* () {
+ let iterator;
+ let backups = [];
+ let upgradeBackupPrefix = Paths.upgradeBackupPrefix;
+
+ try {
+ iterator = new OS.File.DirectoryIterator(Paths.backups);
+
+ // iterate over all files in the backup directory
+ yield iterator.forEach(function (file) {
+ // check the upgradeBackupPrefix
+ if (file.path.startsWith(Paths.upgradeBackupPrefix)) {
+ // the file is a backup
+ backups.push(file.path);
+ }
+ }, this);
+ } finally {
+ if (iterator) {
+ iterator.close();
+ }
+ }
+
+ // return results
+ return backups;
+});
+
+add_task(function* init() {
+ // Wait until initialization is complete
+ yield SessionStore.promiseInitialized;
+ yield SessionFile.wipe();
+});
+
+add_task(function* test_upgrade_backup() {
+ let test = yield prepareTest();
+ info("Let's check if we create an upgrade backup");
+ yield OS.File.writeAtomic(Paths.clean, test.contents);
+ yield SessionFile.read(); // First call to read() initializes the SessionWorker
+ yield SessionFile.write(""); // First call to write() triggers the backup
+
+ is(Services.prefs.getCharPref(PREF_UPGRADE), test.buildID, "upgrade backup should be set");
+
+ is((yield OS.File.exists(Paths.upgradeBackup)), true, "upgrade backup file has been created");
+
+ let data = yield OS.File.read(Paths.upgradeBackup);
+ is(test.contents, (new TextDecoder()).decode(data), "upgrade backup contains the expected contents");
+
+ info("Let's check that we don't overwrite this upgrade backup");
+ let newContents = JSON.stringify({"something else entirely": Math.random()});
+ yield OS.File.writeAtomic(Paths.clean, newContents);
+ yield SessionFile.read(); // Reinitialize the SessionWorker
+ yield SessionFile.write(""); // Next call to write() shouldn't trigger the backup
+ data = yield OS.File.read(Paths.upgradeBackup);
+ is(test.contents, (new TextDecoder()).decode(data), "upgrade backup hasn't changed");
+});
+
+add_task(function* test_upgrade_backup_removal() {
+ let test = yield prepareTest();
+ let maxUpgradeBackups = Preferences.get(PREF_MAX_UPGRADE_BACKUPS, 3);
+ info("Let's see if we remove backups if there are too many");
+ yield OS.File.writeAtomic(Paths.clean, test.contents);
+
+ // if the nextUpgradeBackup already exists (from another test), remove it
+ if (OS.File.exists(Paths.nextUpgradeBackup)) {
+ yield OS.File.remove(Paths.nextUpgradeBackup);
+ }
+
+ // create dummy backups
+ yield OS.File.writeAtomic(Paths.upgradeBackupPrefix + "20080101010101", "");
+ yield OS.File.writeAtomic(Paths.upgradeBackupPrefix + "20090101010101", "");
+ yield OS.File.writeAtomic(Paths.upgradeBackupPrefix + "20100101010101", "");
+ yield OS.File.writeAtomic(Paths.upgradeBackupPrefix + "20110101010101", "");
+ yield OS.File.writeAtomic(Paths.upgradeBackupPrefix + "20120101010101", "");
+ yield OS.File.writeAtomic(Paths.upgradeBackupPrefix + "20130101010101", "");
+
+ // get currently existing backups
+ let backups = yield getUpgradeBackups();
+
+ // trigger new backup
+ yield SessionFile.read(); // First call to read() initializes the SessionWorker
+ yield SessionFile.write(""); // First call to write() triggers the backup and the cleanup
+
+ // a new backup should have been created (and still exist)
+ is(Services.prefs.getCharPref(PREF_UPGRADE), test.buildID, "upgrade backup should be set");
+ is((yield OS.File.exists(Paths.upgradeBackup)), true, "upgrade backup file has been created");
+
+ // get currently existing backups and check their count
+ let newBackups = yield getUpgradeBackups();
+ is(newBackups.length, maxUpgradeBackups, "expected number of backups are present after removing old backups");
+
+ // find all backups that were created during the last call to `SessionFile.write("");`
+ // ie, filter out all the backups that have already been present before the call
+ newBackups = newBackups.filter(function (backup) {
+ return backups.indexOf(backup) < 0;
+ });
+
+ // check that exactly one new backup was created
+ is(newBackups.length, 1, "one new backup was created that was not removed");
+
+ yield SessionFile.write(""); // Second call to write() should not trigger anything
+
+ backups = yield getUpgradeBackups();
+ is(backups.length, maxUpgradeBackups, "second call to SessionFile.write() didn't create or remove more backups");
+});
+
diff --git a/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js b/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js
new file mode 100644
index 000000000..781692909
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js
@@ -0,0 +1,26 @@
+/* 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 test checks that closed private windows can't be restored
+
+function test() {
+ waitForExplicitFinish();
+
+ // Purging the list of closed windows
+ forgetClosedWindows();
+
+ // Load a private window, then close it
+ // and verify it doesn't get remembered for restoring
+ whenNewWindowLoaded({private: true}, function (win) {
+ info("The private window got loaded");
+ win.addEventListener("SSWindowClosing", function onclosing() {
+ win.removeEventListener("SSWindowClosing", onclosing, false);
+ executeSoon(function () {
+ is(ss.getClosedWindowCount(), 0,
+ "The private window should not have been stored");
+ });
+ }, false);
+ BrowserTestUtils.closeWindow(win).then(finish);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_windowStateContainer.js b/browser/components/sessionstore/test/browser_windowStateContainer.js
new file mode 100644
index 000000000..beb838088
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_windowStateContainer.js
@@ -0,0 +1,122 @@
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(function* () {
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+
+ // Create 4 tabs with different userContextId.
+ for (let userContextId = 1; userContextId < 5; userContextId++) {
+ let tab = win.gBrowser.addTab("http://example.com/", {userContextId});
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ }
+
+ // Move the default tab of window to the end.
+ // We want the 1st tab to have non-default userContextId, so later when we
+ // restore into win2 we can test restore into an existing tab with different
+ // userContextId.
+ win.gBrowser.moveTabTo(win.gBrowser.tabs[0], win.gBrowser.tabs.length - 1);
+
+ let winState = JSON.parse(ss.getWindowState(win));
+
+ for (let i = 0; i < 4; i++) {
+ Assert.equal(winState.windows[0].tabs[i].userContextId, i + 1,
+ "1st Window: tabs[" + i + "].userContextId should exist.");
+ }
+
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
+
+ // Create tabs with different userContextId, but this time we create them with
+ // fewer tabs and with different order with win.
+ for (let userContextId = 3; userContextId > 0; userContextId--) {
+ let tab = win2.gBrowser.addTab("http://example.com/", {userContextId});
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+ }
+
+ ss.setWindowState(win2, JSON.stringify(winState), true);
+
+ for (let i = 0; i < 4; i++) {
+ let browser = win2.gBrowser.tabs[i].linkedBrowser;
+ yield ContentTask.spawn(browser, { expectedId: i + 1 }, function* (args) {
+ Assert.equal(docShell.getOriginAttributes().userContextId,
+ args.expectedId,
+ "The docShell has the correct userContextId");
+
+ Assert.equal(content.document.nodePrincipal.originAttributes.userContextId,
+ args.expectedId,
+ "The document has the correct userContextId");
+ });
+ }
+
+ // Test the last tab, which doesn't have userContextId.
+ let browser = win2.gBrowser.tabs[4].linkedBrowser;
+ yield ContentTask.spawn(browser, { expectedId: 0 }, function* (args) {
+ Assert.equal(docShell.getOriginAttributes().userContextId,
+ args.expectedId,
+ "The docShell has the correct userContextId");
+
+ Assert.equal(content.document.nodePrincipal.originAttributes.userContextId,
+ args.expectedId,
+ "The document has the correct userContextId");
+ });
+
+ yield BrowserTestUtils.closeWindow(win);
+ yield BrowserTestUtils.closeWindow(win2);
+});
+
+add_task(function* () {
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+ yield TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+
+ let tab = win.gBrowser.addTab("http://example.com/", { userContextId: 1 });
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+
+ // win should have 1 default tab, and 1 container tab.
+ Assert.equal(win.gBrowser.tabs.length, 2, "win should have 2 tabs");
+
+ let winState = JSON.parse(ss.getWindowState(win));
+
+ for (let i = 0; i < 2; i++) {
+ Assert.equal(winState.windows[0].tabs[i].userContextId, i,
+ "1st Window: tabs[" + i + "].userContextId should be " + i);
+ }
+
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
+
+ let tab2 = win2.gBrowser.addTab("http://example.com/", { userContextId : 1 });
+ yield promiseBrowserLoaded(tab2.linkedBrowser);
+ yield TabStateFlusher.flush(tab2.linkedBrowser);
+
+ // Move the first normal tab to end, so the first tab of win2 will be a
+ // container tab.
+ win2.gBrowser.moveTabTo(win2.gBrowser.tabs[0], win2.gBrowser.tabs.length - 1);
+ yield TabStateFlusher.flush(win2.gBrowser.tabs[0].linkedBrowser);
+
+ ss.setWindowState(win2, JSON.stringify(winState), true);
+
+ for (let i = 0; i < 2; i++) {
+ let browser = win2.gBrowser.tabs[i].linkedBrowser;
+ yield ContentTask.spawn(browser, { expectedId: i }, function* (args) {
+ Assert.equal(docShell.getOriginAttributes().userContextId,
+ args.expectedId,
+ "The docShell has the correct userContextId");
+
+ Assert.equal(content.document.nodePrincipal.originAttributes.userContextId,
+ args.expectedId,
+ "The document has the correct userContextId");
+ });
+ }
+
+ yield BrowserTestUtils.closeWindow(win);
+ yield BrowserTestUtils.closeWindow(win2);
+});
+
diff --git a/browser/components/sessionstore/test/content-forms.js b/browser/components/sessionstore/test/content-forms.js
new file mode 100644
index 000000000..da7bc9c08
--- /dev/null
+++ b/browser/components/sessionstore/test/content-forms.js
@@ -0,0 +1,133 @@
+/* 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/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/**
+ * This frame script is only loaded for sessionstore mochitests. It contains
+ * a bunch of utility functions used to test form data collection and
+ * restoration in remote browsers.
+ */
+
+function queryElement(data) {
+ let frame = content;
+ if (data.hasOwnProperty("frame")) {
+ frame = content.frames[data.frame];
+ }
+
+ let doc = frame.document;
+
+ if (data.hasOwnProperty("id")) {
+ return doc.getElementById(data.id);
+ }
+
+ if (data.hasOwnProperty("selector")) {
+ return doc.querySelector(data.selector);
+ }
+
+ if (data.hasOwnProperty("xpath")) {
+ let xptype = Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE;
+ return doc.evaluate(data.xpath, doc, null, xptype, null).singleNodeValue;
+ }
+
+ throw new Error("couldn't query element");
+}
+
+function dispatchUIEvent(input, type) {
+ let event = input.ownerDocument.createEvent("UIEvents");
+ event.initUIEvent(type, true, true, input.ownerGlobal, 0);
+ input.dispatchEvent(event);
+}
+
+function defineListener(type, cb) {
+ addMessageListener("ss-test:" + type, function ({data}) {
+ sendAsyncMessage("ss-test:" + type, cb(data));
+ });
+}
+
+defineListener("sendKeyEvent", function (data) {
+ let frame = content;
+ if (data.hasOwnProperty("frame")) {
+ frame = content.frames[data.frame];
+ }
+
+ let ifreq = frame.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+
+ let keyCode = data.key.charCodeAt(0);
+ let charCode = Ci.nsIDOMKeyEvent.DOM_VK_A + keyCode - "a".charCodeAt(0);
+
+ utils.sendKeyEvent("keydown", keyCode, charCode, null);
+ utils.sendKeyEvent("keypress", keyCode, charCode, null);
+ utils.sendKeyEvent("keyup", keyCode, charCode, null);
+});
+
+defineListener("getInnerHTML", function (data) {
+ return queryElement(data).innerHTML;
+});
+
+defineListener("getTextContent", function (data) {
+ return queryElement(data).textContent;
+});
+
+defineListener("getInputValue", function (data) {
+ return queryElement(data).value;
+});
+
+defineListener("setInputValue", function (data) {
+ let input = queryElement(data);
+ input.value = data.value;
+ dispatchUIEvent(input, "input");
+});
+
+defineListener("getInputChecked", function (data) {
+ return queryElement(data).checked;
+});
+
+defineListener("setInputChecked", function (data) {
+ let input = queryElement(data);
+ input.checked = data.checked;
+ dispatchUIEvent(input, "change");
+});
+
+defineListener("getSelectedIndex", function (data) {
+ return queryElement(data).selectedIndex;
+});
+
+defineListener("setSelectedIndex", function (data) {
+ let input = queryElement(data);
+ input.selectedIndex = data.index;
+ dispatchUIEvent(input, "change");
+});
+
+defineListener("getMultipleSelected", function (data) {
+ let input = queryElement(data);
+ return Array.map(input.options, (opt, idx) => idx)
+ .filter(idx => input.options[idx].selected);
+});
+
+defineListener("setMultipleSelected", function (data) {
+ let input = queryElement(data);
+ Array.forEach(input.options, (opt, idx) => opt.selected = data.indices.indexOf(idx) > -1);
+ dispatchUIEvent(input, "change");
+});
+
+defineListener("getFileNameArray", function (data) {
+ return queryElement(data).mozGetFileNameArray();
+});
+
+defineListener("setFileNameArray", function (data) {
+ let input = queryElement(data);
+ input.mozSetFileNameArray(data.names, data.names.length);
+ dispatchUIEvent(input, "input");
+});
+
+defineListener("setFormElementValues", function (data) {
+ for (let elem of content.document.forms[0].elements) {
+ elem.value = data.value;
+ dispatchUIEvent(elem, "input");
+ }
+});
diff --git a/browser/components/sessionstore/test/content.js b/browser/components/sessionstore/test/content.js
new file mode 100644
index 000000000..e815a6783
--- /dev/null
+++ b/browser/components/sessionstore/test/content.js
@@ -0,0 +1,222 @@
+/* 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/. */
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
+var gFrameTree = new FrameTree(this);
+
+function executeSoon(callback) {
+ Services.tm.mainThread.dispatch(callback, Components.interfaces.nsIThread.DISPATCH_NORMAL);
+}
+
+gFrameTree.addObserver({
+ onFrameTreeReset: function () {
+ sendAsyncMessage("ss-test:onFrameTreeReset");
+ },
+
+ onFrameTreeCollected: function () {
+ sendAsyncMessage("ss-test:onFrameTreeCollected");
+ }
+});
+
+var historyListener = {
+ OnHistoryNewEntry: function () {
+ sendAsyncMessage("ss-test:OnHistoryNewEntry");
+ },
+
+ OnHistoryGoBack: function () {
+ sendAsyncMessage("ss-test:OnHistoryGoBack");
+ return true;
+ },
+
+ OnHistoryGoForward: function () {
+ sendAsyncMessage("ss-test:OnHistoryGoForward");
+ return true;
+ },
+
+ OnHistoryGotoIndex: function () {
+ sendAsyncMessage("ss-test:OnHistoryGotoIndex");
+ return true;
+ },
+
+ OnHistoryPurge: function () {
+ sendAsyncMessage("ss-test:OnHistoryPurge");
+ return true;
+ },
+
+ OnHistoryReload: function () {
+ sendAsyncMessage("ss-test:OnHistoryReload");
+ return true;
+ },
+
+ OnHistoryReplaceEntry: function () {
+ sendAsyncMessage("ss-test:OnHistoryReplaceEntry");
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsISHistoryListener,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+var {sessionHistory} = docShell.QueryInterface(Ci.nsIWebNavigation);
+if (sessionHistory) {
+ sessionHistory.addSHistoryListener(historyListener);
+}
+
+/**
+ * This frame script is only loaded for sessionstore mochitests. It enables us
+ * to modify and query docShell data when running with multiple processes.
+ */
+
+addEventListener("hashchange", function () {
+ sendAsyncMessage("ss-test:hashchange");
+});
+
+addMessageListener("ss-test:purgeDomainData", function ({data: domain}) {
+ Services.obs.notifyObservers(null, "browser:purge-domain-data", domain);
+ content.setTimeout(() => sendAsyncMessage("ss-test:purgeDomainData"));
+});
+
+addMessageListener("ss-test:getStyleSheets", function (msg) {
+ let sheets = content.document.styleSheets;
+ let titles = Array.map(sheets, ss => [ss.title, ss.disabled]);
+ sendSyncMessage("ss-test:getStyleSheets", titles);
+});
+
+addMessageListener("ss-test:enableStyleSheetsForSet", function (msg) {
+ let sheets = content.document.styleSheets;
+ let change = false;
+ for (let i = 0; i < sheets.length; i++) {
+ if (sheets[i].disabled != (msg.data.indexOf(sheets[i].title) == -1)) {
+ change = true;
+ break;
+ }
+ }
+ function observer() {
+ Services.obs.removeObserver(observer, "style-sheet-applicable-state-changed");
+
+ // It's possible our observer will run before the one in
+ // content-sessionStore.js. Therefore, we run ours a little
+ // later.
+ executeSoon(() => sendAsyncMessage("ss-test:enableStyleSheetsForSet"));
+ }
+ if (change) {
+ // We don't want to reply until content-sessionStore.js has seen
+ // the change.
+ Services.obs.addObserver(observer, "style-sheet-applicable-state-changed", false);
+
+ content.document.enableStyleSheetsForSet(msg.data);
+ } else {
+ sendAsyncMessage("ss-test:enableStyleSheetsForSet");
+ }
+});
+
+addMessageListener("ss-test:enableSubDocumentStyleSheetsForSet", function (msg) {
+ let iframe = content.document.getElementById(msg.data.id);
+ iframe.contentDocument.enableStyleSheetsForSet(msg.data.set);
+ sendAsyncMessage("ss-test:enableSubDocumentStyleSheetsForSet");
+});
+
+addMessageListener("ss-test:getAuthorStyleDisabled", function (msg) {
+ let {authorStyleDisabled} =
+ docShell.contentViewer;
+ sendSyncMessage("ss-test:getAuthorStyleDisabled", authorStyleDisabled);
+});
+
+addMessageListener("ss-test:setAuthorStyleDisabled", function (msg) {
+ let markupDocumentViewer =
+ docShell.contentViewer;
+ markupDocumentViewer.authorStyleDisabled = msg.data;
+ sendSyncMessage("ss-test:setAuthorStyleDisabled");
+});
+
+addMessageListener("ss-test:setUsePrivateBrowsing", function (msg) {
+ let loadContext =
+ docShell.QueryInterface(Ci.nsILoadContext);
+ loadContext.usePrivateBrowsing = msg.data;
+ sendAsyncMessage("ss-test:setUsePrivateBrowsing");
+});
+
+addMessageListener("ss-test:getScrollPosition", function (msg) {
+ let frame = content;
+ if (msg.data.hasOwnProperty("frame")) {
+ frame = content.frames[msg.data.frame];
+ }
+ let {scrollX: x, scrollY: y} = frame;
+ sendAsyncMessage("ss-test:getScrollPosition", {x: x, y: y});
+});
+
+addMessageListener("ss-test:setScrollPosition", function (msg) {
+ let frame = content;
+ let {x, y} = msg.data;
+ if (msg.data.hasOwnProperty("frame")) {
+ frame = content.frames[msg.data.frame];
+ }
+ frame.scrollTo(x, y);
+
+ frame.addEventListener("scroll", function onScroll(event) {
+ if (frame.document == event.target) {
+ frame.removeEventListener("scroll", onScroll);
+ sendAsyncMessage("ss-test:setScrollPosition");
+ }
+ });
+});
+
+addMessageListener("ss-test:createDynamicFrames", function ({data}) {
+ function createIFrame(rows) {
+ let frames = content.document.getElementById(data.id);
+ frames.setAttribute("rows", rows);
+
+ let frame = content.document.createElement("frame");
+ frame.setAttribute("src", data.url);
+ frames.appendChild(frame);
+ }
+
+ addEventListener("DOMContentLoaded", function onContentLoaded(event) {
+ if (content.document == event.target) {
+ removeEventListener("DOMContentLoaded", onContentLoaded, true);
+ // DOMContentLoaded is fired right after we finished parsing the document.
+ createIFrame("33%, 33%, 33%");
+ }
+ }, true);
+
+ addEventListener("load", function onLoad(event) {
+ if (content.document == event.target) {
+ removeEventListener("load", onLoad, true);
+
+ // Creating this frame on the same tick as the load event
+ // means that it must not be included in the frame tree.
+ createIFrame("25%, 25%, 25%, 25%");
+ }
+ }, true);
+
+ sendAsyncMessage("ss-test:createDynamicFrames");
+});
+
+addMessageListener("ss-test:removeLastFrame", function ({data}) {
+ let frames = content.document.getElementById(data.id);
+ frames.lastElementChild.remove();
+ sendAsyncMessage("ss-test:removeLastFrame");
+});
+
+addMessageListener("ss-test:mapFrameTree", function (msg) {
+ let result = gFrameTree.map(frame => ({href: frame.location.href}));
+ sendAsyncMessage("ss-test:mapFrameTree", result);
+});
+
+addMessageListener("ss-test:click", function ({data}) {
+ content.document.getElementById(data.id).click();
+ sendAsyncMessage("ss-test:click");
+});
+
+addEventListener("load", function(event) {
+ let subframe = event.target != content.document;
+ sendAsyncMessage("ss-test:loadEvent", {subframe: subframe, url: event.target.documentURI});
+}, true);
diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js
new file mode 100644
index 000000000..5a8c5dbfc
--- /dev/null
+++ b/browser/components/sessionstore/test/head.js
@@ -0,0 +1,564 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const TAB_STATE_NEEDS_RESTORE = 1;
+const TAB_STATE_RESTORING = 2;
+
+const ROOT = getRootDirectory(gTestPath);
+const HTTPROOT = ROOT.replace("chrome://mochitests/content/", "http://example.com/");
+const FRAME_SCRIPTS = [
+ ROOT + "content.js",
+ ROOT + "content-forms.js"
+];
+
+var mm = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+
+for (let script of FRAME_SCRIPTS) {
+ mm.loadFrameScript(script, true);
+}
+
+registerCleanupFunction(() => {
+ for (let script of FRAME_SCRIPTS) {
+ mm.removeDelayedFrameScript(script, true);
+ }
+});
+
+const {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {SessionStore} = Cu.import("resource:///modules/sessionstore/SessionStore.jsm", {});
+const {SessionSaver} = Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", {});
+const {SessionFile} = Cu.import("resource:///modules/sessionstore/SessionFile.jsm", {});
+const {TabState} = Cu.import("resource:///modules/sessionstore/TabState.jsm", {});
+const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+
+const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+
+// Some tests here assume that all restored tabs are loaded without waiting for
+// the user to bring them to the foreground. We ensure this by resetting the
+// related preference (see the "firefox.js" defaults file for details).
+Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+});
+
+// Obtain access to internals
+Services.prefs.setBoolPref("browser.sessionstore.debug", true);
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.debug");
+});
+
+
+// This kicks off the search service used on about:home and allows the
+// session restore tests to be run standalone without triggering errors.
+Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs;
+
+function provideWindow(aCallback, aURL, aFeatures) {
+ function callbackSoon(aWindow) {
+ executeSoon(function executeCallbackSoon() {
+ aCallback(aWindow);
+ });
+ }
+
+ let win = openDialog(getBrowserURL(), "", aFeatures || "chrome,all,dialog=no", aURL || "about:blank");
+ whenWindowLoaded(win, function onWindowLoaded(aWin) {
+ if (!aURL) {
+ info("Loaded a blank window.");
+ callbackSoon(aWin);
+ return;
+ }
+
+ aWin.gBrowser.selectedBrowser.addEventListener("load", function selectedBrowserLoadListener() {
+ aWin.gBrowser.selectedBrowser.removeEventListener("load", selectedBrowserLoadListener, true);
+ callbackSoon(aWin);
+ }, true);
+ });
+}
+
+// This assumes that tests will at least have some state/entries
+function waitForBrowserState(aState, aSetStateCallback) {
+ if (typeof aState == "string") {
+ aState = JSON.parse(aState);
+ }
+ if (typeof aState != "object") {
+ throw new TypeError("Argument must be an object or a JSON representation of an object");
+ }
+ let windows = [window];
+ let tabsRestored = 0;
+ let expectedTabsRestored = 0;
+ let expectedWindows = aState.windows.length;
+ let windowsOpen = 1;
+ let listening = false;
+ let windowObserving = false;
+ let restoreHiddenTabs = Services.prefs.getBoolPref(
+ "browser.sessionstore.restore_hidden_tabs");
+
+ aState.windows.forEach(function (winState) {
+ winState.tabs.forEach(function (tabState) {
+ if (restoreHiddenTabs || !tabState.hidden)
+ expectedTabsRestored++;
+ });
+ });
+
+ // There must be only hidden tabs and restoreHiddenTabs = false. We still
+ // expect one of them to be restored because it gets shown automatically.
+ if (!expectedTabsRestored)
+ expectedTabsRestored = 1;
+
+ function onSSTabRestored(aEvent) {
+ if (++tabsRestored == expectedTabsRestored) {
+ // Remove the event listener from each window
+ windows.forEach(function(win) {
+ win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true);
+ });
+ listening = false;
+ info("running " + aSetStateCallback.name);
+ executeSoon(aSetStateCallback);
+ }
+ }
+
+ // Used to add our listener to further windows so we can catch SSTabRestored
+ // coming from them when creating a multi-window state.
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ newWindow.addEventListener("load", function() {
+ newWindow.removeEventListener("load", arguments.callee, false);
+
+ if (++windowsOpen == expectedWindows) {
+ Services.ww.unregisterNotification(windowObserver);
+ windowObserving = false;
+ }
+
+ // Track this window so we can remove the progress listener later
+ windows.push(newWindow);
+ // Add the progress listener
+ newWindow.gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true);
+ }, false);
+ }
+ }
+
+ // We only want to register the notification if we expect more than 1 window
+ if (expectedWindows > 1) {
+ registerCleanupFunction(function() {
+ if (windowObserving) {
+ Services.ww.unregisterNotification(windowObserver);
+ }
+ });
+ windowObserving = true;
+ Services.ww.registerNotification(windowObserver);
+ }
+
+ registerCleanupFunction(function() {
+ if (listening) {
+ windows.forEach(function(win) {
+ win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true);
+ });
+ }
+ });
+ // Add the event listener for this window as well.
+ listening = true;
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true);
+
+ // Ensure setBrowserState() doesn't remove the initial tab.
+ gBrowser.selectedTab = gBrowser.tabs[0];
+
+ // Finally, call setBrowserState
+ ss.setBrowserState(JSON.stringify(aState));
+}
+
+function promiseBrowserState(aState) {
+ return new Promise(resolve => waitForBrowserState(aState, resolve));
+}
+
+function promiseTabState(tab, state) {
+ if (typeof(state) != "string") {
+ state = JSON.stringify(state);
+ }
+
+ let promise = promiseTabRestored(tab);
+ ss.setTabState(tab, state);
+ return promise;
+}
+
+/**
+ * Wait for a content -> chrome message.
+ */
+function promiseContentMessage(browser, name) {
+ let mm = browser.messageManager;
+
+ return new Promise(resolve => {
+ function removeListener() {
+ mm.removeMessageListener(name, listener);
+ }
+
+ function listener(msg) {
+ removeListener();
+ resolve(msg.data);
+ }
+
+ mm.addMessageListener(name, listener);
+ registerCleanupFunction(removeListener);
+ });
+}
+
+function waitForTopic(aTopic, aTimeout, aCallback) {
+ let observing = false;
+ function removeObserver() {
+ if (!observing)
+ return;
+ Services.obs.removeObserver(observer, aTopic);
+ observing = false;
+ }
+
+ let timeout = setTimeout(function () {
+ removeObserver();
+ aCallback(false);
+ }, aTimeout);
+
+ function observer(aSubject, aTopic, aData) {
+ removeObserver();
+ timeout = clearTimeout(timeout);
+ executeSoon(() => aCallback(true));
+ }
+
+ registerCleanupFunction(function() {
+ removeObserver();
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ });
+
+ observing = true;
+ Services.obs.addObserver(observer, aTopic, false);
+}
+
+/**
+ * Wait until session restore has finished collecting its data and is
+ * has written that data ("sessionstore-state-write-complete").
+ *
+ * @param {function} aCallback If sessionstore-state-write-complete is sent
+ * within buffering interval + 100 ms, the callback is passed |true|,
+ * otherwise, it is passed |false|.
+ */
+function waitForSaveState(aCallback) {
+ let timeout = 100 +
+ Services.prefs.getIntPref("browser.sessionstore.interval");
+ return waitForTopic("sessionstore-state-write-complete", timeout, aCallback);
+}
+function promiseSaveState() {
+ return new Promise(resolve => {
+ waitForSaveState(isSuccessful => {
+ if (!isSuccessful) {
+ throw new Error("timeout");
+ }
+
+ resolve();
+ });
+ });
+}
+function forceSaveState() {
+ return SessionSaver.run();
+}
+
+function promiseRecoveryFileContents() {
+ let promise = forceSaveState();
+ return promise.then(function() {
+ return OS.File.read(SessionFile.Paths.recovery, { encoding: "utf-8" });
+ });
+}
+
+var promiseForEachSessionRestoreFile = Task.async(function*(cb) {
+ for (let key of SessionFile.Paths.loadOrder) {
+ let data = "";
+ try {
+ data = yield OS.File.read(SessionFile.Paths[key], { encoding: "utf-8" });
+ } catch (ex) {
+ // Ignore missing files
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ throw ex;
+ }
+ }
+ cb(data, key);
+ }
+});
+
+function promiseBrowserLoaded(aBrowser, ignoreSubFrames = true, wantLoad = null) {
+ return BrowserTestUtils.browserLoaded(aBrowser, !ignoreSubFrames, wantLoad);
+}
+
+function whenWindowLoaded(aWindow, aCallback = next) {
+ aWindow.addEventListener("load", function windowLoadListener() {
+ aWindow.removeEventListener("load", windowLoadListener, false);
+ executeSoon(function executeWhenWindowLoaded() {
+ aCallback(aWindow);
+ });
+ }, false);
+}
+function promiseWindowLoaded(aWindow) {
+ return new Promise(resolve => whenWindowLoaded(aWindow, resolve));
+}
+
+var gUniqueCounter = 0;
+function r() {
+ return Date.now() + "-" + (++gUniqueCounter);
+}
+
+function* BrowserWindowIterator() {
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ let currentWindow = windowsEnum.getNext();
+ if (!currentWindow.closed) {
+ yield currentWindow;
+ }
+ }
+}
+
+var gWebProgressListener = {
+ _callback: null,
+
+ setCallback: function (aCallback) {
+ if (!this._callback) {
+ window.gBrowser.addTabsProgressListener(this);
+ }
+ this._callback = aCallback;
+ },
+
+ unsetCallback: function () {
+ if (this._callback) {
+ this._callback = null;
+ window.gBrowser.removeTabsProgressListener(this);
+ }
+ },
+
+ onStateChange: function (aBrowser, aWebProgress, aRequest,
+ aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ this._callback(aBrowser);
+ }
+ }
+};
+
+registerCleanupFunction(function () {
+ gWebProgressListener.unsetCallback();
+});
+
+var gProgressListener = {
+ _callback: null,
+
+ setCallback: function (callback) {
+ Services.obs.addObserver(this, "sessionstore-debug-tab-restored", false);
+ this._callback = callback;
+ },
+
+ unsetCallback: function () {
+ if (this._callback) {
+ this._callback = null;
+ Services.obs.removeObserver(this, "sessionstore-debug-tab-restored");
+ }
+ },
+
+ observe: function (browser, topic, data) {
+ gProgressListener.onRestored(browser);
+ },
+
+ onRestored: function (browser) {
+ if (browser.__SS_restoreState == TAB_STATE_RESTORING) {
+ let args = [browser].concat(gProgressListener._countTabs());
+ gProgressListener._callback.apply(gProgressListener, args);
+ }
+ },
+
+ _countTabs: function () {
+ let needsRestore = 0, isRestoring = 0, wasRestored = 0;
+
+ for (let win of BrowserWindowIterator()) {
+ for (let i = 0; i < win.gBrowser.tabs.length; i++) {
+ let browser = win.gBrowser.tabs[i].linkedBrowser;
+ if (!browser.__SS_restoreState)
+ wasRestored++;
+ else if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+ isRestoring++;
+ else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+ needsRestore++;
+ }
+ }
+ return [needsRestore, isRestoring, wasRestored];
+ }
+};
+
+registerCleanupFunction(function () {
+ gProgressListener.unsetCallback();
+});
+
+// Close all but our primary window.
+function promiseAllButPrimaryWindowClosed() {
+ let windows = [];
+ for (let win of BrowserWindowIterator()) {
+ if (win != window) {
+ windows.push(win);
+ }
+ }
+
+ return Promise.all(windows.map(BrowserTestUtils.closeWindow));
+}
+
+// Forget all closed windows.
+function forgetClosedWindows() {
+ while (ss.getClosedWindowCount() > 0) {
+ ss.forgetClosedWindow(0);
+ }
+}
+
+/**
+ * When opening a new window it is not sufficient to wait for its load event.
+ * We need to use whenDelayedStartupFinshed() here as the browser window's
+ * delayedStartup() routine is executed one tick after the window's load event
+ * has been dispatched. browser-delayed-startup-finished might be deferred even
+ * further if parts of the window's initialization process take more time than
+ * expected (e.g. reading a big session state from disk).
+ */
+function whenNewWindowLoaded(aOptions, aCallback) {
+ let features = "";
+ let url = "about:blank";
+
+ if (aOptions && aOptions.private || false) {
+ features = ",private";
+ url = "about:privatebrowsing";
+ }
+
+ let win = openDialog(getBrowserURL(), "", "chrome,all,dialog=no" + features, url);
+ let delayedStartup = promiseDelayedStartupFinished(win);
+
+ let browserLoaded = new Promise(resolve => {
+ if (url == "about:blank") {
+ resolve();
+ return;
+ }
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ let browser = win.gBrowser.selectedBrowser;
+ promiseBrowserLoaded(browser).then(resolve);
+ });
+ });
+
+ Promise.all([delayedStartup, browserLoaded]).then(() => aCallback(win));
+}
+function promiseNewWindowLoaded(aOptions) {
+ return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve));
+}
+
+/**
+ * This waits for the browser-delayed-startup-finished notification of a given
+ * window. It indicates that the windows has loaded completely and is ready to
+ * be used for testing.
+ */
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+function promiseDelayedStartupFinished(aWindow) {
+ return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve));
+}
+
+function promiseEvent(element, eventType, isCapturing = false) {
+ return new Promise(resolve => {
+ element.addEventListener(eventType, function listener(event) {
+ element.removeEventListener(eventType, listener, isCapturing);
+ resolve(event);
+ }, isCapturing);
+ });
+}
+
+function promiseTabRestored(tab) {
+ return promiseEvent(tab, "SSTabRestored");
+}
+
+function promiseTabRestoring(tab) {
+ return promiseEvent(tab, "SSTabRestoring");
+}
+
+function sendMessage(browser, name, data = {}) {
+ browser.messageManager.sendAsyncMessage(name, data);
+ return promiseContentMessage(browser, name);
+}
+
+// This creates list of functions that we will map to their corresponding
+// ss-test:* messages names. Those will be sent to the frame script and
+// be used to read and modify form data.
+const FORM_HELPERS = [
+ "getTextContent",
+ "getInputValue", "setInputValue",
+ "getInputChecked", "setInputChecked",
+ "getSelectedIndex", "setSelectedIndex",
+ "getMultipleSelected", "setMultipleSelected",
+ "getFileNameArray", "setFileNameArray",
+];
+
+for (let name of FORM_HELPERS) {
+ let msg = "ss-test:" + name;
+ this[name] = (browser, data) => sendMessage(browser, msg, data);
+}
+
+// Removes the given tab immediately and returns a promise that resolves when
+// all pending status updates (messages) of the closing tab have been received.
+function promiseRemoveTab(tab) {
+ return BrowserTestUtils.removeTab(tab);
+}
+
+// Write DOMSessionStorage data to the given browser.
+function modifySessionStorage(browser, data, options = {}) {
+ return ContentTask.spawn(browser, [data, options], function* ([data, options]) {
+ let frame = content;
+ if (options && "frameIndex" in options) {
+ frame = content.frames[options.frameIndex];
+ }
+
+ let keys = new Set(Object.keys(data));
+ let storage = frame.sessionStorage;
+
+ return new Promise(resolve => {
+ addEventListener("MozSessionStorageChanged", function onStorageChanged(event) {
+ if (event.storageArea == storage) {
+ keys.delete(event.key);
+ }
+
+ if (keys.size == 0) {
+ removeEventListener("MozSessionStorageChanged", onStorageChanged, true);
+ resolve();
+ }
+ }, true);
+
+ for (let key of keys) {
+ frame.sessionStorage[key] = data[key];
+ }
+ });
+ });
+}
+
+function pushPrefs(...aPrefs) {
+ return new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve);
+ });
+}
+
+function popPrefs() {
+ return new Promise(resolve => {
+ SpecialPowers.popPrefEnv(resolve);
+ });
+}
+
+function* checkScroll(tab, expected, msg) {
+ let browser = tab.linkedBrowser;
+ yield TabStateFlusher.flush(browser);
+
+ let scroll = JSON.parse(ss.getTabState(tab)).scroll || null;
+ is(JSON.stringify(scroll), JSON.stringify(expected), msg);
+}
diff --git a/browser/components/sessionstore/test/restore_redirect_http.html b/browser/components/sessionstore/test/restore_redirect_http.html
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/browser/components/sessionstore/test/restore_redirect_http.html
diff --git a/browser/components/sessionstore/test/restore_redirect_http.html^headers^ b/browser/components/sessionstore/test/restore_redirect_http.html^headers^
new file mode 100644
index 000000000..533bda36f
--- /dev/null
+++ b/browser/components/sessionstore/test/restore_redirect_http.html^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Moved Temporarily
+Location: restore_redirect_target.html
diff --git a/browser/components/sessionstore/test/restore_redirect_js.html b/browser/components/sessionstore/test/restore_redirect_js.html
new file mode 100644
index 000000000..1f5f0e54c
--- /dev/null
+++ b/browser/components/sessionstore/test/restore_redirect_js.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+<script>
+var newLocation = window.location.toString().replace("restore_redirect_js.html", "restore_redirect_target.html");
+window.location.replace(newLocation);
+</script>
+</head>
+</html> \ No newline at end of file
diff --git a/browser/components/sessionstore/test/restore_redirect_target.html b/browser/components/sessionstore/test/restore_redirect_target.html
new file mode 100644
index 000000000..6c8b3aae5
--- /dev/null
+++ b/browser/components/sessionstore/test/restore_redirect_target.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+<title>Test page</title>
+</head>
+<body>Test page</body>
+</html>
diff --git a/browser/components/sessionstore/test/unit/.eslintrc.js b/browser/components/sessionstore/test/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json b/browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json
new file mode 100644
index 000000000..928de6a39
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json
@@ -0,0 +1 @@
+{"profile-after-change":true,"final-ui-startup":true,"sessionstore-windows-restored":true,"quit-application-granted":true,"quit-application":true,"sessionstore-final-state-write-complete":true,"profile-change-net-teardown":true,"profile-change-teardown":true,"profile-before-change":true} \ No newline at end of file
diff --git a/browser/components/sessionstore/test/unit/data/sessionstore_invalid.js b/browser/components/sessionstore/test/unit/data/sessionstore_invalid.js
new file mode 100644
index 000000000..a8c3ff2ff
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/data/sessionstore_invalid.js
@@ -0,0 +1,3 @@
+{
+ "windows": // invalid json
+}
diff --git a/browser/components/sessionstore/test/unit/data/sessionstore_valid.js b/browser/components/sessionstore/test/unit/data/sessionstore_valid.js
new file mode 100644
index 000000000..f9511f29f
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/data/sessionstore_valid.js
@@ -0,0 +1,3 @@
+{
+ "windows": []
+} \ No newline at end of file
diff --git a/browser/components/sessionstore/test/unit/head.js b/browser/components/sessionstore/test/unit/head.js
new file mode 100644
index 000000000..b62856012
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/head.js
@@ -0,0 +1,32 @@
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// Call a function once initialization of SessionStartup is complete
+function afterSessionStartupInitialization(cb) {
+ do_print("Waiting for session startup initialization");
+ let observer = function() {
+ try {
+ do_print("Session startup initialization observed");
+ Services.obs.removeObserver(observer, "sessionstore-state-finalized");
+ cb();
+ } catch (ex) {
+ do_throw(ex);
+ }
+ };
+
+ // We need the Crash Monitor initialized for sessionstartup to run
+ // successfully.
+ Components.utils.import("resource://gre/modules/CrashMonitor.jsm");
+ CrashMonitor.init();
+
+ // Start sessionstartup initialization.
+ let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
+ getService(Ci.nsIObserver);
+ Services.obs.addObserver(startup, "final-ui-startup", false);
+ Services.obs.addObserver(startup, "quit-application", false);
+ Services.obs.notifyObservers(null, "final-ui-startup", "");
+ Services.obs.addObserver(observer, "sessionstore-state-finalized", false);
+};
diff --git a/browser/components/sessionstore/test/unit/test_backup_once.js b/browser/components/sessionstore/test/unit/test_backup_once.js
new file mode 100644
index 000000000..fff34ad58
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_backup_once.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+var {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+var {SessionWorker} = Cu.import("resource:///modules/sessionstore/SessionWorker.jsm", {});
+
+var File = OS.File;
+var Paths;
+var SessionFile;
+
+// We need a XULAppInfo to initialize SessionFile
+Cu.import("resource://testing-common/AppInfo.jsm", this);
+updateAppInfo({
+ name: "SessionRestoreTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* init() {
+ // Make sure that we have a profile before initializing SessionFile
+ let profd = do_get_profile();
+ SessionFile = Cu.import("resource:///modules/sessionstore/SessionFile.jsm", {}).SessionFile;
+ Paths = SessionFile.Paths;
+
+
+ let source = do_get_file("data/sessionstore_valid.js");
+ source.copyTo(profd, "sessionstore.js");
+
+ // Finish initialization of SessionFile
+ yield SessionFile.read();
+});
+
+var pathStore;
+var pathBackup;
+var decoder;
+
+function promise_check_exist(path, shouldExist) {
+ return Task.spawn(function*() {
+ do_print("Ensuring that " + path + (shouldExist?" exists":" does not exist"));
+ if ((yield OS.File.exists(path)) != shouldExist) {
+ throw new Error("File " + path + " should " + (shouldExist?"exist":"not exist"));
+ }
+ });
+}
+
+function promise_check_contents(path, expect) {
+ return Task.spawn(function*() {
+ do_print("Checking whether " + path + " has the right contents");
+ let actual = yield OS.File.read(path, { encoding: "utf-8"});
+ Assert.deepEqual(JSON.parse(actual), expect, `File ${path} contains the expected data.`);
+ });
+}
+
+function generateFileContents(id) {
+ let url = `http://example.com/test_backup_once#${id}_${Math.random()}`;
+ return {windows: [{tabs: [{entries: [{url}], index: 1}]}]}
+}
+
+// Write to the store, and check that it creates:
+// - $Path.recovery with the new data
+// - $Path.nextUpgradeBackup with the old data
+add_task(function* test_first_write_backup() {
+ let initial_content = generateFileContents("initial");
+ let new_content = generateFileContents("test_1");
+
+ do_print("Before the first write, none of the files should exist");
+ yield promise_check_exist(Paths.backups, false);
+
+ yield File.makeDir(Paths.backups);
+ yield File.writeAtomic(Paths.clean, JSON.stringify(initial_content), { encoding: "utf-8" });
+ yield SessionFile.write(new_content);
+
+ do_print("After first write, a few files should have been created");
+ yield promise_check_exist(Paths.backups, true);
+ yield promise_check_exist(Paths.clean, false);
+ yield promise_check_exist(Paths.cleanBackup, true);
+ yield promise_check_exist(Paths.recovery, true);
+ yield promise_check_exist(Paths.recoveryBackup, false);
+ yield promise_check_exist(Paths.nextUpgradeBackup, true);
+
+ yield promise_check_contents(Paths.recovery, new_content);
+ yield promise_check_contents(Paths.nextUpgradeBackup, initial_content);
+});
+
+// Write to the store again, and check that
+// - $Path.clean is not written
+// - $Path.recovery contains the new data
+// - $Path.recoveryBackup contains the previous data
+add_task(function* test_second_write_no_backup() {
+ let new_content = generateFileContents("test_2");
+ let previous_backup_content = yield File.read(Paths.recovery, { encoding: "utf-8" });
+ previous_backup_content = JSON.parse(previous_backup_content);
+
+ yield OS.File.remove(Paths.cleanBackup);
+
+ yield SessionFile.write(new_content);
+
+ yield promise_check_exist(Paths.backups, true);
+ yield promise_check_exist(Paths.clean, false);
+ yield promise_check_exist(Paths.cleanBackup, false);
+ yield promise_check_exist(Paths.recovery, true);
+ yield promise_check_exist(Paths.nextUpgradeBackup, true);
+
+ yield promise_check_contents(Paths.recovery, new_content);
+ yield promise_check_contents(Paths.recoveryBackup, previous_backup_content);
+});
+
+// Make sure that we create $Paths.clean and remove $Paths.recovery*
+// upon shutdown
+add_task(function* test_shutdown() {
+ let output = generateFileContents("test_3");
+
+ yield File.writeAtomic(Paths.recovery, "I should disappear");
+ yield File.writeAtomic(Paths.recoveryBackup, "I should also disappear");
+
+ yield SessionWorker.post("write", [output, { isFinalWrite: true, performShutdownCleanup: true}]);
+
+ do_check_false((yield File.exists(Paths.recovery)));
+ do_check_false((yield File.exists(Paths.recoveryBackup)));
+ yield promise_check_contents(Paths.clean, output);
+});
diff --git a/browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js b/browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js
new file mode 100644
index 000000000..c7d8b03ed
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The primary purpose of this test is to ensure that
+ * the sessionstore component records information about
+ * corrupted backup files into a histogram.
+ */
+
+"use strict";
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+const Telemetry = Services.telemetry;
+const Path = OS.Path;
+const HistogramId = "FX_SESSION_RESTORE_ALL_FILES_CORRUPT";
+
+// Prepare the session file.
+var profd = do_get_profile();
+Cu.import("resource:///modules/sessionstore/SessionFile.jsm", this);
+
+/**
+ * A utility function for resetting the histogram and the contents
+ * of the backup directory.
+ */
+function reset_session(backups = {}) {
+
+ // Reset the histogram.
+ Telemetry.getHistogramById(HistogramId).clear();
+
+ // Reset the contents of the backups directory
+ OS.File.makeDir(SessionFile.Paths.backups);
+ for (let key of SessionFile.Paths.loadOrder) {
+ if (backups.hasOwnProperty(key)) {
+ OS.File.copy(backups[key], SessionFile.Paths[key]);
+ } else {
+ OS.File.remove(SessionFile.Paths[key]);
+ }
+ }
+}
+
+/**
+ * In order to use FX_SESSION_RESTORE_ALL_FILES_CORRUPT histogram
+ * it has to be registered in "toolkit/components/telemetry/Histograms.json".
+ * This test ensures that the histogram is registered and empty.
+ */
+add_task(function* test_ensure_histogram_exists_and_empty() {
+ let s = Telemetry.getHistogramById(HistogramId).snapshot();
+ Assert.equal(s.sum, 0, "Initially, the sum of probes is 0");
+});
+
+/**
+ * Makes sure that the histogram is negatively updated when no
+ * backup files are present.
+ */
+add_task(function* test_no_files_exist() {
+ // No session files are available to SessionFile.
+ reset_session();
+
+ yield SessionFile.read();
+ // Checking if the histogram is updated negatively
+ let h = Telemetry.getHistogramById(HistogramId);
+ let s = h.snapshot();
+ Assert.equal(s.counts[0], 1, "One probe for the 'false' bucket.");
+ Assert.equal(s.counts[1], 0, "No probes in the 'true' bucket.");
+});
+
+/**
+ * Makes sure that the histogram is negatively updated when at least one
+ * backup file is not corrupted.
+ */
+add_task(function* test_one_file_valid() {
+ // Corrupting some backup files.
+ let invalidSession = "data/sessionstore_invalid.js";
+ let validSession = "data/sessionstore_valid.js";
+ reset_session({
+ clean : invalidSession,
+ cleanBackup: validSession,
+ recovery: invalidSession,
+ recoveryBackup: invalidSession
+ });
+
+ yield SessionFile.read();
+ // Checking if the histogram is updated negatively.
+ let h = Telemetry.getHistogramById(HistogramId);
+ let s = h.snapshot();
+ Assert.equal(s.counts[0], 1, "One probe for the 'false' bucket.");
+ Assert.equal(s.counts[1], 0, "No probes in the 'true' bucket.");
+});
+
+/**
+ * Makes sure that the histogram is positively updated when all
+ * backup files are corrupted.
+ */
+add_task(function* test_all_files_corrupt() {
+ // Corrupting all backup files.
+ let invalidSession = "data/sessionstore_invalid.js";
+ reset_session({
+ clean : invalidSession,
+ cleanBackup: invalidSession,
+ recovery: invalidSession,
+ recoveryBackup: invalidSession
+ });
+
+ yield SessionFile.read();
+ // Checking if the histogram is positively updated.
+ let h = Telemetry.getHistogramById(HistogramId);
+ let s = h.snapshot();
+ Assert.equal(s.counts[1], 1, "One probe for the 'true' bucket.");
+ Assert.equal(s.counts[0], 0, "No probes in the 'false' bucket.");
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/browser/components/sessionstore/test/unit/test_shutdown_cleanup.js b/browser/components/sessionstore/test/unit/test_shutdown_cleanup.js
new file mode 100644
index 000000000..b99e566e9
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_shutdown_cleanup.js
@@ -0,0 +1,127 @@
+"use strict";
+
+/**
+ * This test ensures that we correctly clean up the session state before
+ * writing to disk a last time on shutdown. For now it only tests that each
+ * tab's shistory is capped to a maximum number of preceding and succeeding
+ * entries.
+ */
+
+const {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+const {SessionWorker} = Cu.import("resource:///modules/sessionstore/SessionWorker.jsm", {});
+
+const profd = do_get_profile();
+const {SessionFile} = Cu.import("resource:///modules/sessionstore/SessionFile.jsm", {});
+const {Paths} = SessionFile;
+
+const {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+const {File} = OS;
+
+const MAX_ENTRIES = 9;
+const URL = "http://example.com/#";
+
+// We need a XULAppInfo to initialize SessionFile
+Cu.import("resource://testing-common/AppInfo.jsm", this);
+updateAppInfo({
+ name: "SessionRestoreTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+add_task(function* setup() {
+ let source = do_get_file("data/sessionstore_valid.js");
+ source.copyTo(profd, "sessionstore.js");
+
+ // Finish SessionFile initialization.
+ yield SessionFile.read();
+
+ // Reset prefs on cleanup.
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.sessionstore.max_serialize_back");
+ Services.prefs.clearUserPref("browser.sessionstore.max_serialize_forward");
+ });
+});
+
+function createSessionState(index) {
+ // Generate the tab state entries and set the one-based
+ // tab-state index to the middle session history entry.
+ let tabState = {entries: [], index};
+ for (let i = 0; i < MAX_ENTRIES; i++) {
+ tabState.entries.push({url: URL + i});
+ }
+
+ return {windows: [{tabs: [tabState]}]};
+}
+
+function* setMaxBackForward(back, fwd) {
+ Services.prefs.setIntPref("browser.sessionstore.max_serialize_back", back);
+ Services.prefs.setIntPref("browser.sessionstore.max_serialize_forward", fwd);
+ yield SessionFile.read();
+}
+
+function* writeAndParse(state, path, options = {}) {
+ yield SessionWorker.post("write", [state, options]);
+ return JSON.parse(yield File.read(path, {encoding: "utf-8"}));
+}
+
+add_task(function* test_shistory_cap_none() {
+ let state = createSessionState(5);
+
+ // Don't limit the number of shistory entries.
+ yield setMaxBackForward(-1, -1);
+
+ // Check that no caps are applied.
+ let diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+ Assert.deepEqual(state, diskState, "no cap applied");
+});
+
+add_task(function* test_shistory_cap_middle() {
+ let state = createSessionState(5);
+ yield setMaxBackForward(2, 3);
+
+ // Cap is only applied on clean shutdown.
+ let diskState = yield writeAndParse(state, Paths.recovery);
+ Assert.deepEqual(state, diskState, "no cap applied");
+
+ // Check that the right number of shistory entries was discarded
+ // and the shistory index updated accordingly.
+ diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+ let tabState = state.windows[0].tabs[0];
+ tabState.entries = tabState.entries.slice(2, 8);
+ tabState.index = 3;
+ Assert.deepEqual(state, diskState, "cap applied");
+});
+
+add_task(function* test_shistory_cap_lower_bound() {
+ let state = createSessionState(1);
+ yield setMaxBackForward(5, 5);
+
+ // Cap is only applied on clean shutdown.
+ let diskState = yield writeAndParse(state, Paths.recovery);
+ Assert.deepEqual(state, diskState, "no cap applied");
+
+ // Check that the right number of shistory entries was discarded.
+ diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+ let tabState = state.windows[0].tabs[0];
+ tabState.entries = tabState.entries.slice(0, 6);
+ Assert.deepEqual(state, diskState, "cap applied");
+});
+
+add_task(function* test_shistory_cap_upper_bound() {
+ let state = createSessionState(MAX_ENTRIES);
+ yield setMaxBackForward(5, 5);
+
+ // Cap is only applied on clean shutdown.
+ let diskState = yield writeAndParse(state, Paths.recovery);
+ Assert.deepEqual(state, diskState, "no cap applied");
+
+ // Check that the right number of shistory entries was discarded
+ // and the shistory index updated accordingly.
+ diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+ let tabState = state.windows[0].tabs[0];
+ tabState.entries = tabState.entries.slice(3);
+ tabState.index = 6;
+ Assert.deepEqual(state, diskState, "cap applied");
+});
diff --git a/browser/components/sessionstore/test/unit/test_startup_invalid_session.js b/browser/components/sessionstore/test/unit/test_startup_invalid_session.js
new file mode 100644
index 000000000..9f6df8585
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_startup_invalid_session.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ let profd = do_get_profile();
+
+ let sourceSession = do_get_file("data/sessionstore_invalid.js");
+ sourceSession.copyTo(profd, "sessionstore.js");
+
+ let sourceCheckpoints = do_get_file("data/sessionCheckpoints_all.json");
+ sourceCheckpoints.copyTo(profd, "sessionCheckpoints.json");
+
+ do_test_pending();
+ let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
+ getService(Ci.nsISessionStartup);
+
+ afterSessionStartupInitialization(function cb() {
+ do_check_eq(startup.sessionType, Ci.nsISessionStartup.NO_SESSION);
+ do_test_finished();
+ });
+}
diff --git a/browser/components/sessionstore/test/unit/test_startup_nosession_async.js b/browser/components/sessionstore/test/unit/test_startup_nosession_async.js
new file mode 100644
index 000000000..5185b02d6
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_startup_nosession_async.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+// Test nsISessionStartup.sessionType in the following scenario:
+// - no sessionstore.js;
+// - the session store has been loaded, so no need to go
+// through the synchronous fallback
+
+function run_test() {
+ do_get_profile();
+ // Initialize the profile (the session startup uses it)
+
+ do_test_pending();
+ let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
+ getService(Ci.nsISessionStartup);
+
+ afterSessionStartupInitialization(function cb() {
+ do_check_eq(startup.sessionType, Ci.nsISessionStartup.NO_SESSION);
+ do_test_finished();
+ });
+} \ No newline at end of file
diff --git a/browser/components/sessionstore/test/unit/test_startup_session_async.js b/browser/components/sessionstore/test/unit/test_startup_session_async.js
new file mode 100644
index 000000000..459acf885
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_startup_session_async.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+// Test nsISessionStartup.sessionType in the following scenario:
+// - valid sessionstore.js;
+// - valid sessionCheckpoints.json with all checkpoints;
+// - the session store has been loaded
+
+function run_test() {
+ let profd = do_get_profile();
+
+ let sourceSession = do_get_file("data/sessionstore_valid.js");
+ sourceSession.copyTo(profd, "sessionstore.js");
+
+ let sourceCheckpoints = do_get_file("data/sessionCheckpoints_all.json");
+ sourceCheckpoints.copyTo(profd, "sessionCheckpoints.json");
+
+ do_test_pending();
+ let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
+ getService(Ci.nsISessionStartup);
+
+ afterSessionStartupInitialization(function cb() {
+ do_check_eq(startup.sessionType, Ci.nsISessionStartup.DEFER_SESSION);
+ do_test_finished();
+ });
+}
diff --git a/browser/components/sessionstore/test/unit/xpcshell.ini b/browser/components/sessionstore/test/unit/xpcshell.ini
new file mode 100644
index 000000000..09980f877
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/xpcshell.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ data/sessionCheckpoints_all.json
+ data/sessionstore_invalid.js
+ data/sessionstore_valid.js
+
+[test_backup_once.js]
+[test_histogram_corrupt_files.js]
+[test_shutdown_cleanup.js]
+[test_startup_nosession_async.js]
+[test_startup_session_async.js]
+[test_startup_invalid_session.js]