diff options
author | Matt A. Tobin <email@mattatobin.com> | 2019-12-16 13:57:01 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2019-12-16 13:57:01 -0500 |
commit | 06494f307850c576868831bd28a61464eab1f359 (patch) | |
tree | f281f5c46c3e0b73c7eabe22f02622dc013b0c35 /application/basilisk/components/sessionstore/content | |
parent | e7d4713e0765c79feddf2384d343d10595fa5cb3 (diff) | |
download | uxp-06494f307850c576868831bd28a61464eab1f359.tar.gz |
Remove Basilisk from the Unified XUL Platform repository
Development will proceed at https://github.com/MoonchildProductions/Basilisk
Diffstat (limited to 'application/basilisk/components/sessionstore/content')
3 files changed, 0 insertions, 1356 deletions
diff --git a/application/basilisk/components/sessionstore/content/aboutSessionRestore.js b/application/basilisk/components/sessionstore/content/aboutSessionRestore.js deleted file mode 100644 index 8f265235d7..0000000000 --- a/application/basilisk/components/sessionstore/content/aboutSessionRestore.js +++ /dev/null @@ -1,373 +0,0 @@ -/* 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; - } - - try { - gStateObject = JSON.parse(sessionData.value); - } catch (e) { - Cu.reportError(e); - } - - // 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 = []; - if (gStateObject) { - 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; - - if (!treeView.treeBox) { - 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 gTreeData[idx] ? "open" in gTreeData[idx] : false; - }, - 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/application/basilisk/components/sessionstore/content/aboutSessionRestore.xhtml b/application/basilisk/components/sessionstore/content/aboutSessionRestore.xhtml deleted file mode 100644 index bcd9084e77..0000000000 --- a/application/basilisk/components/sessionstore/content/aboutSessionRestore.xhtml +++ /dev/null @@ -1,86 +0,0 @@ -<?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/application/basilisk/components/sessionstore/content/content-sessionStore.js b/application/basilisk/components/sessionstore/content/content-sessionStore.js deleted file mode 100644 index 858e35750e..0000000000 --- a/application/basilisk/components/sessionstore/content/content-sessionStore.js +++ /dev/null @@ -1,897 +0,0 @@ -/* 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. -}); |