gecko-dev/browser/components/sessionstore/content/content-sessionStore.js
2013-12-18 12:39:53 -05:00

494 lines
16 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
function debug(msg) {
Services.console.logStringMessage("SessionStoreContent: " + msg);
}
let Cu = Components.utils;
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource:///modules/sessionstore/Utils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
"resource:///modules/sessionstore/PageStyle.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
"resource:///modules/sessionstore/SessionHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
"resource:///modules/sessionstore/SessionStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
"resource:///modules/sessionstore/TextAndScrollData.jsm");
/**
* 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;
};
}
/**
* Determines whether the given storage event was triggered by changes
* to the sessionStorage object and not the local or globalStorage.
*/
function isSessionStorageEvent(event) {
try {
return event.storageArea == content.sessionStorage;
} catch (ex if ex instanceof Ci.nsIException && ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
// This page does not have a DOMSessionStorage
// (this is typically the case for about: pages)
return false;
}
}
/**
* Listens for and handles content events that we need for the
* session store service to be notified of state changes in content.
*/
let EventListener = {
DOM_EVENTS: [
"pageshow", "change", "input"
],
init: function () {
this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
},
handleEvent: function (event) {
switch (event.type) {
case "pageshow":
if (event.persisted)
sendAsyncMessage("SessionStore:pageshow");
break;
case "input":
case "change":
sendAsyncMessage("SessionStore:input");
break;
default:
debug("received unknown event '" + event.type + "'");
break;
}
}
};
/**
* Listens for and handles messages sent by the session store service.
*/
let MessageListener = {
MESSAGES: [
"SessionStore:collectSessionHistory"
],
init: function () {
this.MESSAGES.forEach(m => addMessageListener(m, this));
},
receiveMessage: function ({name, data: {id}}) {
switch (name) {
case "SessionStore:collectSessionHistory":
let history = SessionHistory.collect(docShell);
if ("index" in history) {
let tabIndex = history.index - 1;
// Don't include private data. It's only needed when duplicating
// tabs, which collects data synchronously.
TextAndScrollData.updateFrame(history.entries[tabIndex],
content,
docShell.isAppTab);
}
sendAsyncMessage(name, {id: id, data: history});
break;
default:
debug("received unknown message '" + name + "'");
break;
}
}
};
/**
* If session data must be collected synchronously, we do it via
* method calls to this object (rather than via messages to
* MessageListener). When using multiple processes, these methods run
* in the content process, but the parent synchronously waits on them
* using cross-process object wrappers. Without multiple processes, we
* still use this code for synchronous collection.
*/
let SyncHandler = {
init: function () {
// Send this object as a CPOW to chrome. In single-process mode,
// the synchronous send ensures that the handler object is
// available in SessionStore.jsm immediately upon loading
// content-sessionStore.js.
sendSyncMessage("SessionStore:setupSyncHandler", {}, {handler: this});
},
collectSessionHistory: function (includePrivateData) {
let history = SessionHistory.collect(docShell);
if ("index" in history) {
let tabIndex = history.index - 1;
TextAndScrollData.updateFrame(history.entries[tabIndex],
content,
docShell.isAppTab,
{includePrivateData: includePrivateData});
}
return history;
},
/**
* This function is used to make the tab process flush all data that
* hasn't been sent to the parent process, yet.
*
* @param id (int)
* A unique id that represents the last message received by the chrome
* process before flushing. We will use this to determine data that
* would be lost when data has been sent asynchronously shortly
* before flushing synchronously.
*/
flush: function (id) {
MessageQueue.flush(id);
},
/**
* DO NOT USE - DEBUGGING / TESTING ONLY
*
* This function is used to simulate certain situations where race conditions
* can occur by sending data shortly before flushing synchronously.
*/
flushAsync: function () {
MessageQueue.flushAsync();
}
};
let ProgressListener = {
init: function() {
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
},
onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
// We are changing page, so time to invalidate the state of the tab
sendAsyncMessage("SessionStore:loadStart");
},
onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {},
onProgressChange: function() {},
onStatusChange: function() {},
onSecurityChange: function() {},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};
/**
* 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, if any. The pageStyle is represented by a string.
*/
let PageStyleListener = {
init: function () {
Services.obs.addObserver(this, "author-style-disabled-changed", true);
Services.obs.addObserver(this, "style-sheet-applicable-state-changed", true);
},
observe: function (subject, topic) {
if (subject.defaultView && subject.defaultView.top == content) {
MessageQueue.push("pageStyle", () => PageStyle.collect(docShell) || null);
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference])
};
/**
* 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.
*/
let 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 () {
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
},
/**
* onLocationChange() is called as soon as we start loading a page after
* we are certain that there's nothing blocking the load (e.g. a content
* policy added by AdBlock or the like).
*/
onLocationChange: 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);
}
},
onStateChange: function () {},
onProgressChange: function () {},
onStatusChange: function () {},
onSecurityChange: function () {},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};
/**
* 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.
*/
let SessionStorageListener = {
init: function () {
addEventListener("MozStorageChanged", this);
Services.obs.addObserver(this, "browser:purge-domain-data", true);
Services.obs.addObserver(this, "browser:purge-session-history", true);
},
handleEvent: function (event) {
// Ignore events triggered by localStorage or globalStorage changes.
if (isSessionStorageEvent(event)) {
this.collect();
}
},
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);
},
collect: function () {
MessageQueue.push("storage", () => SessionStorage.collect(docShell));
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference])
};
/**
* 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.
*/
let PrivacyListener = {
init: function() {
docShell.addWeakPrivacyTransitionObserver(this);
},
// 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.
*/
let MessageQueue = {
/**
* A unique, monotonically increasing ID used for outgoing messages. This is
* important to make it possible to reuse tabs and allow sync flushes before
* data could be destroyed.
*/
_id: 1,
/**
* 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(),
/**
* A map holding the |this._id| value for every type of data back when it
* was pushed onto the queue. We will use those IDs to find the data to send
* and flush.
*/
_lastUpdated: 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,
/**
* 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));
this._lastUpdated.set(key, this._id);
if (!this._timeout) {
// 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)
* {id: 123} to override the update ID used to accumulate data to send.
* {sync: true} to send data to the parent process synchronously.
*/
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 sync = options && options.sync;
let startID = (options && options.id) || this._id;
// We use sendRpcMessage in the sync case because we may have been called
// through a CPOW. RPC messages are the only synchronous messages that the
// child is allowed to send to the parent while it is handling a CPOW
// request.
let sendMessage = sync ? sendRpcMessage : sendAsyncMessage;
let data = {};
for (let [key, id] of this._lastUpdated) {
// There is no data for the given key anymore because
// the parent process already marked it as received.
if (!this._data.has(key)) {
continue;
}
if (startID > id) {
// If the |id| passed by the parent process is higher than the one
// stored in |_lastUpdated| for the given key we know that the parent
// received all necessary data and we can remove it from the map.
this._data.delete(key);
continue;
}
data[key] = this._data.get(key)();
}
// Send all data to the parent process.
sendMessage("SessionStore:update", {id: this._id, data: data});
// Increase our unique message ID.
this._id++;
},
/**
* This function is used to make the message queue flush all queue data that
* hasn't been sent to the parent process, yet.
*
* @param id (int)
* A unique id that represents the latest message received by the
* chrome process. We can use this to determine which messages have not
* yet been received because they are still stuck in the event queue.
*/
flush: function (id) {
// It's important to always send data, even if there is nothing to flush.
// The update message will be received by the parent process that can then
// update its last received update ID to ignore stale messages.
this.send({id: id + 1, sync: true});
this._data.clear();
this._lastUpdated.clear();
},
/**
* DO NOT USE - DEBUGGING / TESTING ONLY
*
* This function is used to simulate certain situations where race conditions
* can occur by sending data shortly before flushing synchronously.
*/
flushAsync: function () {
if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) {
throw new Error("flushAsync() must be used for testing, only.");
}
this.send();
}
};
EventListener.init();
MessageListener.init();
SyncHandler.init();
ProgressListener.init();
PageStyleListener.init();
SessionStorageListener.init();
DocShellCapabilitiesListener.init();
PrivacyListener.init();