зеркало из https://github.com/mozilla/gecko-dev.git
Merge m-c to b2g-inbound.
This commit is contained in:
Коммит
b75c40bb3e
|
@ -8,6 +8,6 @@ module.metadata = {
|
|||
};
|
||||
|
||||
exports.Loader = require('./loader').Loader;
|
||||
exports.Symbiont = require('./symbiont').Symbiont;
|
||||
exports.Symbiont = require('../deprecated/symbiont').Symbiont;
|
||||
exports.Worker = require('./worker').Worker;
|
||||
|
||||
|
|
|
@ -0,0 +1,404 @@
|
|||
/* 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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
const { Class } = require('../core/heritage');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { on, off, emit } = require('../event/core');
|
||||
const {
|
||||
requiresAddonGlobal,
|
||||
attach, detach, destroy
|
||||
} = require('./utils');
|
||||
const { delay: async } = require('../lang/functional');
|
||||
const { Ci, Cu, Cc } = require('chrome');
|
||||
const timer = require('../timers');
|
||||
const { URL } = require('../url');
|
||||
const { sandbox, evaluate, load } = require('../loader/sandbox');
|
||||
const { merge } = require('../util/object');
|
||||
const xulApp = require('../system/xul-app');
|
||||
const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
|
||||
'17.0a2', '*');
|
||||
const { getTabForContentWindow } = require('../tabs/utils');
|
||||
|
||||
// WeakMap of sandboxes so we can access private values
|
||||
const sandboxes = new WeakMap();
|
||||
|
||||
/* Trick the linker in order to ensure shipping these files in the XPI.
|
||||
require('./content-worker.js');
|
||||
Then, retrieve URL of these files in the XPI:
|
||||
*/
|
||||
let prefix = module.uri.split('sandbox.js')[0];
|
||||
const CONTENT_WORKER_URL = prefix + 'content-worker.js';
|
||||
|
||||
// Fetch additional list of domains to authorize access to for each content
|
||||
// script. It is stored in manifest `metadata` field which contains
|
||||
// package.json data. This list is originaly defined by authors in
|
||||
// `permissions` attribute of their package.json addon file.
|
||||
const permissions = require('@loader/options').metadata['permissions'] || {};
|
||||
const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
|
||||
|
||||
const JS_VERSION = '1.8';
|
||||
|
||||
const WorkerSandbox = Class({
|
||||
|
||||
implements: [
|
||||
EventTarget
|
||||
],
|
||||
|
||||
/**
|
||||
* Emit a message to the worker content sandbox
|
||||
*/
|
||||
emit: function emit(...args) {
|
||||
// Ensure having an asynchronous behavior
|
||||
let self = this;
|
||||
async(function () {
|
||||
emitToContent(self, JSON.stringify(args, replacer));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronous version of `emit`.
|
||||
* /!\ Should only be used when it is strictly mandatory /!\
|
||||
* Doesn't ensure passing only JSON values.
|
||||
* Mainly used by context-menu in order to avoid breaking it.
|
||||
*/
|
||||
emitSync: function emitSync(...args) {
|
||||
return emitToContent(this, args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells if content script has at least one listener registered for one event,
|
||||
* through `self.on('xxx', ...)`.
|
||||
* /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
|
||||
*/
|
||||
hasListenerFor: function hasListenerFor(name) {
|
||||
return modelFor(this).hasListenerFor(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures sandbox and loads content scripts into it.
|
||||
* @param {Worker} worker
|
||||
* content worker
|
||||
*/
|
||||
initialize: function WorkerSandbox(worker, window) {
|
||||
let model = {};
|
||||
sandboxes.set(this, model);
|
||||
model.worker = worker;
|
||||
// We receive a wrapped window, that may be an xraywrapper if it's content
|
||||
let proto = window;
|
||||
|
||||
// TODO necessary?
|
||||
// Ensure that `emit` has always the right `this`
|
||||
this.emit = this.emit.bind(this);
|
||||
this.emitSync = this.emitSync.bind(this);
|
||||
|
||||
// Eventually use expanded principal sandbox feature, if some are given.
|
||||
//
|
||||
// But prevent it when the Worker isn't used for a content script but for
|
||||
// injecting `addon` object into a Panel, Widget, ... scope.
|
||||
// That's because:
|
||||
// 1/ It is useless to use multiple domains as the worker is only used
|
||||
// to communicate with the addon,
|
||||
// 2/ By using it it would prevent the document to have access to any JS
|
||||
// value of the worker. As JS values coming from multiple domain principals
|
||||
// can't be accessed by 'mono-principals' (principal with only one domain).
|
||||
// Even if this principal is for a domain that is specified in the multiple
|
||||
// domain principal.
|
||||
let principals = window;
|
||||
let wantGlobalProperties = [];
|
||||
if (EXPANDED_PRINCIPALS.length > 0 && !requiresAddonGlobal(worker)) {
|
||||
principals = EXPANDED_PRINCIPALS.concat(window);
|
||||
// We have to replace XHR constructor of the content document
|
||||
// with a custom cross origin one, automagically added by platform code:
|
||||
delete proto.XMLHttpRequest;
|
||||
wantGlobalProperties.push('XMLHttpRequest');
|
||||
}
|
||||
|
||||
// Instantiate trusted code in another Sandbox in order to prevent content
|
||||
// script from messing with standard classes used by proxy and API code.
|
||||
let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
|
||||
apiSandbox.console = console;
|
||||
|
||||
// Create the sandbox and bind it to window in order for content scripts to
|
||||
// have access to all standard globals (window, document, ...)
|
||||
let content = sandbox(principals, {
|
||||
sandboxPrototype: proto,
|
||||
wantXrays: true,
|
||||
wantGlobalProperties: wantGlobalProperties,
|
||||
sameZoneAs: window,
|
||||
metadata: { SDKContentScript: true }
|
||||
});
|
||||
model.sandbox = content;
|
||||
|
||||
// We have to ensure that window.top and window.parent are the exact same
|
||||
// object than window object, i.e. the sandbox global object. But not
|
||||
// always, in case of iframes, top and parent are another window object.
|
||||
let top = window.top === window ? content : content.top;
|
||||
let parent = window.parent === window ? content : content.parent;
|
||||
merge(content, {
|
||||
// We need 'this === window === top' to be true in toplevel scope:
|
||||
get window() content,
|
||||
get top() top,
|
||||
get parent() parent,
|
||||
// Use the Greasemonkey naming convention to provide access to the
|
||||
// unwrapped window object so the content script can access document
|
||||
// JavaScript values.
|
||||
// NOTE: this functionality is experimental and may change or go away
|
||||
// at any time!
|
||||
get unsafeWindow() window.wrappedJSObject
|
||||
});
|
||||
|
||||
// Load trusted code that will inject content script API.
|
||||
// We need to expose JS objects defined in same principal in order to
|
||||
// avoid having any kind of wrapper.
|
||||
load(apiSandbox, CONTENT_WORKER_URL);
|
||||
|
||||
// prepare a clean `self.options`
|
||||
let options = 'contentScriptOptions' in worker ?
|
||||
JSON.stringify(worker.contentScriptOptions) :
|
||||
undefined;
|
||||
|
||||
// Then call `inject` method and communicate with this script
|
||||
// by trading two methods that allow to send events to the other side:
|
||||
// - `onEvent` called by content script
|
||||
// - `result.emitToContent` called by addon script
|
||||
// Bug 758203: We have to explicitely define `__exposedProps__` in order
|
||||
// to allow access to these chrome object attributes from this sandbox with
|
||||
// content priviledges
|
||||
// https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
|
||||
let onEvent = onContentEvent.bind(null, this);
|
||||
// `ContentWorker` is defined in CONTENT_WORKER_URL file
|
||||
let chromeAPI = createChromeAPI();
|
||||
let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
|
||||
|
||||
// Merge `emitToContent` and `hasListenerFor` into our private
|
||||
// model of the WorkerSandbox so we can communicate with content
|
||||
// script
|
||||
merge(model, result);
|
||||
|
||||
// Handle messages send by this script:
|
||||
setListeners(this);
|
||||
|
||||
// Inject `addon` global into target document if document is trusted,
|
||||
// `addon` in document is equivalent to `self` in content script.
|
||||
if (requiresAddonGlobal(worker)) {
|
||||
Object.defineProperty(getUnsafeWindow(window), 'addon', {
|
||||
value: content.self
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Inject our `console` into target document if worker doesn't have a tab
|
||||
// (e.g Panel, PageWorker, Widget).
|
||||
// `worker.tab` can't be used because bug 804935.
|
||||
if (!getTabForContentWindow(window)) {
|
||||
let win = getUnsafeWindow(window);
|
||||
|
||||
// export our chrome console to content window, using the same approach
|
||||
// of `ConsoleAPI`:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
|
||||
//
|
||||
// and described here:
|
||||
// https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
|
||||
let con = Cu.createObjectIn(win);
|
||||
|
||||
let genPropDesc = function genPropDesc(fun) {
|
||||
return { enumerable: true, configurable: true, writable: true,
|
||||
value: console[fun] };
|
||||
}
|
||||
|
||||
const properties = {
|
||||
log: genPropDesc('log'),
|
||||
info: genPropDesc('info'),
|
||||
warn: genPropDesc('warn'),
|
||||
error: genPropDesc('error'),
|
||||
debug: genPropDesc('debug'),
|
||||
trace: genPropDesc('trace'),
|
||||
dir: genPropDesc('dir'),
|
||||
group: genPropDesc('group'),
|
||||
groupCollapsed: genPropDesc('groupCollapsed'),
|
||||
groupEnd: genPropDesc('groupEnd'),
|
||||
time: genPropDesc('time'),
|
||||
timeEnd: genPropDesc('timeEnd'),
|
||||
profile: genPropDesc('profile'),
|
||||
profileEnd: genPropDesc('profileEnd'),
|
||||
__noSuchMethod__: { enumerable: true, configurable: true, writable: true,
|
||||
value: function() {} }
|
||||
};
|
||||
|
||||
Object.defineProperties(con, properties);
|
||||
Cu.makeObjectPropsNormal(con);
|
||||
|
||||
win.console = con;
|
||||
};
|
||||
|
||||
// The order of `contentScriptFile` and `contentScript` evaluation is
|
||||
// intentional, so programs can load libraries like jQuery from script URLs
|
||||
// and use them in scripts.
|
||||
let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
|
||||
: null,
|
||||
contentScript = ('contentScript' in worker) ? worker.contentScript : null;
|
||||
|
||||
if (contentScriptFile)
|
||||
importScripts.apply(null, [this].concat(contentScriptFile));
|
||||
if (contentScript) {
|
||||
evaluateIn(
|
||||
this,
|
||||
Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
|
||||
);
|
||||
}
|
||||
},
|
||||
destroy: function destroy() {
|
||||
this.emitSync('detach');
|
||||
let model = modelFor(this);
|
||||
model.sandbox = null
|
||||
model.worker = null;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
exports.WorkerSandbox = WorkerSandbox;
|
||||
|
||||
/**
|
||||
* Imports scripts to the sandbox by reading files under urls and
|
||||
* evaluating its source. If exception occurs during evaluation
|
||||
* `'error'` event is emitted on the worker.
|
||||
* This is actually an analog to the `importScript` method in web
|
||||
* workers but in our case it's not exposed even though content
|
||||
* scripts may be able to do it synchronously since IO operation
|
||||
* takes place in the UI process.
|
||||
*/
|
||||
function importScripts (workerSandbox, ...urls) {
|
||||
let { worker, sandbox } = modelFor(workerSandbox);
|
||||
for (let i in urls) {
|
||||
let contentScriptFile = urls[i];
|
||||
try {
|
||||
let uri = URL(contentScriptFile);
|
||||
if (uri.scheme === 'resource')
|
||||
load(sandbox, String(uri));
|
||||
else
|
||||
throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
|
||||
}
|
||||
catch(e) {
|
||||
emit(worker, 'error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setListeners (workerSandbox) {
|
||||
let { worker } = modelFor(workerSandbox);
|
||||
// console.xxx calls
|
||||
workerSandbox.on('console', function consoleListener (kind, ...args) {
|
||||
console[kind].apply(console, args);
|
||||
});
|
||||
|
||||
// self.postMessage calls
|
||||
workerSandbox.on('message', function postMessage(data) {
|
||||
// destroyed?
|
||||
if (worker)
|
||||
emit(worker, 'message', data);
|
||||
});
|
||||
|
||||
// self.port.emit calls
|
||||
workerSandbox.on('event', function portEmit (...eventArgs) {
|
||||
// If not destroyed, emit event information to worker
|
||||
// `eventArgs` has the event name as first element,
|
||||
// and remaining elements are additional arguments to pass
|
||||
if (worker)
|
||||
emit.apply(null, [worker.port].concat(eventArgs));
|
||||
});
|
||||
|
||||
// unwrap, recreate and propagate async Errors thrown from content-script
|
||||
workerSandbox.on('error', function onError({instanceOfError, value}) {
|
||||
if (worker) {
|
||||
let error = value;
|
||||
if (instanceOfError) {
|
||||
error = new Error(value.message, value.fileName, value.lineNumber);
|
||||
error.stack = value.stack;
|
||||
error.name = value.name;
|
||||
}
|
||||
emit(worker, 'error', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates code in the sandbox.
|
||||
* @param {String} code
|
||||
* JavaScript source to evaluate.
|
||||
* @param {String} [filename='javascript:' + code]
|
||||
* Name of the file
|
||||
*/
|
||||
function evaluateIn (workerSandbox, code, filename) {
|
||||
let { worker, sandbox } = modelFor(workerSandbox);
|
||||
try {
|
||||
evaluate(sandbox, code, filename || 'javascript:' + code);
|
||||
}
|
||||
catch(e) {
|
||||
emit(worker, 'error', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called by the worker sandbox when it needs to send a message
|
||||
*/
|
||||
function onContentEvent (workerSandbox, args) {
|
||||
// As `emit`, we ensure having an asynchronous behavior
|
||||
async(function () {
|
||||
// We emit event to chrome/addon listeners
|
||||
emit.apply(null, [workerSandbox].concat(JSON.parse(args)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function modelFor (workerSandbox) {
|
||||
return sandboxes.get(workerSandbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON.stringify is buggy with cross-sandbox values,
|
||||
* it may return '{}' on functions. Use a replacer to match them correctly.
|
||||
*/
|
||||
function replacer (k, v) {
|
||||
return typeof v === 'function' ? undefined : v;
|
||||
}
|
||||
|
||||
function getUnsafeWindow (win) {
|
||||
return win.wrappedJSObject || win;
|
||||
}
|
||||
|
||||
function emitToContent (workerSandbox, args) {
|
||||
return modelFor(workerSandbox).emitToContent(args);
|
||||
}
|
||||
|
||||
function createChromeAPI () {
|
||||
return {
|
||||
timers: {
|
||||
setTimeout: timer.setTimeout,
|
||||
setInterval: timer.setInterval,
|
||||
clearTimeout: timer.clearTimeout,
|
||||
clearInterval: timer.clearInterval,
|
||||
__exposedProps__: {
|
||||
setTimeout: 'r',
|
||||
setInterval: 'r',
|
||||
clearTimeout: 'r',
|
||||
clearInterval: 'r'
|
||||
},
|
||||
},
|
||||
sandbox: {
|
||||
evaluate: evaluate,
|
||||
__exposedProps__: {
|
||||
evaluate: 'r'
|
||||
}
|
||||
},
|
||||
__exposedProps__: {
|
||||
timers: 'r',
|
||||
sandbox: 'r'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,17 +1,19 @@
|
|||
/* 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";
|
||||
'use strict';
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
let assetsURI = require("../self").data.url();
|
||||
let { merge } = require('../util/object');
|
||||
let assetsURI = require('../self').data.url();
|
||||
let isArray = Array.isArray;
|
||||
let method = require('method/core');
|
||||
|
||||
function isAddonContent({ contentURL }) {
|
||||
return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
|
||||
return typeof(contentURL) === 'string' && contentURL.indexOf(assetsURI) === 0;
|
||||
}
|
||||
exports.isAddonContent = isAddonContent;
|
||||
|
||||
|
@ -24,18 +26,57 @@ function hasContentScript({ contentScript, contentScriptFile }) {
|
|||
exports.hasContentScript = hasContentScript;
|
||||
|
||||
function requiresAddonGlobal(model) {
|
||||
return isAddonContent(model) && !hasContentScript(model);
|
||||
return model.injectInDocument || (isAddonContent(model) && !hasContentScript(model));
|
||||
}
|
||||
exports.requiresAddonGlobal = requiresAddonGlobal;
|
||||
|
||||
function getAttachEventType(model) {
|
||||
if (!model) return null;
|
||||
let when = model.contentScriptWhen;
|
||||
return requiresAddonGlobal(model) ? "document-element-inserted" :
|
||||
when === "start" ? "document-element-inserted" :
|
||||
when === "end" ? "load" :
|
||||
when === "ready" ? "DOMContentLoaded" :
|
||||
return requiresAddonGlobal(model) ? 'document-element-inserted' :
|
||||
when === 'start' ? 'document-element-inserted' :
|
||||
when === 'end' ? 'load' :
|
||||
when === 'ready' ? 'DOMContentLoaded' :
|
||||
null;
|
||||
}
|
||||
exports.getAttachEventType = getAttachEventType;
|
||||
|
||||
let attach = method('worker-attach');
|
||||
exports.attach = attach;
|
||||
|
||||
let detach = method('worker-detach');
|
||||
exports.detach = detach;
|
||||
|
||||
let destroy = method('worker-destroy');
|
||||
exports.destroy = destroy;
|
||||
|
||||
function WorkerHost (workerFor) {
|
||||
// Define worker properties that just proxy to underlying worker
|
||||
return ['postMessage', 'port', 'url', 'tab'].reduce(function(proto, name) {
|
||||
// Use descriptor properties instead so we can call
|
||||
// the worker function in the context of the worker so we
|
||||
// don't have to create new functions with `fn.bind(worker)`
|
||||
let descriptorProp = {
|
||||
value: function (...args) {
|
||||
let worker = workerFor(this);
|
||||
return worker[name].apply(worker, args);
|
||||
}
|
||||
};
|
||||
|
||||
let accessorProp = {
|
||||
get: function () { return workerFor(this)[name]; },
|
||||
set: function (value) { workerFor(this)[name] = value; }
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, name, merge({
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
}, isDescriptor(name) ? descriptorProp : accessorProp));
|
||||
return proto;
|
||||
}, {});
|
||||
|
||||
function isDescriptor (prop) {
|
||||
return ~['postMessage'].indexOf(prop);
|
||||
}
|
||||
}
|
||||
exports.WorkerHost = WorkerHost;
|
||||
|
|
|
@ -7,38 +7,25 @@ module.metadata = {
|
|||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Trait } = require('../deprecated/traits');
|
||||
const { EventEmitter, EventEmitterTrait } = require('../deprecated/events');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { on, off, emit, setListeners } = require('../event/core');
|
||||
const {
|
||||
attach, detach, destroy
|
||||
} = require('./utils');
|
||||
const { method } = require('../lang/functional');
|
||||
const { Ci, Cu, Cc } = require('chrome');
|
||||
const timer = require('../timers');
|
||||
const { URL } = require('../url');
|
||||
const unload = require('../system/unload');
|
||||
const observers = require('../deprecated/observer-service');
|
||||
const { Cortex } = require('../deprecated/cortex');
|
||||
const { sandbox, evaluate, load } = require("../loader/sandbox");
|
||||
const { merge } = require('../util/object');
|
||||
const xulApp = require("../system/xul-app");
|
||||
const { getInnerId } = require("../window/utils")
|
||||
const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
|
||||
"17.0a2", "*");
|
||||
const events = require('../system/events');
|
||||
const { getInnerId } = require("../window/utils");
|
||||
const { WorkerSandbox } = require('./sandbox');
|
||||
const { getTabForWindow } = require('../tabs/helpers');
|
||||
const { getTabForContentWindow } = require('../tabs/utils');
|
||||
|
||||
/* Trick the linker in order to ensure shipping these files in the XPI.
|
||||
require('./content-worker.js');
|
||||
Then, retrieve URL of these files in the XPI:
|
||||
*/
|
||||
let prefix = module.uri.split('worker.js')[0];
|
||||
const CONTENT_WORKER_URL = prefix + 'content-worker.js';
|
||||
// A weak map of workers to hold private attributes that
|
||||
// should not be exposed
|
||||
const workers = new WeakMap();
|
||||
|
||||
// Fetch additional list of domains to authorize access to for each content
|
||||
// script. It is stored in manifest `metadata` field which contains
|
||||
// package.json data. This list is originaly defined by authors in
|
||||
// `permissions` attribute of their package.json addon file.
|
||||
const permissions = require('@loader/options').metadata['permissions'] || {};
|
||||
const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
|
||||
|
||||
const JS_VERSION = '1.8';
|
||||
let modelFor = (worker) => workers.get(worker);
|
||||
|
||||
const ERR_DESTROYED =
|
||||
"Couldn't find the worker to receive this message. " +
|
||||
|
@ -48,357 +35,43 @@ const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
|
|||
"until it is visible again.";
|
||||
|
||||
|
||||
const WorkerSandbox = EventEmitter.compose({
|
||||
|
||||
/**
|
||||
* Emit a message to the worker content sandbox
|
||||
*/
|
||||
emit: function emit() {
|
||||
// First ensure having a regular array
|
||||
// (otherwise, `arguments` would be mapped to an object by `stringify`)
|
||||
let array = Array.slice(arguments);
|
||||
// JSON.stringify is buggy with cross-sandbox values,
|
||||
// it may return "{}" on functions. Use a replacer to match them correctly.
|
||||
function replacer(k, v) {
|
||||
return typeof v === "function" ? undefined : v;
|
||||
}
|
||||
// Ensure having an asynchronous behavior
|
||||
let self = this;
|
||||
timer.setTimeout(function () {
|
||||
self._emitToContent(JSON.stringify(array, replacer));
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronous version of `emit`.
|
||||
* /!\ Should only be used when it is strictly mandatory /!\
|
||||
* Doesn't ensure passing only JSON values.
|
||||
* Mainly used by context-menu in order to avoid breaking it.
|
||||
*/
|
||||
emitSync: function emitSync() {
|
||||
let args = Array.slice(arguments);
|
||||
return this._emitToContent(args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells if content script has at least one listener registered for one event,
|
||||
* through `self.on('xxx', ...)`.
|
||||
* /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
|
||||
*/
|
||||
hasListenerFor: function hasListenerFor(name) {
|
||||
return this._hasListenerFor(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method called by the worker sandbox when it needs to send a message
|
||||
*/
|
||||
_onContentEvent: function onContentEvent(args) {
|
||||
// As `emit`, we ensure having an asynchronous behavior
|
||||
let self = this;
|
||||
timer.setTimeout(function () {
|
||||
// We emit event to chrome/addon listeners
|
||||
self._emit.apply(self, JSON.parse(args));
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures sandbox and loads content scripts into it.
|
||||
* @param {Worker} worker
|
||||
* content worker
|
||||
*/
|
||||
constructor: function WorkerSandbox(worker) {
|
||||
this._addonWorker = worker;
|
||||
|
||||
// Ensure that `emit` has always the right `this`
|
||||
this.emit = this.emit.bind(this);
|
||||
this.emitSync = this.emitSync.bind(this);
|
||||
|
||||
// We receive a wrapped window, that may be an xraywrapper if it's content
|
||||
let window = worker._window;
|
||||
let proto = window;
|
||||
|
||||
// Eventually use expanded principal sandbox feature, if some are given.
|
||||
//
|
||||
// But prevent it when the Worker isn't used for a content script but for
|
||||
// injecting `addon` object into a Panel, Widget, ... scope.
|
||||
// That's because:
|
||||
// 1/ It is useless to use multiple domains as the worker is only used
|
||||
// to communicate with the addon,
|
||||
// 2/ By using it it would prevent the document to have access to any JS
|
||||
// value of the worker. As JS values coming from multiple domain principals
|
||||
// can't be accessed by "mono-principals" (principal with only one domain).
|
||||
// Even if this principal is for a domain that is specified in the multiple
|
||||
// domain principal.
|
||||
let principals = window;
|
||||
let wantGlobalProperties = []
|
||||
if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
|
||||
principals = EXPANDED_PRINCIPALS.concat(window);
|
||||
// We have to replace XHR constructor of the content document
|
||||
// with a custom cross origin one, automagically added by platform code:
|
||||
delete proto.XMLHttpRequest;
|
||||
wantGlobalProperties.push("XMLHttpRequest");
|
||||
}
|
||||
|
||||
// Instantiate trusted code in another Sandbox in order to prevent content
|
||||
// script from messing with standard classes used by proxy and API code.
|
||||
let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
|
||||
apiSandbox.console = console;
|
||||
|
||||
// Create the sandbox and bind it to window in order for content scripts to
|
||||
// have access to all standard globals (window, document, ...)
|
||||
let content = this._sandbox = sandbox(principals, {
|
||||
sandboxPrototype: proto,
|
||||
wantXrays: true,
|
||||
wantGlobalProperties: wantGlobalProperties,
|
||||
sameZoneAs: window,
|
||||
metadata: { SDKContentScript: true }
|
||||
});
|
||||
// We have to ensure that window.top and window.parent are the exact same
|
||||
// object than window object, i.e. the sandbox global object. But not
|
||||
// always, in case of iframes, top and parent are another window object.
|
||||
let top = window.top === window ? content : content.top;
|
||||
let parent = window.parent === window ? content : content.parent;
|
||||
merge(content, {
|
||||
// We need "this === window === top" to be true in toplevel scope:
|
||||
get window() content,
|
||||
get top() top,
|
||||
get parent() parent,
|
||||
// Use the Greasemonkey naming convention to provide access to the
|
||||
// unwrapped window object so the content script can access document
|
||||
// JavaScript values.
|
||||
// NOTE: this functionality is experimental and may change or go away
|
||||
// at any time!
|
||||
get unsafeWindow() window.wrappedJSObject
|
||||
});
|
||||
|
||||
// Load trusted code that will inject content script API.
|
||||
// We need to expose JS objects defined in same principal in order to
|
||||
// avoid having any kind of wrapper.
|
||||
load(apiSandbox, CONTENT_WORKER_URL);
|
||||
|
||||
// prepare a clean `self.options`
|
||||
let options = 'contentScriptOptions' in worker ?
|
||||
JSON.stringify( worker.contentScriptOptions ) :
|
||||
undefined;
|
||||
|
||||
// Then call `inject` method and communicate with this script
|
||||
// by trading two methods that allow to send events to the other side:
|
||||
// - `onEvent` called by content script
|
||||
// - `result.emitToContent` called by addon script
|
||||
// Bug 758203: We have to explicitely define `__exposedProps__` in order
|
||||
// to allow access to these chrome object attributes from this sandbox with
|
||||
// content priviledges
|
||||
// https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
|
||||
let chromeAPI = {
|
||||
timers: {
|
||||
setTimeout: timer.setTimeout,
|
||||
setInterval: timer.setInterval,
|
||||
clearTimeout: timer.clearTimeout,
|
||||
clearInterval: timer.clearInterval,
|
||||
__exposedProps__: {
|
||||
setTimeout: 'r',
|
||||
setInterval: 'r',
|
||||
clearTimeout: 'r',
|
||||
clearInterval: 'r'
|
||||
}
|
||||
},
|
||||
sandbox: {
|
||||
evaluate: evaluate,
|
||||
__exposedProps__: {
|
||||
evaluate: 'r',
|
||||
}
|
||||
},
|
||||
__exposedProps__: {
|
||||
timers: 'r',
|
||||
sandbox: 'r',
|
||||
}
|
||||
};
|
||||
let onEvent = this._onContentEvent.bind(this);
|
||||
// `ContentWorker` is defined in CONTENT_WORKER_URL file
|
||||
let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
|
||||
this._emitToContent = result.emitToContent;
|
||||
this._hasListenerFor = result.hasListenerFor;
|
||||
|
||||
// Handle messages send by this script:
|
||||
let self = this;
|
||||
// console.xxx calls
|
||||
this.on("console", function consoleListener(kind) {
|
||||
console[kind].apply(console, Array.slice(arguments, 1));
|
||||
});
|
||||
|
||||
// self.postMessage calls
|
||||
this.on("message", function postMessage(data) {
|
||||
// destroyed?
|
||||
if (self._addonWorker)
|
||||
self._addonWorker._emit('message', data);
|
||||
});
|
||||
|
||||
// self.port.emit calls
|
||||
this.on("event", function portEmit(name, args) {
|
||||
// destroyed?
|
||||
if (self._addonWorker)
|
||||
self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
|
||||
});
|
||||
|
||||
// unwrap, recreate and propagate async Errors thrown from content-script
|
||||
this.on("error", function onError({instanceOfError, value}) {
|
||||
if (self._addonWorker) {
|
||||
let error = value;
|
||||
if (instanceOfError) {
|
||||
error = new Error(value.message, value.fileName, value.lineNumber);
|
||||
error.stack = value.stack;
|
||||
error.name = value.name;
|
||||
}
|
||||
self._addonWorker._emit('error', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Inject `addon` global into target document if document is trusted,
|
||||
// `addon` in document is equivalent to `self` in content script.
|
||||
if (worker._injectInDocument) {
|
||||
let win = window.wrappedJSObject ? window.wrappedJSObject : window;
|
||||
Object.defineProperty(win, "addon", {
|
||||
value: content.self
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Inject our `console` into target document if worker doesn't have a tab
|
||||
// (e.g Panel, PageWorker, Widget).
|
||||
// `worker.tab` can't be used because bug 804935.
|
||||
if (!getTabForContentWindow(window)) {
|
||||
let win = window.wrappedJSObject ? window.wrappedJSObject : window;
|
||||
|
||||
// export our chrome console to content window, using the same approach
|
||||
// of `ConsoleAPI`:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
|
||||
//
|
||||
// and described here:
|
||||
// https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
|
||||
let con = Cu.createObjectIn(win);
|
||||
|
||||
let genPropDesc = function genPropDesc(fun) {
|
||||
return { enumerable: true, configurable: true, writable: true,
|
||||
value: console[fun] };
|
||||
}
|
||||
|
||||
const properties = {
|
||||
log: genPropDesc('log'),
|
||||
info: genPropDesc('info'),
|
||||
warn: genPropDesc('warn'),
|
||||
error: genPropDesc('error'),
|
||||
debug: genPropDesc('debug'),
|
||||
trace: genPropDesc('trace'),
|
||||
dir: genPropDesc('dir'),
|
||||
group: genPropDesc('group'),
|
||||
groupCollapsed: genPropDesc('groupCollapsed'),
|
||||
groupEnd: genPropDesc('groupEnd'),
|
||||
time: genPropDesc('time'),
|
||||
timeEnd: genPropDesc('timeEnd'),
|
||||
profile: genPropDesc('profile'),
|
||||
profileEnd: genPropDesc('profileEnd'),
|
||||
__noSuchMethod__: { enumerable: true, configurable: true, writable: true,
|
||||
value: function() {} }
|
||||
};
|
||||
|
||||
Object.defineProperties(con, properties);
|
||||
Cu.makeObjectPropsNormal(con);
|
||||
|
||||
win.console = con;
|
||||
};
|
||||
|
||||
// The order of `contentScriptFile` and `contentScript` evaluation is
|
||||
// intentional, so programs can load libraries like jQuery from script URLs
|
||||
// and use them in scripts.
|
||||
let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
|
||||
: null,
|
||||
contentScript = ('contentScript' in worker) ? worker.contentScript : null;
|
||||
|
||||
if (contentScriptFile) {
|
||||
if (Array.isArray(contentScriptFile))
|
||||
this._importScripts.apply(this, contentScriptFile);
|
||||
else
|
||||
this._importScripts(contentScriptFile);
|
||||
}
|
||||
if (contentScript) {
|
||||
this._evaluate(
|
||||
Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
|
||||
);
|
||||
}
|
||||
},
|
||||
destroy: function destroy() {
|
||||
this.emitSync("detach");
|
||||
this._sandbox = null;
|
||||
this._addonWorker = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* JavaScript sandbox where all the content scripts are evaluated.
|
||||
* {Sandbox}
|
||||
*/
|
||||
_sandbox: null,
|
||||
|
||||
/**
|
||||
* Reference to the addon side of the worker.
|
||||
* @type {Worker}
|
||||
*/
|
||||
_addonWorker: null,
|
||||
|
||||
/**
|
||||
* Evaluates code in the sandbox.
|
||||
* @param {String} code
|
||||
* JavaScript source to evaluate.
|
||||
* @param {String} [filename='javascript:' + code]
|
||||
* Name of the file
|
||||
*/
|
||||
_evaluate: function(code, filename) {
|
||||
try {
|
||||
evaluate(this._sandbox, code, filename || 'javascript:' + code);
|
||||
}
|
||||
catch(e) {
|
||||
this._addonWorker._emit('error', e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Imports scripts to the sandbox by reading files under urls and
|
||||
* evaluating its source. If exception occurs during evaluation
|
||||
* `"error"` event is emitted on the worker.
|
||||
* This is actually an analog to the `importScript` method in web
|
||||
* workers but in our case it's not exposed even though content
|
||||
* scripts may be able to do it synchronously since IO operation
|
||||
* takes place in the UI process.
|
||||
*/
|
||||
_importScripts: function _importScripts(url) {
|
||||
let urls = Array.slice(arguments, 0);
|
||||
for each (let contentScriptFile in urls) {
|
||||
try {
|
||||
let uri = URL(contentScriptFile);
|
||||
if (uri.scheme === 'resource')
|
||||
load(this._sandbox, String(uri));
|
||||
else
|
||||
throw Error("Unsupported `contentScriptFile` url: " + String(uri));
|
||||
}
|
||||
catch(e) {
|
||||
this._addonWorker._emit('error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Message-passing facility for communication between code running
|
||||
* in the content and add-on process.
|
||||
* @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
|
||||
*/
|
||||
const Worker = EventEmitter.compose({
|
||||
on: Trait.required,
|
||||
_removeAllListeners: Trait.required,
|
||||
const Worker = Class({
|
||||
implements: [EventTarget],
|
||||
initialize: function WorkerConstructor (options) {
|
||||
// Save model in weak map to not expose properties
|
||||
let model = createModel();
|
||||
workers.set(this, model);
|
||||
|
||||
// List of messages fired before worker is initialized
|
||||
get _earlyEvents() {
|
||||
delete this._earlyEvents;
|
||||
this._earlyEvents = [];
|
||||
return this._earlyEvents;
|
||||
options = options || {};
|
||||
|
||||
if ('contentScriptFile' in options)
|
||||
this.contentScriptFile = options.contentScriptFile;
|
||||
if ('contentScriptOptions' in options)
|
||||
this.contentScriptOptions = options.contentScriptOptions;
|
||||
if ('contentScript' in options)
|
||||
this.contentScript = options.contentScript;
|
||||
if ('injectInDocument' in options)
|
||||
this.injectInDocument = !!options.injectInDocument;
|
||||
|
||||
setListeners(this, options);
|
||||
|
||||
unload.ensure(this, "destroy");
|
||||
|
||||
// Ensure that worker.port is initialized for contentWorker to be able
|
||||
// to send events during worker initialization.
|
||||
this.port = createPort(this);
|
||||
|
||||
model.documentUnload = documentUnload.bind(this);
|
||||
model.pageShow = pageShow.bind(this);
|
||||
model.pageHide = pageHide.bind(this);
|
||||
|
||||
if ('window' in options)
|
||||
attach(this, options.window);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -413,239 +86,197 @@ const Worker = EventEmitter.compose({
|
|||
* implementing `onMessage` function in the global scope of this worker.
|
||||
* @param {Number|String|JSON} data
|
||||
*/
|
||||
postMessage: function (data) {
|
||||
let args = ['message'].concat(Array.slice(arguments));
|
||||
if (!this._inited) {
|
||||
this._earlyEvents.push(args);
|
||||
postMessage: function (...data) {
|
||||
let model = modelFor(this);
|
||||
let args = ['message'].concat(data);
|
||||
if (!model.inited) {
|
||||
model.earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
processMessage.apply(this, args);
|
||||
processMessage.apply(null, [this].concat(args));
|
||||
},
|
||||
|
||||
/**
|
||||
* EventEmitter, that behaves (calls listeners) asynchronously.
|
||||
* A way to send customized messages to / from the worker.
|
||||
* Events from in the worker can be observed / emitted via
|
||||
* worker.on / worker.emit.
|
||||
*/
|
||||
get port() {
|
||||
// We generate dynamically this attribute as it needs to be accessible
|
||||
// before Worker.constructor gets called. (For ex: Panel)
|
||||
|
||||
// create an event emitter that receive and send events from/to the worker
|
||||
this._port = EventEmitterTrait.create({
|
||||
emit: this._emitEventToContent.bind(this)
|
||||
});
|
||||
|
||||
// expose wrapped port, that exposes only public properties:
|
||||
// We need to destroy this getter in order to be able to set the
|
||||
// final value. We need to update only public port attribute as we never
|
||||
// try to access port attribute from private API.
|
||||
delete this._public.port;
|
||||
this._public.port = Cortex(this._port);
|
||||
// Replicate public port to the private object
|
||||
delete this.port;
|
||||
this.port = this._public.port;
|
||||
|
||||
return this._port;
|
||||
get url () {
|
||||
let model = modelFor(this);
|
||||
// model.window will be null after detach
|
||||
return model.window ? model.window.document.location.href : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Same object than this.port but private API.
|
||||
* Allow access to _emit, in order to send event to port.
|
||||
*/
|
||||
_port: null,
|
||||
|
||||
/**
|
||||
* Emit a custom event to the content script,
|
||||
* i.e. emit this event on `self.port`
|
||||
*/
|
||||
_emitEventToContent: function () {
|
||||
let args = ['event'].concat(Array.slice(arguments));
|
||||
if (!this._inited) {
|
||||
this._earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
processMessage.apply(this, args);
|
||||
get contentURL () {
|
||||
let model = modelFor(this);
|
||||
return model.window ? model.window.document.URL : null;
|
||||
},
|
||||
|
||||
// Is worker connected to the content worker sandbox ?
|
||||
_inited: false,
|
||||
|
||||
// Is worker being frozen? i.e related document is frozen in bfcache.
|
||||
// Content script should not be reachable if frozen.
|
||||
_frozen: true,
|
||||
|
||||
constructor: function Worker(options) {
|
||||
options = options || {};
|
||||
|
||||
if ('contentScriptFile' in options)
|
||||
this.contentScriptFile = options.contentScriptFile;
|
||||
if ('contentScriptOptions' in options)
|
||||
this.contentScriptOptions = options.contentScriptOptions;
|
||||
if ('contentScript' in options)
|
||||
this.contentScript = options.contentScript;
|
||||
|
||||
this._setListeners(options);
|
||||
|
||||
unload.ensure(this._public, "destroy");
|
||||
|
||||
// Ensure that worker._port is initialized for contentWorker to be able
|
||||
// to send events during worker initialization.
|
||||
this.port;
|
||||
|
||||
this._documentUnload = this._documentUnload.bind(this);
|
||||
this._pageShow = this._pageShow.bind(this);
|
||||
this._pageHide = this._pageHide.bind(this);
|
||||
|
||||
if ("window" in options) this._attach(options.window);
|
||||
},
|
||||
|
||||
_setListeners: function(options) {
|
||||
if ('onError' in options)
|
||||
this.on('error', options.onError);
|
||||
if ('onMessage' in options)
|
||||
this.on('message', options.onMessage);
|
||||
if ('onDetach' in options)
|
||||
this.on('detach', options.onDetach);
|
||||
},
|
||||
|
||||
_attach: function(window) {
|
||||
this._window = window;
|
||||
// Track document unload to destroy this worker.
|
||||
// We can't watch for unload event on page's window object as it
|
||||
// prevents bfcache from working:
|
||||
// https://developer.mozilla.org/En/Working_with_BFCache
|
||||
this._windowID = getInnerId(this._window);
|
||||
observers.add("inner-window-destroyed", this._documentUnload);
|
||||
|
||||
// Listen to pagehide event in order to freeze the content script
|
||||
// while the document is frozen in bfcache:
|
||||
this._window.addEventListener("pageshow", this._pageShow, true);
|
||||
this._window.addEventListener("pagehide", this._pageHide, true);
|
||||
|
||||
// will set this._contentWorker pointing to the private API:
|
||||
this._contentWorker = WorkerSandbox(this);
|
||||
|
||||
// Mainly enable worker.port.emit to send event to the content worker
|
||||
this._inited = true;
|
||||
this._frozen = false;
|
||||
|
||||
// Process all events and messages that were fired before the
|
||||
// worker was initialized.
|
||||
this._earlyEvents.forEach((function (args) {
|
||||
processMessage.apply(this, args);
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
_documentUnload: function _documentUnload(subject, topic, data) {
|
||||
let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
if (innerWinID != this._windowID) return false;
|
||||
this._workerCleanup();
|
||||
return true;
|
||||
},
|
||||
|
||||
_pageShow: function _pageShow() {
|
||||
this._contentWorker.emitSync("pageshow");
|
||||
this._emit("pageshow");
|
||||
this._frozen = false;
|
||||
},
|
||||
|
||||
_pageHide: function _pageHide() {
|
||||
this._contentWorker.emitSync("pagehide");
|
||||
this._emit("pagehide");
|
||||
this._frozen = true;
|
||||
},
|
||||
|
||||
get url() {
|
||||
// this._window will be null after detach
|
||||
return this._window ? this._window.document.location.href : null;
|
||||
},
|
||||
|
||||
get tab() {
|
||||
// this._window will be null after detach
|
||||
if (this._window)
|
||||
return getTabForWindow(this._window);
|
||||
get tab () {
|
||||
let model = modelFor(this);
|
||||
// model.window will be null after detach
|
||||
if (model.window)
|
||||
return getTabForWindow(model.window);
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells content worker to unload itself and
|
||||
* removes all the references from itself.
|
||||
*/
|
||||
destroy: function destroy() {
|
||||
this._workerCleanup();
|
||||
this._inited = true;
|
||||
this._removeAllListeners();
|
||||
// Implemented to provide some of the previous features of exposing sandbox
|
||||
// so that Worker can be extended
|
||||
getSandbox: function () {
|
||||
return modelFor(this).contentWorker;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all internal references to the attached document
|
||||
* Tells _port to unload itself and removes all the references from itself.
|
||||
*/
|
||||
_workerCleanup: function _workerCleanup() {
|
||||
// maybe unloaded before content side is created
|
||||
// As Symbiont call worker.constructor on document load
|
||||
if (this._contentWorker)
|
||||
this._contentWorker.destroy();
|
||||
this._contentWorker = null;
|
||||
if (this._window) {
|
||||
this._window.removeEventListener("pageshow", this._pageShow, true);
|
||||
this._window.removeEventListener("pagehide", this._pageHide, true);
|
||||
}
|
||||
this._window = null;
|
||||
// This method may be called multiple times,
|
||||
// avoid dispatching `detach` event more than once
|
||||
if (this._windowID) {
|
||||
this._windowID = null;
|
||||
observers.remove("inner-window-destroyed", this._documentUnload);
|
||||
this._earlyEvents.length = 0;
|
||||
this._emit("detach");
|
||||
}
|
||||
this._inited = false;
|
||||
},
|
||||
toString: function () { return '[object Worker]'; },
|
||||
attach: method(attach),
|
||||
detach: method(detach),
|
||||
destroy: method(destroy)
|
||||
});
|
||||
exports.Worker = Worker;
|
||||
|
||||
/**
|
||||
* Receive an event from the content script that need to be sent to
|
||||
* worker.port. Provide a way for composed object to catch all events.
|
||||
*/
|
||||
_onContentScriptEvent: function _onContentScriptEvent() {
|
||||
this._port._emit.apply(this._port, arguments);
|
||||
},
|
||||
attach.define(Worker, function (worker, window) {
|
||||
let model = modelFor(worker);
|
||||
model.window = window;
|
||||
// Track document unload to destroy this worker.
|
||||
// We can't watch for unload event on page's window object as it
|
||||
// prevents bfcache from working:
|
||||
// https://developer.mozilla.org/En/Working_with_BFCache
|
||||
model.windowID = getInnerId(model.window);
|
||||
events.on("inner-window-destroyed", model.documentUnload);
|
||||
|
||||
/**
|
||||
* Reference to the content side of the worker.
|
||||
* @type {WorkerGlobalScope}
|
||||
*/
|
||||
_contentWorker: null,
|
||||
// Listen to pagehide event in order to freeze the content script
|
||||
// while the document is frozen in bfcache:
|
||||
model.window.addEventListener("pageshow", model.pageShow, true);
|
||||
model.window.addEventListener("pagehide", model.pageHide, true);
|
||||
|
||||
/**
|
||||
* Reference to the window that is accessible from
|
||||
* the content scripts.
|
||||
* @type {Object}
|
||||
*/
|
||||
_window: null,
|
||||
// will set model.contentWorker pointing to the private API:
|
||||
model.contentWorker = WorkerSandbox(worker, model.window);
|
||||
|
||||
/**
|
||||
* Flag to enable `addon` object injection in document. (bug 612726)
|
||||
* @type {Boolean}
|
||||
*/
|
||||
_injectInDocument: false
|
||||
// Mainly enable worker.port.emit to send event to the content worker
|
||||
model.inited = true;
|
||||
model.frozen = false;
|
||||
|
||||
// Fire off `attach` event
|
||||
emit(worker, 'attach', window);
|
||||
|
||||
// Process all events and messages that were fired before the
|
||||
// worker was initialized.
|
||||
model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
|
||||
});
|
||||
|
||||
/**
|
||||
* Fired from postMessage and _emitEventToContent, or from the _earlyMessage
|
||||
* Remove all internal references to the attached document
|
||||
* Tells _port to unload itself and removes all the references from itself.
|
||||
*/
|
||||
detach.define(Worker, function (worker) {
|
||||
let model = modelFor(worker);
|
||||
// maybe unloaded before content side is created
|
||||
if (model.contentWorker)
|
||||
model.contentWorker.destroy();
|
||||
model.contentWorker = null;
|
||||
if (model.window) {
|
||||
model.window.removeEventListener("pageshow", model.pageShow, true);
|
||||
model.window.removeEventListener("pagehide", model.pageHide, true);
|
||||
}
|
||||
model.window = null;
|
||||
// This method may be called multiple times,
|
||||
// avoid dispatching `detach` event more than once
|
||||
if (model.windowID) {
|
||||
model.windowID = null;
|
||||
events.off("inner-window-destroyed", model.documentUnload);
|
||||
model.earlyEvents.length = 0;
|
||||
emit(worker, 'detach');
|
||||
}
|
||||
model.inited = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Tells content worker to unload itself and
|
||||
* removes all the references from itself.
|
||||
*/
|
||||
destroy.define(Worker, function (worker) {
|
||||
detach(worker);
|
||||
modelFor(worker).inited = true;
|
||||
// Specifying no type or listener removes all listeners
|
||||
// from target
|
||||
off(worker);
|
||||
});
|
||||
|
||||
/**
|
||||
* Events fired by workers
|
||||
*/
|
||||
function documentUnload ({ subject, data }) {
|
||||
let model = modelFor(this);
|
||||
let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
if (innerWinID != model.windowID) return false;
|
||||
detach(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
function pageShow () {
|
||||
let model = modelFor(this);
|
||||
model.contentWorker.emitSync('pageshow');
|
||||
emit(this, 'pageshow');
|
||||
model.frozen = false;
|
||||
}
|
||||
|
||||
function pageHide () {
|
||||
let model = modelFor(this);
|
||||
model.contentWorker.emitSync('pagehide');
|
||||
emit(this, 'pagehide');
|
||||
model.frozen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired from postMessage and emitEventToContent, or from the earlyMessage
|
||||
* queue when fired before the content is loaded. Sends arguments to
|
||||
* contentWorker if able
|
||||
*/
|
||||
|
||||
function processMessage () {
|
||||
if (!this._contentWorker)
|
||||
function processMessage (worker, ...args) {
|
||||
let model = modelFor(worker) || {};
|
||||
if (!model.contentWorker)
|
||||
throw new Error(ERR_DESTROYED);
|
||||
if (this._frozen)
|
||||
if (model.frozen)
|
||||
throw new Error(ERR_FROZEN);
|
||||
|
||||
this._contentWorker.emit.apply(null, Array.slice(arguments));
|
||||
model.contentWorker.emit.apply(null, args);
|
||||
}
|
||||
|
||||
function createModel () {
|
||||
return {
|
||||
// List of messages fired before worker is initialized
|
||||
earlyEvents: [],
|
||||
// Is worker connected to the content worker sandbox ?
|
||||
inited: false,
|
||||
// Is worker being frozen? i.e related document is frozen in bfcache.
|
||||
// Content script should not be reachable if frozen.
|
||||
frozen: true,
|
||||
/**
|
||||
* Reference to the content side of the worker.
|
||||
* @type {WorkerGlobalScope}
|
||||
*/
|
||||
contentWorker: null,
|
||||
/**
|
||||
* Reference to the window that is accessible from
|
||||
* the content scripts.
|
||||
* @type {Object}
|
||||
*/
|
||||
window: null
|
||||
};
|
||||
}
|
||||
|
||||
function createPort (worker) {
|
||||
let port = EventTarget();
|
||||
port.emit = emitEventToContent.bind(null, worker);
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a custom event to the content script,
|
||||
* i.e. emit this event on `self.port`
|
||||
*/
|
||||
function emitEventToContent (worker, ...eventArgs) {
|
||||
let model = modelFor(worker);
|
||||
let args = ['event'].concat(eventArgs);
|
||||
if (!model.inited) {
|
||||
model.earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
processMessage.apply(null, [worker].concat(args));
|
||||
}
|
||||
|
||||
exports.Worker = Worker;
|
||||
|
|
|
@ -363,10 +363,12 @@ let menuRules = mix(labelledItemRules, {
|
|||
}
|
||||
});
|
||||
|
||||
let ContextWorker = Worker.compose({
|
||||
let ContextWorker = Class({
|
||||
implements: [ Worker ],
|
||||
|
||||
//Returns true if any context listeners are defined in the worker's port.
|
||||
anyContextListeners: function anyContextListeners() {
|
||||
return this._contentWorker.hasListenerFor("context");
|
||||
return this.getSandbox().hasListenerFor("context");
|
||||
},
|
||||
|
||||
// Calls the context workers context listeners and returns the first result
|
||||
|
@ -374,7 +376,7 @@ let ContextWorker = Worker.compose({
|
|||
// listeners returned false then returns false. If there are no listeners
|
||||
// then returns null.
|
||||
getMatchedContext: function getCurrentContexts(popupNode) {
|
||||
let results = this._contentWorker.emitSync("context", popupNode);
|
||||
let results = this.getSandbox().emitSync("context", popupNode);
|
||||
return results.reduce(function(val, result) val || result, null);
|
||||
},
|
||||
|
||||
|
@ -382,7 +384,7 @@ let ContextWorker = Worker.compose({
|
|||
// context-clicked, and clickedItemData is the data of the item that was
|
||||
// clicked.
|
||||
fireClick: function fireClick(popupNode, clickedItemData) {
|
||||
this._contentWorker.emitSync("click", popupNode, clickedItemData);
|
||||
this.getSandbox().emitSync("click", popupNode, clickedItemData);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -27,29 +27,6 @@ const VALID_TYPES = [
|
|||
|
||||
const { isArray } = Array;
|
||||
|
||||
/**
|
||||
* Returns a function C that creates instances of privateCtor. C may be called
|
||||
* with or without the new keyword. The prototype of each instance returned
|
||||
* from C is C.prototype, and C.prototype is an object whose prototype is
|
||||
* privateCtor.prototype. Instances returned from C will therefore be instances
|
||||
* of both C and privateCtor. Additionally, the constructor of each instance
|
||||
* returned from C is C.
|
||||
*
|
||||
* @param privateCtor
|
||||
* A constructor.
|
||||
* @return A function that makes new instances of privateCtor.
|
||||
*/
|
||||
exports.publicConstructor = function publicConstructor(privateCtor) {
|
||||
function PublicCtor() {
|
||||
let obj = { constructor: PublicCtor, __proto__: PublicCtor.prototype };
|
||||
memory.track(obj, privateCtor.name);
|
||||
privateCtor.apply(obj, arguments);
|
||||
return obj;
|
||||
}
|
||||
PublicCtor.prototype = { __proto__: privateCtor.prototype };
|
||||
return PublicCtor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a validated options dictionary given some requirements. If any of
|
||||
* the requirements are not met, an exception is thrown.
|
||||
|
|
|
@ -1,67 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const {Cc,Ci} = require("chrome");
|
||||
const apiUtils = require("./api-utils");
|
||||
|
||||
/**
|
||||
* A bundle of strings.
|
||||
*
|
||||
* @param url {String}
|
||||
* the URL of the string bundle
|
||||
*/
|
||||
exports.StringBundle = apiUtils.publicConstructor(function StringBundle(url) {
|
||||
|
||||
let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
|
||||
getService(Ci.nsIStringBundleService).
|
||||
createBundle(url);
|
||||
|
||||
this.__defineGetter__("url", function () url);
|
||||
|
||||
/**
|
||||
* Get a string from the bundle.
|
||||
*
|
||||
* @param name {String}
|
||||
* the name of the string to get
|
||||
* @param args {array} [optional]
|
||||
* an array of arguments that replace occurrences of %S in the string
|
||||
*
|
||||
* @returns {String} the value of the string
|
||||
*/
|
||||
this.get = function strings_get(name, args) {
|
||||
try {
|
||||
if (args)
|
||||
return stringBundle.formatStringFromName(name, args, args.length);
|
||||
else
|
||||
return stringBundle.GetStringFromName(name);
|
||||
}
|
||||
catch(ex) {
|
||||
// f.e. "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE)
|
||||
// [nsIStringBundle.GetStringFromName]"
|
||||
throw new Error("String '" + name + "' could not be retrieved from the " +
|
||||
"bundle due to an unknown error (it doesn't exist?).");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterate the strings in the bundle.
|
||||
*
|
||||
*/
|
||||
apiUtils.addIterator(
|
||||
this,
|
||||
function keysValsGen() {
|
||||
let enumerator = stringBundle.getSimpleEnumeration();
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
|
||||
yield [elem.key, elem.value];
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
|
@ -1,48 +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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const {Cc,Ci,Cu,components} = require("chrome");
|
||||
var trackedObjects = {};
|
||||
const { Cc, Ci, Cu, components } = require("chrome");
|
||||
const { when: unload } = require("../system/unload")
|
||||
|
||||
var Compacter = {
|
||||
INTERVAL: 5000,
|
||||
notify: function(timer) {
|
||||
var trackedObjects = {};
|
||||
const Compacter = {
|
||||
notify: function() {
|
||||
var newTrackedObjects = {};
|
||||
|
||||
for (let name in trackedObjects) {
|
||||
var oldBin = trackedObjects[name];
|
||||
var newBin = [];
|
||||
var strongRefs = [];
|
||||
for (var i = 0; i < oldBin.length; i++) {
|
||||
var strongRef = oldBin[i].weakref.get();
|
||||
let oldBin = trackedObjects[name];
|
||||
let newBin = [];
|
||||
let strongRefs = [];
|
||||
|
||||
for (let i = 0, l = oldBin.length; i < l; i++) {
|
||||
let strongRef = oldBin[i].weakref.get();
|
||||
|
||||
if (strongRef && strongRefs.indexOf(strongRef) == -1) {
|
||||
strongRefs.push(strongRef);
|
||||
newBin.push(oldBin[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (newBin.length)
|
||||
newTrackedObjects[name] = newBin;
|
||||
}
|
||||
|
||||
trackedObjects = newTrackedObjects;
|
||||
}
|
||||
};
|
||||
|
||||
var timer = Cc["@mozilla.org/timer;1"]
|
||||
.createInstance(Ci.nsITimer);
|
||||
|
||||
timer.initWithCallback(Compacter,
|
||||
Compacter.INTERVAL,
|
||||
5000,
|
||||
Ci.nsITimer.TYPE_REPEATING_SLACK);
|
||||
|
||||
var track = exports.track = function track(object, bin, stackFrameNumber) {
|
||||
function track(object, bin, stackFrameNumber) {
|
||||
var frame = components.stack.caller;
|
||||
var weakref = Cu.getWeakReference(object);
|
||||
|
||||
if (!bin && 'constructor' in object)
|
||||
bin = object.constructor.name;
|
||||
if (bin == "Object")
|
||||
|
@ -61,7 +65,8 @@ var track = exports.track = function track(object, bin, stackFrameNumber) {
|
|||
filename: frame.filename,
|
||||
lineNo: frame.lineNumber,
|
||||
bin: bin});
|
||||
};
|
||||
}
|
||||
exports.track = track;
|
||||
|
||||
var getBins = exports.getBins = function getBins() {
|
||||
var names = [];
|
||||
|
@ -70,49 +75,55 @@ var getBins = exports.getBins = function getBins() {
|
|||
return names;
|
||||
};
|
||||
|
||||
var getObjects = exports.getObjects = function getObjects(bin) {
|
||||
function getLiveObjectsInBin(bin, array) {
|
||||
for (var i = 0; i < bin.length; i++) {
|
||||
var object = bin[i].weakref.get();
|
||||
if (object)
|
||||
array.push(bin[i]);
|
||||
function getObjects(bin) {
|
||||
var results = [];
|
||||
|
||||
function getLiveObjectsInBin(bin) {
|
||||
for (let i = 0, l = bin.length; i < l; i++) {
|
||||
let object = bin[i].weakref.get();
|
||||
|
||||
if (object) {
|
||||
results.push(bin[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results = [];
|
||||
if (bin) {
|
||||
if (bin in trackedObjects)
|
||||
getLiveObjectsInBin(trackedObjects[bin], results);
|
||||
} else
|
||||
getLiveObjectsInBin(trackedObjects[bin]);
|
||||
}
|
||||
else {
|
||||
for (let name in trackedObjects)
|
||||
getLiveObjectsInBin(trackedObjects[name], results);
|
||||
return results;
|
||||
};
|
||||
getLiveObjectsInBin(trackedObjects[name]);
|
||||
}
|
||||
|
||||
var gc = exports.gc = function gc() {
|
||||
return results;
|
||||
}
|
||||
exports.getObjects = getObjects;
|
||||
|
||||
function gc() {
|
||||
// Components.utils.forceGC() doesn't currently perform
|
||||
// cycle collection, which means that e.g. DOM elements
|
||||
// won't be collected by it. Fortunately, there are
|
||||
// other ways...
|
||||
|
||||
var window = Cc["@mozilla.org/appshell/appShellService;1"]
|
||||
var test_utils = Cc["@mozilla.org/appshell/appShellService;1"]
|
||||
.getService(Ci.nsIAppShellService)
|
||||
.hiddenDOMWindow;
|
||||
var test_utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
.hiddenDOMWindow
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
test_utils.garbageCollect();
|
||||
// Clean metadata for dead objects
|
||||
Compacter.notify();
|
||||
|
||||
// Not sure why, but sometimes it appears that we don't get
|
||||
// them all with just one CC, so let's do it again.
|
||||
test_utils.garbageCollect();
|
||||
};
|
||||
exports.gc = gc;
|
||||
|
||||
require("../system/unload").when(
|
||||
function() {
|
||||
trackedObjects = {};
|
||||
if (timer) {
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
unload(_ => {
|
||||
trackedObjects = {};
|
||||
if (timer) {
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,134 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { when: unload } = require("../system/unload");
|
||||
const { ns } = require("../core/namespace");
|
||||
const { on, off, emit, once } = require("../system/events");
|
||||
const { id } = require("../self");
|
||||
|
||||
const subscribers = ns();
|
||||
const cache = [];
|
||||
|
||||
/**
|
||||
* Topics specifically available to Jetpack-generated extensions.
|
||||
*
|
||||
* Using these predefined consts instead of the platform strings is good:
|
||||
* - allows us to scope topics specifically for Jetpacks
|
||||
* - addons aren't dependent on strings nor behavior of core platform topics
|
||||
* - the core platform topics are not clearly named
|
||||
*
|
||||
*/
|
||||
exports.topics = {
|
||||
/**
|
||||
* A topic indicating that the application is in a state usable
|
||||
* by add-ons.
|
||||
*/
|
||||
APPLICATION_READY: id + "_APPLICATION_READY"
|
||||
};
|
||||
|
||||
function Listener(callback, target) {
|
||||
return function listener({ subject, data }) {
|
||||
callback.call(target || callback, subject, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the given callback as an observer of the given topic.
|
||||
*
|
||||
* @param topic {String}
|
||||
* the topic to observe
|
||||
*
|
||||
* @param callback {Object}
|
||||
* the callback; an Object that implements nsIObserver or a Function
|
||||
* that gets called when the notification occurs
|
||||
*
|
||||
* @param target {Object} [optional]
|
||||
* the object to use as |this| when calling a Function callback
|
||||
*
|
||||
* @returns the observer
|
||||
*/
|
||||
function add(topic, callback, target) {
|
||||
let listeners = subscribers(callback);
|
||||
if (!(topic in listeners)) {
|
||||
let listener = Listener(callback, target);
|
||||
listeners[topic] = listener;
|
||||
|
||||
// Cache callback unless it's already cached.
|
||||
if (!~cache.indexOf(callback))
|
||||
cache.push(callback);
|
||||
|
||||
on(topic, listener);
|
||||
}
|
||||
};
|
||||
exports.add = add;
|
||||
|
||||
/**
|
||||
* Unregister the given callback as an observer of the given topic.
|
||||
*
|
||||
* @param topic {String}
|
||||
* the topic being observed
|
||||
*
|
||||
* @param callback {Object}
|
||||
* the callback doing the observing
|
||||
*
|
||||
* @param target {Object} [optional]
|
||||
* the object being used as |this| when calling a Function callback
|
||||
*/
|
||||
function remove(topic, callback, target) {
|
||||
let listeners = subscribers(callback);
|
||||
if (topic in listeners) {
|
||||
let listener = listeners[topic];
|
||||
delete listeners[topic];
|
||||
|
||||
// If no more observers are registered and callback is still in cache
|
||||
// then remove it.
|
||||
let index = cache.indexOf(callback);
|
||||
if (~index && !Object.keys(listeners).length)
|
||||
cache.splice(index, 1)
|
||||
|
||||
off(topic, listener);
|
||||
}
|
||||
};
|
||||
exports.remove = remove;
|
||||
|
||||
/**
|
||||
* Notify observers about something.
|
||||
*
|
||||
* @param topic {String}
|
||||
* the topic to notify observers about
|
||||
*
|
||||
* @param subject {Object} [optional]
|
||||
* some information about the topic; can be any JS object or primitive
|
||||
*
|
||||
* @param data {String} [optional] [deprecated]
|
||||
* some more information about the topic; deprecated as the subject
|
||||
* is sufficient to pass all needed information to the JS observers
|
||||
* that this module targets; if you have multiple values to pass to
|
||||
* the observer, wrap them in an object and pass them via the subject
|
||||
* parameter (i.e.: { foo: 1, bar: "some string", baz: myObject })
|
||||
*/
|
||||
function notify(topic, subject, data) {
|
||||
emit(topic, {
|
||||
subject: subject === undefined ? null : subject,
|
||||
data: data === undefined ? null : data
|
||||
});
|
||||
}
|
||||
exports.notify = notify;
|
||||
|
||||
unload(function() {
|
||||
// Make a copy of cache first, since cache will be changing as we
|
||||
// iterate through it.
|
||||
cache.slice().forEach(function(callback) {
|
||||
Object.keys(subscribers(callback)).forEach(function(topic) {
|
||||
remove(topic, callback);
|
||||
});
|
||||
});
|
||||
})
|
|
@ -4,13 +4,13 @@
|
|||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const { Worker } = require('./worker');
|
||||
const { Loader } = require('./loader');
|
||||
const { Worker } = require('./traits-worker');
|
||||
const { Loader } = require('../content/loader');
|
||||
const hiddenFrames = require('../frame/hidden-frame');
|
||||
const observers = require('../deprecated/observer-service');
|
||||
const { on, off } = require('../system/events');
|
||||
const unload = require('../system/unload');
|
||||
const { getDocShell } = require("../frame/utils");
|
||||
const { ignoreWindow } = require('../private-browsing/utils');
|
||||
|
@ -28,7 +28,7 @@ const Symbiont = Worker.resolve({
|
|||
constructor: '_initWorker',
|
||||
destroy: '_workerDestroy'
|
||||
}).compose(Loader, {
|
||||
|
||||
|
||||
/**
|
||||
* The constructor requires all the options that are required by
|
||||
* `require('content').Worker` with the difference that the `frame` option
|
||||
|
@ -80,7 +80,7 @@ const Symbiont = Worker.resolve({
|
|||
|
||||
unload.ensure(this._public, "destroy");
|
||||
},
|
||||
|
||||
|
||||
destroy: function destroy() {
|
||||
this._workerDestroy();
|
||||
this._unregisterListener();
|
||||
|
@ -90,14 +90,14 @@ const Symbiont = Worker.resolve({
|
|||
this._hiddenFrame = null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* XUL iframe or browser elements with attribute `type` being `content`.
|
||||
* Used to create `ContentSymbiont` from.
|
||||
* @type {nsIFrame|nsIBrowser}
|
||||
*/
|
||||
_frame: null,
|
||||
|
||||
|
||||
/**
|
||||
* Listener to the `'frameReady"` event (emitted when `iframe` is ready).
|
||||
* Removes listener, sets right permissions to the frame and loads content.
|
||||
|
@ -105,7 +105,7 @@ const Symbiont = Worker.resolve({
|
|||
_initFrame: function _initFrame(frame) {
|
||||
if (this._loadListener)
|
||||
this._unregisterListener();
|
||||
|
||||
|
||||
this._frame = frame;
|
||||
|
||||
if (getDocShell(frame)) {
|
||||
|
@ -113,16 +113,16 @@ const Symbiont = Worker.resolve({
|
|||
}
|
||||
else {
|
||||
if (this._waitForFrame) {
|
||||
observers.remove('content-document-global-created', this._waitForFrame);
|
||||
off('content-document-global-created', this._waitForFrame);
|
||||
}
|
||||
this._waitForFrame = this.__waitForFrame.bind(this, frame);
|
||||
observers.add('content-document-global-created', this._waitForFrame);
|
||||
on('content-document-global-created', this._waitForFrame);
|
||||
}
|
||||
},
|
||||
|
||||
__waitForFrame: function _waitForFrame(frame, win, topic) {
|
||||
__waitForFrame: function _waitForFrame(frame, { subject: win }) {
|
||||
if (frame.contentWindow == win) {
|
||||
observers.remove('content-document-global-created', this._waitForFrame);
|
||||
off('content-document-global-created', this._waitForFrame);
|
||||
delete this._waitForFrame;
|
||||
this._reallyInitFrame(frame);
|
||||
}
|
||||
|
@ -157,13 +157,13 @@ const Symbiont = Worker.resolve({
|
|||
this._onInit();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let self = this;
|
||||
|
||||
|
||||
if ('start' == this.contentScriptWhen) {
|
||||
this._loadEvent = 'start';
|
||||
observers.add('document-element-inserted',
|
||||
this._loadListener = function onStart(doc) {
|
||||
on('document-element-inserted',
|
||||
this._loadListener = function onStart({ subject: doc }) {
|
||||
let window = doc.defaultView;
|
||||
|
||||
if (ignoreWindow(window)) {
|
||||
|
@ -174,41 +174,41 @@ const Symbiont = Worker.resolve({
|
|||
self._unregisterListener();
|
||||
self._onInit();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
|
||||
let self = this;
|
||||
this._loadEvent = eventName;
|
||||
frame.addEventListener(eventName,
|
||||
frame.addEventListener(eventName,
|
||||
this._loadListener = function _onReady(event) {
|
||||
|
||||
|
||||
if (event.target != frame.contentDocument)
|
||||
return;
|
||||
self._unregisterListener();
|
||||
|
||||
|
||||
self._onInit();
|
||||
|
||||
|
||||
}, true);
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Unregister listener that watchs for document being ready to be injected.
|
||||
* This listener is registered in `Symbiont._initFrame`.
|
||||
*/
|
||||
_unregisterListener: function _unregisterListener() {
|
||||
if (this._waitForFrame) {
|
||||
observers.remove('content-document-global-created', this._waitForFrame);
|
||||
off('content-document-global-created', this._waitForFrame);
|
||||
delete this._waitForFrame;
|
||||
}
|
||||
|
||||
if (!this._loadListener)
|
||||
return;
|
||||
if (this._loadEvent == "start") {
|
||||
observers.remove('document-element-inserted', this._loadListener);
|
||||
off('document-element-inserted', this._loadListener);
|
||||
}
|
||||
else {
|
||||
this._frame.removeEventListener(this._loadEvent, this._loadListener,
|
||||
|
@ -216,14 +216,14 @@ const Symbiont = Worker.resolve({
|
|||
}
|
||||
this._loadListener = null;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Called by Symbiont itself when the frame is ready to load
|
||||
* content scripts according to contentScriptWhen. Overloaded by Panel.
|
||||
* Called by Symbiont itself when the frame is ready to load
|
||||
* content scripts according to contentScriptWhen. Overloaded by Panel.
|
||||
*/
|
||||
_onInit: function () {
|
||||
this._initWorker({ window: this._frame.contentWindow });
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
exports.Symbiont = Symbiont;
|
|
@ -0,0 +1,660 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
*
|
||||
* `deprecated/traits-worker` was previously `content/worker` and kept
|
||||
* only due to `deprecated/symbiont` using it, which is necessary for
|
||||
* `widget`, until that reaches deprecation EOL.
|
||||
*
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "deprecated"
|
||||
};
|
||||
|
||||
const { Trait } = require('./traits');
|
||||
const { EventEmitter, EventEmitterTrait } = require('./events');
|
||||
const { Ci, Cu, Cc } = require('chrome');
|
||||
const timer = require('../timers');
|
||||
const { URL } = require('../url');
|
||||
const unload = require('../system/unload');
|
||||
const observers = require('../system/events');
|
||||
const { Cortex } = require('./cortex');
|
||||
const { sandbox, evaluate, load } = require("../loader/sandbox");
|
||||
const { merge } = require('../util/object');
|
||||
const xulApp = require("../system/xul-app");
|
||||
const { getInnerId } = require("../window/utils")
|
||||
const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
|
||||
"17.0a2", "*");
|
||||
const { getTabForWindow } = require('../tabs/helpers');
|
||||
const { getTabForContentWindow } = require('../tabs/utils');
|
||||
|
||||
/* Trick the linker in order to ensure shipping these files in the XPI.
|
||||
require('../content/content-worker.js');
|
||||
Then, retrieve URL of these files in the XPI:
|
||||
*/
|
||||
let prefix = module.uri.split('deprecated/traits-worker.js')[0];
|
||||
const CONTENT_WORKER_URL = prefix + 'content/content-worker.js';
|
||||
|
||||
// Fetch additional list of domains to authorize access to for each content
|
||||
// script. It is stored in manifest `metadata` field which contains
|
||||
// package.json data. This list is originaly defined by authors in
|
||||
// `permissions` attribute of their package.json addon file.
|
||||
const permissions = require('@loader/options').metadata['permissions'] || {};
|
||||
const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
|
||||
|
||||
const JS_VERSION = '1.8';
|
||||
|
||||
const ERR_DESTROYED =
|
||||
"Couldn't find the worker to receive this message. " +
|
||||
"The script may not be initialized yet, or may already have been unloaded.";
|
||||
|
||||
const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
|
||||
"until it is visible again.";
|
||||
|
||||
|
||||
const WorkerSandbox = EventEmitter.compose({
|
||||
|
||||
/**
|
||||
* Emit a message to the worker content sandbox
|
||||
*/
|
||||
emit: function emit() {
|
||||
// First ensure having a regular array
|
||||
// (otherwise, `arguments` would be mapped to an object by `stringify`)
|
||||
let array = Array.slice(arguments);
|
||||
// JSON.stringify is buggy with cross-sandbox values,
|
||||
// it may return "{}" on functions. Use a replacer to match them correctly.
|
||||
function replacer(k, v) {
|
||||
return typeof v === "function" ? undefined : v;
|
||||
}
|
||||
// Ensure having an asynchronous behavior
|
||||
let self = this;
|
||||
timer.setTimeout(function () {
|
||||
self._emitToContent(JSON.stringify(array, replacer));
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronous version of `emit`.
|
||||
* /!\ Should only be used when it is strictly mandatory /!\
|
||||
* Doesn't ensure passing only JSON values.
|
||||
* Mainly used by context-menu in order to avoid breaking it.
|
||||
*/
|
||||
emitSync: function emitSync() {
|
||||
let args = Array.slice(arguments);
|
||||
return this._emitToContent(args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells if content script has at least one listener registered for one event,
|
||||
* through `self.on('xxx', ...)`.
|
||||
* /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
|
||||
*/
|
||||
hasListenerFor: function hasListenerFor(name) {
|
||||
return this._hasListenerFor(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method called by the worker sandbox when it needs to send a message
|
||||
*/
|
||||
_onContentEvent: function onContentEvent(args) {
|
||||
// As `emit`, we ensure having an asynchronous behavior
|
||||
let self = this;
|
||||
timer.setTimeout(function () {
|
||||
// We emit event to chrome/addon listeners
|
||||
self._emit.apply(self, JSON.parse(args));
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Configures sandbox and loads content scripts into it.
|
||||
* @param {Worker} worker
|
||||
* content worker
|
||||
*/
|
||||
constructor: function WorkerSandbox(worker) {
|
||||
this._addonWorker = worker;
|
||||
|
||||
// Ensure that `emit` has always the right `this`
|
||||
this.emit = this.emit.bind(this);
|
||||
this.emitSync = this.emitSync.bind(this);
|
||||
|
||||
// We receive a wrapped window, that may be an xraywrapper if it's content
|
||||
let window = worker._window;
|
||||
let proto = window;
|
||||
|
||||
// Eventually use expanded principal sandbox feature, if some are given.
|
||||
//
|
||||
// But prevent it when the Worker isn't used for a content script but for
|
||||
// injecting `addon` object into a Panel, Widget, ... scope.
|
||||
// That's because:
|
||||
// 1/ It is useless to use multiple domains as the worker is only used
|
||||
// to communicate with the addon,
|
||||
// 2/ By using it it would prevent the document to have access to any JS
|
||||
// value of the worker. As JS values coming from multiple domain principals
|
||||
// can't be accessed by "mono-principals" (principal with only one domain).
|
||||
// Even if this principal is for a domain that is specified in the multiple
|
||||
// domain principal.
|
||||
let principals = window;
|
||||
let wantGlobalProperties = []
|
||||
if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
|
||||
principals = EXPANDED_PRINCIPALS.concat(window);
|
||||
// We have to replace XHR constructor of the content document
|
||||
// with a custom cross origin one, automagically added by platform code:
|
||||
delete proto.XMLHttpRequest;
|
||||
wantGlobalProperties.push("XMLHttpRequest");
|
||||
}
|
||||
|
||||
// Instantiate trusted code in another Sandbox in order to prevent content
|
||||
// script from messing with standard classes used by proxy and API code.
|
||||
let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
|
||||
apiSandbox.console = console;
|
||||
|
||||
// Create the sandbox and bind it to window in order for content scripts to
|
||||
// have access to all standard globals (window, document, ...)
|
||||
let content = this._sandbox = sandbox(principals, {
|
||||
sandboxPrototype: proto,
|
||||
wantXrays: true,
|
||||
wantGlobalProperties: wantGlobalProperties,
|
||||
sameZoneAs: window,
|
||||
metadata: { SDKContentScript: true }
|
||||
});
|
||||
// We have to ensure that window.top and window.parent are the exact same
|
||||
// object than window object, i.e. the sandbox global object. But not
|
||||
// always, in case of iframes, top and parent are another window object.
|
||||
let top = window.top === window ? content : content.top;
|
||||
let parent = window.parent === window ? content : content.parent;
|
||||
merge(content, {
|
||||
// We need "this === window === top" to be true in toplevel scope:
|
||||
get window() content,
|
||||
get top() top,
|
||||
get parent() parent,
|
||||
// Use the Greasemonkey naming convention to provide access to the
|
||||
// unwrapped window object so the content script can access document
|
||||
// JavaScript values.
|
||||
// NOTE: this functionality is experimental and may change or go away
|
||||
// at any time!
|
||||
get unsafeWindow() window.wrappedJSObject
|
||||
});
|
||||
|
||||
// Load trusted code that will inject content script API.
|
||||
// We need to expose JS objects defined in same principal in order to
|
||||
// avoid having any kind of wrapper.
|
||||
load(apiSandbox, CONTENT_WORKER_URL);
|
||||
|
||||
// prepare a clean `self.options`
|
||||
let options = 'contentScriptOptions' in worker ?
|
||||
JSON.stringify( worker.contentScriptOptions ) :
|
||||
undefined;
|
||||
|
||||
// Then call `inject` method and communicate with this script
|
||||
// by trading two methods that allow to send events to the other side:
|
||||
// - `onEvent` called by content script
|
||||
// - `result.emitToContent` called by addon script
|
||||
// Bug 758203: We have to explicitely define `__exposedProps__` in order
|
||||
// to allow access to these chrome object attributes from this sandbox with
|
||||
// content priviledges
|
||||
// https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
|
||||
let chromeAPI = {
|
||||
timers: {
|
||||
setTimeout: timer.setTimeout,
|
||||
setInterval: timer.setInterval,
|
||||
clearTimeout: timer.clearTimeout,
|
||||
clearInterval: timer.clearInterval,
|
||||
__exposedProps__: {
|
||||
setTimeout: 'r',
|
||||
setInterval: 'r',
|
||||
clearTimeout: 'r',
|
||||
clearInterval: 'r'
|
||||
}
|
||||
},
|
||||
sandbox: {
|
||||
evaluate: evaluate,
|
||||
__exposedProps__: {
|
||||
evaluate: 'r',
|
||||
}
|
||||
},
|
||||
__exposedProps__: {
|
||||
timers: 'r',
|
||||
sandbox: 'r',
|
||||
}
|
||||
};
|
||||
let onEvent = this._onContentEvent.bind(this);
|
||||
// `ContentWorker` is defined in CONTENT_WORKER_URL file
|
||||
let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
|
||||
this._emitToContent = result.emitToContent;
|
||||
this._hasListenerFor = result.hasListenerFor;
|
||||
|
||||
// Handle messages send by this script:
|
||||
let self = this;
|
||||
// console.xxx calls
|
||||
this.on("console", function consoleListener(kind) {
|
||||
console[kind].apply(console, Array.slice(arguments, 1));
|
||||
});
|
||||
|
||||
// self.postMessage calls
|
||||
this.on("message", function postMessage(data) {
|
||||
// destroyed?
|
||||
if (self._addonWorker)
|
||||
self._addonWorker._emit('message', data);
|
||||
});
|
||||
|
||||
// self.port.emit calls
|
||||
this.on("event", function portEmit(name, args) {
|
||||
// destroyed?
|
||||
if (self._addonWorker)
|
||||
self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
|
||||
});
|
||||
|
||||
// unwrap, recreate and propagate async Errors thrown from content-script
|
||||
this.on("error", function onError({instanceOfError, value}) {
|
||||
if (self._addonWorker) {
|
||||
let error = value;
|
||||
if (instanceOfError) {
|
||||
error = new Error(value.message, value.fileName, value.lineNumber);
|
||||
error.stack = value.stack;
|
||||
error.name = value.name;
|
||||
}
|
||||
self._addonWorker._emit('error', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Inject `addon` global into target document if document is trusted,
|
||||
// `addon` in document is equivalent to `self` in content script.
|
||||
if (worker._injectInDocument) {
|
||||
let win = window.wrappedJSObject ? window.wrappedJSObject : window;
|
||||
Object.defineProperty(win, "addon", {
|
||||
value: content.self
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Inject our `console` into target document if worker doesn't have a tab
|
||||
// (e.g Panel, PageWorker, Widget).
|
||||
// `worker.tab` can't be used because bug 804935.
|
||||
if (!getTabForContentWindow(window)) {
|
||||
let win = window.wrappedJSObject ? window.wrappedJSObject : window;
|
||||
|
||||
// export our chrome console to content window, using the same approach
|
||||
// of `ConsoleAPI`:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
|
||||
//
|
||||
// and described here:
|
||||
// https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
|
||||
let con = Cu.createObjectIn(win);
|
||||
|
||||
let genPropDesc = function genPropDesc(fun) {
|
||||
return { enumerable: true, configurable: true, writable: true,
|
||||
value: console[fun] };
|
||||
}
|
||||
|
||||
const properties = {
|
||||
log: genPropDesc('log'),
|
||||
info: genPropDesc('info'),
|
||||
warn: genPropDesc('warn'),
|
||||
error: genPropDesc('error'),
|
||||
debug: genPropDesc('debug'),
|
||||
trace: genPropDesc('trace'),
|
||||
dir: genPropDesc('dir'),
|
||||
group: genPropDesc('group'),
|
||||
groupCollapsed: genPropDesc('groupCollapsed'),
|
||||
groupEnd: genPropDesc('groupEnd'),
|
||||
time: genPropDesc('time'),
|
||||
timeEnd: genPropDesc('timeEnd'),
|
||||
profile: genPropDesc('profile'),
|
||||
profileEnd: genPropDesc('profileEnd'),
|
||||
__noSuchMethod__: { enumerable: true, configurable: true, writable: true,
|
||||
value: function() {} }
|
||||
};
|
||||
|
||||
Object.defineProperties(con, properties);
|
||||
Cu.makeObjectPropsNormal(con);
|
||||
|
||||
win.console = con;
|
||||
};
|
||||
|
||||
// The order of `contentScriptFile` and `contentScript` evaluation is
|
||||
// intentional, so programs can load libraries like jQuery from script URLs
|
||||
// and use them in scripts.
|
||||
let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
|
||||
: null,
|
||||
contentScript = ('contentScript' in worker) ? worker.contentScript : null;
|
||||
|
||||
if (contentScriptFile) {
|
||||
if (Array.isArray(contentScriptFile))
|
||||
this._importScripts.apply(this, contentScriptFile);
|
||||
else
|
||||
this._importScripts(contentScriptFile);
|
||||
}
|
||||
if (contentScript) {
|
||||
this._evaluate(
|
||||
Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
|
||||
);
|
||||
}
|
||||
},
|
||||
destroy: function destroy() {
|
||||
this.emitSync("detach");
|
||||
this._sandbox = null;
|
||||
this._addonWorker = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* JavaScript sandbox where all the content scripts are evaluated.
|
||||
* {Sandbox}
|
||||
*/
|
||||
_sandbox: null,
|
||||
|
||||
/**
|
||||
* Reference to the addon side of the worker.
|
||||
* @type {Worker}
|
||||
*/
|
||||
_addonWorker: null,
|
||||
|
||||
/**
|
||||
* Evaluates code in the sandbox.
|
||||
* @param {String} code
|
||||
* JavaScript source to evaluate.
|
||||
* @param {String} [filename='javascript:' + code]
|
||||
* Name of the file
|
||||
*/
|
||||
_evaluate: function(code, filename) {
|
||||
try {
|
||||
evaluate(this._sandbox, code, filename || 'javascript:' + code);
|
||||
}
|
||||
catch(e) {
|
||||
this._addonWorker._emit('error', e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Imports scripts to the sandbox by reading files under urls and
|
||||
* evaluating its source. If exception occurs during evaluation
|
||||
* `"error"` event is emitted on the worker.
|
||||
* This is actually an analog to the `importScript` method in web
|
||||
* workers but in our case it's not exposed even though content
|
||||
* scripts may be able to do it synchronously since IO operation
|
||||
* takes place in the UI process.
|
||||
*/
|
||||
_importScripts: function _importScripts(url) {
|
||||
let urls = Array.slice(arguments, 0);
|
||||
for each (let contentScriptFile in urls) {
|
||||
try {
|
||||
let uri = URL(contentScriptFile);
|
||||
if (uri.scheme === 'resource')
|
||||
load(this._sandbox, String(uri));
|
||||
else
|
||||
throw Error("Unsupported `contentScriptFile` url: " + String(uri));
|
||||
}
|
||||
catch(e) {
|
||||
this._addonWorker._emit('error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Message-passing facility for communication between code running
|
||||
* in the content and add-on process.
|
||||
* @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
|
||||
*/
|
||||
const Worker = EventEmitter.compose({
|
||||
on: Trait.required,
|
||||
_removeAllListeners: Trait.required,
|
||||
|
||||
// List of messages fired before worker is initialized
|
||||
get _earlyEvents() {
|
||||
delete this._earlyEvents;
|
||||
this._earlyEvents = [];
|
||||
return this._earlyEvents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message to the worker's global scope. Method takes single
|
||||
* argument, which represents data to be sent to the worker. The data may
|
||||
* be any primitive type value or `JSON`. Call of this method asynchronously
|
||||
* emits `message` event with data value in the global scope of this
|
||||
* symbiont.
|
||||
*
|
||||
* `message` event listeners can be set either by calling
|
||||
* `self.on` with a first argument string `"message"` or by
|
||||
* implementing `onMessage` function in the global scope of this worker.
|
||||
* @param {Number|String|JSON} data
|
||||
*/
|
||||
postMessage: function (data) {
|
||||
let args = ['message'].concat(Array.slice(arguments));
|
||||
if (!this._inited) {
|
||||
this._earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
processMessage.apply(this, args);
|
||||
},
|
||||
|
||||
/**
|
||||
* EventEmitter, that behaves (calls listeners) asynchronously.
|
||||
* A way to send customized messages to / from the worker.
|
||||
* Events from in the worker can be observed / emitted via
|
||||
* worker.on / worker.emit.
|
||||
*/
|
||||
get port() {
|
||||
// We generate dynamically this attribute as it needs to be accessible
|
||||
// before Worker.constructor gets called. (For ex: Panel)
|
||||
|
||||
// create an event emitter that receive and send events from/to the worker
|
||||
this._port = EventEmitterTrait.create({
|
||||
emit: this._emitEventToContent.bind(this)
|
||||
});
|
||||
|
||||
// expose wrapped port, that exposes only public properties:
|
||||
// We need to destroy this getter in order to be able to set the
|
||||
// final value. We need to update only public port attribute as we never
|
||||
// try to access port attribute from private API.
|
||||
delete this._public.port;
|
||||
this._public.port = Cortex(this._port);
|
||||
// Replicate public port to the private object
|
||||
delete this.port;
|
||||
this.port = this._public.port;
|
||||
|
||||
return this._port;
|
||||
},
|
||||
|
||||
/**
|
||||
* Same object than this.port but private API.
|
||||
* Allow access to _emit, in order to send event to port.
|
||||
*/
|
||||
_port: null,
|
||||
|
||||
/**
|
||||
* Emit a custom event to the content script,
|
||||
* i.e. emit this event on `self.port`
|
||||
*/
|
||||
_emitEventToContent: function () {
|
||||
let args = ['event'].concat(Array.slice(arguments));
|
||||
if (!this._inited) {
|
||||
this._earlyEvents.push(args);
|
||||
return;
|
||||
}
|
||||
processMessage.apply(this, args);
|
||||
},
|
||||
|
||||
// Is worker connected to the content worker sandbox ?
|
||||
_inited: false,
|
||||
|
||||
// Is worker being frozen? i.e related document is frozen in bfcache.
|
||||
// Content script should not be reachable if frozen.
|
||||
_frozen: true,
|
||||
|
||||
constructor: function Worker(options) {
|
||||
options = options || {};
|
||||
|
||||
if ('contentScriptFile' in options)
|
||||
this.contentScriptFile = options.contentScriptFile;
|
||||
if ('contentScriptOptions' in options)
|
||||
this.contentScriptOptions = options.contentScriptOptions;
|
||||
if ('contentScript' in options)
|
||||
this.contentScript = options.contentScript;
|
||||
|
||||
this._setListeners(options);
|
||||
|
||||
unload.ensure(this._public, "destroy");
|
||||
|
||||
// Ensure that worker._port is initialized for contentWorker to be able
|
||||
// to send events during worker initialization.
|
||||
this.port;
|
||||
|
||||
this._documentUnload = this._documentUnload.bind(this);
|
||||
this._pageShow = this._pageShow.bind(this);
|
||||
this._pageHide = this._pageHide.bind(this);
|
||||
|
||||
if ("window" in options) this._attach(options.window);
|
||||
},
|
||||
|
||||
_setListeners: function(options) {
|
||||
if ('onError' in options)
|
||||
this.on('error', options.onError);
|
||||
if ('onMessage' in options)
|
||||
this.on('message', options.onMessage);
|
||||
if ('onDetach' in options)
|
||||
this.on('detach', options.onDetach);
|
||||
},
|
||||
|
||||
_attach: function(window) {
|
||||
this._window = window;
|
||||
// Track document unload to destroy this worker.
|
||||
// We can't watch for unload event on page's window object as it
|
||||
// prevents bfcache from working:
|
||||
// https://developer.mozilla.org/En/Working_with_BFCache
|
||||
this._windowID = getInnerId(this._window);
|
||||
observers.on("inner-window-destroyed", this._documentUnload);
|
||||
|
||||
// Listen to pagehide event in order to freeze the content script
|
||||
// while the document is frozen in bfcache:
|
||||
this._window.addEventListener("pageshow", this._pageShow, true);
|
||||
this._window.addEventListener("pagehide", this._pageHide, true);
|
||||
|
||||
// will set this._contentWorker pointing to the private API:
|
||||
this._contentWorker = WorkerSandbox(this);
|
||||
|
||||
// Mainly enable worker.port.emit to send event to the content worker
|
||||
this._inited = true;
|
||||
this._frozen = false;
|
||||
|
||||
// Process all events and messages that were fired before the
|
||||
// worker was initialized.
|
||||
this._earlyEvents.forEach((function (args) {
|
||||
processMessage.apply(this, args);
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
_documentUnload: function _documentUnload({ subject, data }) {
|
||||
let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
if (innerWinID != this._windowID) return false;
|
||||
this._workerCleanup();
|
||||
return true;
|
||||
},
|
||||
|
||||
_pageShow: function _pageShow() {
|
||||
this._contentWorker.emitSync("pageshow");
|
||||
this._emit("pageshow");
|
||||
this._frozen = false;
|
||||
},
|
||||
|
||||
_pageHide: function _pageHide() {
|
||||
this._contentWorker.emitSync("pagehide");
|
||||
this._emit("pagehide");
|
||||
this._frozen = true;
|
||||
},
|
||||
|
||||
get url() {
|
||||
// this._window will be null after detach
|
||||
return this._window ? this._window.document.location.href : null;
|
||||
},
|
||||
|
||||
get tab() {
|
||||
// this._window will be null after detach
|
||||
if (this._window)
|
||||
return getTabForWindow(this._window);
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells content worker to unload itself and
|
||||
* removes all the references from itself.
|
||||
*/
|
||||
destroy: function destroy() {
|
||||
this._workerCleanup();
|
||||
this._inited = true;
|
||||
this._removeAllListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all internal references to the attached document
|
||||
* Tells _port to unload itself and removes all the references from itself.
|
||||
*/
|
||||
_workerCleanup: function _workerCleanup() {
|
||||
// maybe unloaded before content side is created
|
||||
// As Symbiont call worker.constructor on document load
|
||||
if (this._contentWorker)
|
||||
this._contentWorker.destroy();
|
||||
this._contentWorker = null;
|
||||
if (this._window) {
|
||||
this._window.removeEventListener("pageshow", this._pageShow, true);
|
||||
this._window.removeEventListener("pagehide", this._pageHide, true);
|
||||
}
|
||||
this._window = null;
|
||||
// This method may be called multiple times,
|
||||
// avoid dispatching `detach` event more than once
|
||||
if (this._windowID) {
|
||||
this._windowID = null;
|
||||
observers.off("inner-window-destroyed", this._documentUnload);
|
||||
this._earlyEvents.length = 0;
|
||||
this._emit("detach");
|
||||
}
|
||||
this._inited = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Receive an event from the content script that need to be sent to
|
||||
* worker.port. Provide a way for composed object to catch all events.
|
||||
*/
|
||||
_onContentScriptEvent: function _onContentScriptEvent() {
|
||||
this._port._emit.apply(this._port, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reference to the content side of the worker.
|
||||
* @type {WorkerGlobalScope}
|
||||
*/
|
||||
_contentWorker: null,
|
||||
|
||||
/**
|
||||
* Reference to the window that is accessible from
|
||||
* the content scripts.
|
||||
* @type {Object}
|
||||
*/
|
||||
_window: null,
|
||||
|
||||
/**
|
||||
* Flag to enable `addon` object injection in document. (bug 612726)
|
||||
* @type {Boolean}
|
||||
*/
|
||||
_injectInDocument: false
|
||||
});
|
||||
|
||||
/**
|
||||
* Fired from postMessage and _emitEventToContent, or from the _earlyMessage
|
||||
* queue when fired before the content is loaded. Sends arguments to
|
||||
* contentWorker if able
|
||||
*/
|
||||
|
||||
function processMessage () {
|
||||
if (!this._contentWorker)
|
||||
throw new Error(ERR_DESTROYED);
|
||||
if (this._frozen)
|
||||
throw new Error(ERR_FROZEN);
|
||||
|
||||
this._contentWorker.emit.apply(null, Array.slice(arguments));
|
||||
}
|
||||
|
||||
exports.Worker = Worker;
|
|
@ -16,6 +16,7 @@ const { ns } = require('../core/namespace');
|
|||
const event = ns();
|
||||
|
||||
const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/;
|
||||
exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN;
|
||||
|
||||
// Utility function to access given event `target` object's event listeners for
|
||||
// the specific event `type`. If listeners for this type does not exists they
|
||||
|
@ -161,9 +162,10 @@ function setListeners(target, listeners) {
|
|||
Object.keys(listeners || {}).forEach(key => {
|
||||
let match = EVENT_TYPE_PATTERN.exec(key);
|
||||
let type = match && match[1].toLowerCase();
|
||||
let listener = listeners[key];
|
||||
if (!type) return;
|
||||
|
||||
if (type && typeof(listener) === 'function')
|
||||
let listener = listeners[key];
|
||||
if (typeof(listener) === 'function')
|
||||
on(target, type, listener);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ module.metadata = {
|
|||
"stability": "unstable"
|
||||
};
|
||||
|
||||
let { emit, on, once, off } = require("./core");
|
||||
let { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core");
|
||||
|
||||
// This module provides set of high order function for working with event
|
||||
// streams (streams in a NodeJS style that dispatch data, end and error
|
||||
|
@ -255,3 +255,21 @@ Reactor.prototype.run = function(input) {
|
|||
this.onStart(input.value);
|
||||
};
|
||||
exports.Reactor = Reactor;
|
||||
|
||||
/**
|
||||
* Takes an object used as options with potential keys like 'onMessage',
|
||||
* used to be called `require('sdk/event/core').setListeners` on.
|
||||
* This strips all keys that would trigger a listener to be set.
|
||||
*
|
||||
* @params {Object} object
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function stripListeners (object) {
|
||||
return Object.keys(object).reduce((agg, key) => {
|
||||
if (!EVENT_TYPE_PATTERN.test(key))
|
||||
agg[key] = object[key];
|
||||
return agg;
|
||||
}, {});
|
||||
}
|
||||
exports.stripListeners = stripListeners;
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
/* 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 observers = require("../deprecated/observer-service");
|
||||
const { on } = require("../system/events");
|
||||
const core = require("./core");
|
||||
const { id: jetpackId} = require('../self');
|
||||
|
||||
const OPTIONS_DISPLAYED = "addon-options-displayed";
|
||||
|
||||
function onOptionsDisplayed(document, addonId) {
|
||||
function onOptionsDisplayed({ subjec: document, data: addonId }) {
|
||||
if (addonId !== jetpackId)
|
||||
return;
|
||||
let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' +
|
||||
|
@ -40,5 +39,4 @@ function onOptionsDisplayed(document, addonId) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
observers.add(OPTIONS_DISPLAYED, onOptionsDisplayed);
|
||||
on(OPTIONS_DISPLAYED, onOptionsDisplayed);
|
||||
|
|
|
@ -7,7 +7,7 @@ module.metadata = {
|
|||
"stability": "stable"
|
||||
};
|
||||
|
||||
const observers = require('./deprecated/observer-service');
|
||||
const observers = require('./system/events');
|
||||
const { Loader, validationAttributes } = require('./content/loader');
|
||||
const { Worker } = require('./content/worker');
|
||||
const { Registry } = require('./util/registry');
|
||||
|
@ -100,7 +100,7 @@ const PageMod = Loader.compose(EventEmitter, {
|
|||
|
||||
let include = options.include;
|
||||
let rules = this.include = Rules();
|
||||
|
||||
|
||||
if (!include)
|
||||
throw new Error('The `include` option must always contain atleast one rule');
|
||||
|
||||
|
@ -217,13 +217,13 @@ const PageModManager = Registry.resolve({
|
|||
}).compose({
|
||||
constructor: function PageModRegistry(constructor) {
|
||||
this._init(PageMod);
|
||||
observers.add(
|
||||
observers.on(
|
||||
'document-element-inserted',
|
||||
this._onContentWindow = this._onContentWindow.bind(this)
|
||||
);
|
||||
},
|
||||
_destructor: function _destructor() {
|
||||
observers.remove('document-element-inserted', this._onContentWindow);
|
||||
observers.off('document-element-inserted', this._onContentWindow);
|
||||
this._removeAllListeners();
|
||||
|
||||
// We need to do some cleaning er PageMods, like unregistering any
|
||||
|
@ -234,7 +234,7 @@ const PageModManager = Registry.resolve({
|
|||
|
||||
this._registryDestructor();
|
||||
},
|
||||
_onContentWindow: function _onContentWindow(document) {
|
||||
_onContentWindow: function _onContentWindow({ subject: document }) {
|
||||
let window = document.defaultView;
|
||||
// XML documents don't have windows, and we don't yet support them.
|
||||
if (!window)
|
||||
|
|
|
@ -9,8 +9,9 @@ module.metadata = {
|
|||
|
||||
const { Class } = require('./core/heritage');
|
||||
const { on, emit, off, setListeners } = require('./event/core');
|
||||
const { filter, pipe, map, merge: streamMerge } = require('./event/utils');
|
||||
const { WorkerHost, Worker, detach, attach, destroy } = require('./worker/utils');
|
||||
const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./event/utils');
|
||||
const { detach, attach, destroy, WorkerHost } = require('./content/utils');
|
||||
const { Worker } = require('./content/worker');
|
||||
const { Disposable } = require('./core/disposable');
|
||||
const { EventTarget } = require('./event/target');
|
||||
const { unload } = require('./system/unload');
|
||||
|
@ -66,8 +67,8 @@ function disableScript (page) {
|
|||
|
||||
function Allow (page) {
|
||||
return {
|
||||
get script() getDocShell(viewFor(page)).allowJavascript,
|
||||
set script(value) value ? enableScript(page) : disableScript(page)
|
||||
get script() { return getDocShell(viewFor(page)).allowJavascript; },
|
||||
set script(value) { return value ? enableScript(page) : disableScript(page); }
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -89,7 +90,6 @@ const Page = Class({
|
|||
setup: function Page(options) {
|
||||
let page = this;
|
||||
options = pageContract(options);
|
||||
setListeners(this, options);
|
||||
let view = makeFrame(window.document, {
|
||||
nodeName: 'iframe',
|
||||
type: 'content',
|
||||
|
@ -100,12 +100,15 @@ const Page = Class({
|
|||
});
|
||||
|
||||
['contentScriptFile', 'contentScript', 'contentScriptWhen']
|
||||
.forEach(function (prop) page[prop] = options[prop]);
|
||||
.forEach(prop => page[prop] = options[prop]);
|
||||
|
||||
views.set(this, view);
|
||||
pages.set(view, this);
|
||||
|
||||
let worker = new Worker(options);
|
||||
// Set listeners on the {Page} object itself, not the underlying worker,
|
||||
// like `onMessage`, as it gets piped
|
||||
setListeners(this, options);
|
||||
let worker = new Worker(stripListeners(options));
|
||||
workers.set(this, worker);
|
||||
pipe(worker, this);
|
||||
|
||||
|
@ -114,7 +117,7 @@ const Page = Class({
|
|||
this.rules.add.apply(this.rules, [].concat(this.include || options.include));
|
||||
}
|
||||
},
|
||||
get allow() Allow(this),
|
||||
get allow() { return Allow(this); },
|
||||
set allow(value) {
|
||||
let allowJavascript = pageContract({ allow: value }).allow.script;
|
||||
return allowJavascript ? enableScript(this) : disableScript(this);
|
||||
|
@ -133,7 +136,7 @@ const Page = Class({
|
|||
views.delete(this);
|
||||
destroy(workers.get(this));
|
||||
},
|
||||
toString: function () '[object Page]'
|
||||
toString: function () { return '[object Page]' }
|
||||
});
|
||||
|
||||
exports.Page = Page;
|
||||
|
|
|
@ -19,8 +19,8 @@ const { isPrivateBrowsingSupported } = require('./self');
|
|||
const { isWindowPBSupported } = require('./private-browsing/utils');
|
||||
const { Class } = require("./core/heritage");
|
||||
const { merge } = require("./util/object");
|
||||
const { WorkerHost, Worker, detach, attach, destroy,
|
||||
requiresAddonGlobal } = require("./worker/utils");
|
||||
const { WorkerHost, detach, attach, destroy } = require("./content/utils");
|
||||
const { Worker } = require("./content/worker");
|
||||
const { Disposable } = require("./core/disposable");
|
||||
const { contract: loaderContract } = require("./content/loader");
|
||||
const { contract } = require("./util/contract");
|
||||
|
@ -29,7 +29,7 @@ const { EventTarget } = require("./event/target");
|
|||
const domPanel = require("./panel/utils");
|
||||
const { events } = require("./panel/events");
|
||||
const systemEvents = require("./system/events");
|
||||
const { filter, pipe } = require("./event/utils");
|
||||
const { filter, pipe, stripListeners } = require("./event/utils");
|
||||
const { getNodeView, getActiveView } = require("./view/core");
|
||||
const { isNil, isObject } = require("./lang/type");
|
||||
const { getAttachEventType } = require("./content/utils");
|
||||
|
@ -117,8 +117,6 @@ const Panel = Class({
|
|||
}, panelContract(options));
|
||||
models.set(this, model);
|
||||
|
||||
// Setup listeners.
|
||||
setListeners(this, options);
|
||||
|
||||
// Setup view
|
||||
let view = domPanel.make();
|
||||
|
@ -130,7 +128,9 @@ const Panel = Class({
|
|||
|
||||
setupAutoHide(this);
|
||||
|
||||
let worker = new Worker(options);
|
||||
// Setup listeners.
|
||||
setListeners(this, options);
|
||||
let worker = new Worker(stripListeners(options));
|
||||
workers.set(this, worker);
|
||||
|
||||
// pipe events from worker to a panel.
|
||||
|
|
|
@ -8,10 +8,9 @@ module.metadata = {
|
|||
};
|
||||
|
||||
const { emit, off } = require("./event/core");
|
||||
const { when: unload } = require("./system/unload");
|
||||
const { PrefsTarget } = require("./preferences/event-target");
|
||||
const { id } = require("./self");
|
||||
const observers = require("./deprecated/observer-service");
|
||||
const { on } = require("./system/events");
|
||||
|
||||
const ADDON_BRANCH = "extensions." + id + ".";
|
||||
const BUTTON_PRESSED = id + "-cmdPressed";
|
||||
|
@ -19,14 +18,9 @@ const BUTTON_PRESSED = id + "-cmdPressed";
|
|||
const target = PrefsTarget({ branchName: ADDON_BRANCH });
|
||||
|
||||
// Listen to clicks on buttons
|
||||
function buttonClick(subject, data) {
|
||||
function buttonClick({ data }) {
|
||||
emit(target, data);
|
||||
}
|
||||
observers.add(BUTTON_PRESSED, buttonClick);
|
||||
|
||||
// Make sure we cleanup listeners on unload.
|
||||
unload(function() {
|
||||
observers.remove(BUTTON_PRESSED, buttonClick);
|
||||
});
|
||||
on(BUTTON_PRESSED, buttonClick);
|
||||
|
||||
module.exports = target;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* 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";
|
||||
|
||||
module.metadata = {
|
||||
|
@ -12,17 +11,25 @@ const { Cc, Ci, Cu } = require("chrome");
|
|||
const { Loader } = require('./loader');
|
||||
const { serializeStack, parseStack } = require("toolkit/loader");
|
||||
const { setTimeout } = require('../timers');
|
||||
const memory = require('../deprecated/memory');
|
||||
const { PlainTextConsole } = require("../console/plain-text");
|
||||
const { when: unload } = require("../system/unload");
|
||||
const { format, fromException } = require("../console/traceback");
|
||||
const system = require("../system");
|
||||
const memory = require('../deprecated/memory');
|
||||
const { gc: gcPromise } = require('./memory');
|
||||
const { defer } = require('../core/promise');
|
||||
|
||||
// Trick manifest builder to make it think we need these modules ?
|
||||
const unit = require("../deprecated/unit-test");
|
||||
const test = require("../../test");
|
||||
const url = require("../url");
|
||||
|
||||
function emptyPromise() {
|
||||
let { promise, resolve } = defer();
|
||||
resolve();
|
||||
return promise;
|
||||
}
|
||||
|
||||
var cService = Cc['@mozilla.org/consoleservice;1'].getService()
|
||||
.QueryInterface(Ci.nsIConsoleService);
|
||||
|
||||
|
@ -143,36 +150,30 @@ function dictDiff(last, curr) {
|
|||
}
|
||||
|
||||
function reportMemoryUsage() {
|
||||
memory.gc();
|
||||
|
||||
var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
|
||||
.getService(Ci.nsIMemoryReporterManager);
|
||||
|
||||
// Bug 916501: this code is *so* bogus -- nsIMemoryReporter changed its |memoryUsed|
|
||||
// field to |amount| *years* ago, and even bigger changes have happened
|
||||
// since -- that it must just never be run.
|
||||
var reporters = mgr.enumerateReporters();
|
||||
if (reporters.hasMoreElements())
|
||||
print("\n");
|
||||
|
||||
while (reporters.hasMoreElements()) {
|
||||
var reporter = reporters.getNext();
|
||||
reporter.QueryInterface(Ci.nsIMemoryReporter);
|
||||
print(reporter.description + ": " + reporter.memoryUsed + "\n");
|
||||
if (!profileMemory) {
|
||||
return emptyPromise();
|
||||
}
|
||||
|
||||
var weakrefs = [info.weakref.get()
|
||||
for each (info in memory.getObjects())];
|
||||
weakrefs = [weakref for each (weakref in weakrefs) if (weakref)];
|
||||
print("Tracked memory objects in testing sandbox: " +
|
||||
weakrefs.length + "\n");
|
||||
return gcPromise().then((function () {
|
||||
var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
|
||||
.getService(Ci.nsIMemoryReporterManager);
|
||||
let count = 0;
|
||||
function logReporter(process, path, kind, units, amount, description) {
|
||||
print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n");
|
||||
}
|
||||
mgr.getReportsForThisProcess(logReporter, null);
|
||||
|
||||
var weakrefs = [info.weakref.get()
|
||||
for each (info in memory.getObjects())];
|
||||
weakrefs = [weakref for each (weakref in weakrefs) if (weakref)];
|
||||
print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n");
|
||||
}));
|
||||
}
|
||||
|
||||
var gWeakrefInfo;
|
||||
|
||||
function checkMemory() {
|
||||
memory.gc();
|
||||
Cu.schedulePreciseGC(function () {
|
||||
return gcPromise().then(_ => {
|
||||
let leaks = getPotentialLeaks();
|
||||
|
||||
let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) {
|
||||
|
@ -188,12 +189,12 @@ function checkMemory() {
|
|||
|
||||
for (let url of windowURLs)
|
||||
console.warn("LEAKED", leaks.windows[url]);
|
||||
|
||||
showResults();
|
||||
});
|
||||
}).then(showResults);
|
||||
}
|
||||
|
||||
function showResults() {
|
||||
let { promise, resolve } = defer();
|
||||
|
||||
if (gWeakrefInfo) {
|
||||
gWeakrefInfo.forEach(
|
||||
function(info) {
|
||||
|
@ -211,6 +212,9 @@ function showResults() {
|
|||
}
|
||||
|
||||
onDone(results);
|
||||
|
||||
resolve();
|
||||
return promise;
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
|
@ -250,7 +254,8 @@ function cleanup() {
|
|||
loader = null;
|
||||
|
||||
memory.gc();
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
results.failed++;
|
||||
console.error("unload.send() threw an exception.");
|
||||
console.exception(e);
|
||||
|
@ -333,7 +338,7 @@ function getPotentialLeaks() {
|
|||
console.error("Unable to parse compartment detail " + matches[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let item = {
|
||||
path: matches[1],
|
||||
principal: details[1],
|
||||
|
@ -349,8 +354,7 @@ function getPotentialLeaks() {
|
|||
return;
|
||||
}
|
||||
|
||||
matches = windowRegexp.exec(path);
|
||||
if (matches) {
|
||||
if (matches = windowRegexp.exec(path)) {
|
||||
if (matches[1] in windows)
|
||||
return;
|
||||
|
||||
|
@ -374,10 +378,9 @@ function getPotentialLeaks() {
|
|||
}
|
||||
}
|
||||
|
||||
let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
|
||||
getService(Ci.nsIMemoryReporterManager);
|
||||
|
||||
mgr.getReportsForThisProcess(logReporter, null);
|
||||
Cc["@mozilla.org/memory-reporter-manager;1"]
|
||||
.getService(Ci.nsIMemoryReporterManager)
|
||||
.getReportsForThisProcess(logReporter, null);
|
||||
|
||||
return { compartments: compartments, windows: windows };
|
||||
}
|
||||
|
@ -387,22 +390,28 @@ function nextIteration(tests) {
|
|||
results.passed += tests.passed;
|
||||
results.failed += tests.failed;
|
||||
|
||||
if (profileMemory)
|
||||
reportMemoryUsage();
|
||||
|
||||
let testRun = [];
|
||||
for each (let test in tests.testRunSummary) {
|
||||
let testCopy = {};
|
||||
for (let info in test) {
|
||||
testCopy[info] = test[info];
|
||||
reportMemoryUsage().then(_ => {
|
||||
let testRun = [];
|
||||
for each (let test in tests.testRunSummary) {
|
||||
let testCopy = {};
|
||||
for (let info in test) {
|
||||
testCopy[info] = test[info];
|
||||
}
|
||||
testRun.push(testCopy);
|
||||
}
|
||||
testRun.push(testCopy);
|
||||
}
|
||||
|
||||
results.testRuns.push(testRun);
|
||||
iterationsLeft--;
|
||||
results.testRuns.push(testRun);
|
||||
iterationsLeft--;
|
||||
|
||||
checkForEnd();
|
||||
})
|
||||
}
|
||||
else {
|
||||
checkForEnd();
|
||||
}
|
||||
}
|
||||
|
||||
function checkForEnd() {
|
||||
if (iterationsLeft && (!stopOnError || results.failed == 0)) {
|
||||
// Pass the loader which has a hooked console that doesn't dispatch
|
||||
// errors to the JS console and avoid firing false alarm in our
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const memory = require('../deprecated/memory');
|
||||
const { defer } = require('../core/promise');
|
||||
|
||||
function gc() {
|
||||
let { promise, resolve } = defer();
|
||||
|
||||
Cu.forceGC();
|
||||
memory.gc();
|
||||
|
||||
Cu.schedulePreciseGC(_ => resolve());
|
||||
|
||||
return promise;
|
||||
}
|
||||
exports.gc = gc;
|
|
@ -10,6 +10,17 @@ module.metadata = {
|
|||
}
|
||||
};
|
||||
|
||||
// Because Firefox Holly, we still need to check if `CustomizableUI` is
|
||||
// available. Once Australis will officially land, we can safely remove it.
|
||||
// See Bug 959142
|
||||
try {
|
||||
require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
|
||||
}
|
||||
catch (e) {
|
||||
throw Error('Unsupported Application: The module ' + module.id +
|
||||
' does not support this application.');
|
||||
}
|
||||
|
||||
const { Class } = require('../../core/heritage');
|
||||
const { merge } = require('../../util/object');
|
||||
const { Disposable } = require('../../core/disposable');
|
||||
|
|
|
@ -10,6 +10,17 @@ module.metadata = {
|
|||
}
|
||||
};
|
||||
|
||||
// Because Firefox Holly, we still need to check if `CustomizableUI` is
|
||||
// available. Once Australis will officially land, we can safely remove it.
|
||||
// See Bug 959142
|
||||
try {
|
||||
require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
|
||||
}
|
||||
catch (e) {
|
||||
throw Error('Unsupported Application: The module ' + module.id +
|
||||
' does not support this application.');
|
||||
}
|
||||
|
||||
const { Class } = require('../../core/heritage');
|
||||
const { merge } = require('../../util/object');
|
||||
const { Disposable } = require('../../core/disposable');
|
||||
|
|
|
@ -14,7 +14,8 @@ const { Cu } = require('chrome');
|
|||
const { on, off, emit } = require('../../event/core');
|
||||
|
||||
const { id: addonID, data } = require('sdk/self');
|
||||
const buttonPrefix = 'button--' + addonID.replace(/@/g, '-at-');
|
||||
const buttonPrefix =
|
||||
'button--' + addonID.toLowerCase().replace(/[^a-z0-9-_]/g, '');
|
||||
|
||||
const { isObject } = require('../../lang/type');
|
||||
|
||||
|
|
|
@ -10,6 +10,17 @@ module.metadata = {
|
|||
}
|
||||
};
|
||||
|
||||
// Because Firefox Holly, we still need to check if `CustomizableUI` is
|
||||
// available. Once Australis will officially land, we can safely remove it.
|
||||
// See Bug 959142
|
||||
try {
|
||||
require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {});
|
||||
}
|
||||
catch (e) {
|
||||
throw Error("Unsupported Application: The module" + module.id +
|
||||
" does not support this application.");
|
||||
}
|
||||
|
||||
require("./frame/view");
|
||||
const { Frame } = require("./frame/model");
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = requ
|
|||
const { ns } = require('../core/namespace');
|
||||
const { remove: removeFromArray } = require('../util/array');
|
||||
const { show, hide, toggle } = require('./sidebar/actions');
|
||||
const { Worker: WorkerTrait } = require('../content/worker');
|
||||
const { Worker } = require('../content/worker');
|
||||
const { contract: sidebarContract } = require('./sidebar/contract');
|
||||
const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view');
|
||||
const { defer } = require('../core/promise');
|
||||
|
@ -34,12 +34,6 @@ const { ensure } = require('../system/unload');
|
|||
const { identify } = require('./id');
|
||||
const { uuid } = require('../util/uuid');
|
||||
|
||||
const Worker = WorkerTrait.resolve({
|
||||
_injectInDocument: '__injectInDocument'
|
||||
}).compose({
|
||||
get _injectInDocument() true
|
||||
});
|
||||
|
||||
const sidebarNS = ns();
|
||||
|
||||
const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
|
||||
|
@ -118,7 +112,8 @@ const Sidebar = Class({
|
|||
}
|
||||
|
||||
let worker = windowNS(window).worker = Worker({
|
||||
window: panelBrowser.contentWindow
|
||||
window: panelBrowser.contentWindow,
|
||||
injectInDocument: true
|
||||
});
|
||||
|
||||
function onWebPanelSidebarUnload() {
|
||||
|
|
|
@ -10,6 +10,17 @@ module.metadata = {
|
|||
}
|
||||
};
|
||||
|
||||
// Because Firefox Holly, we still need to check if `CustomizableUI` is
|
||||
// available. Once Australis will officially land, we can safely remove it.
|
||||
// See Bug 959142
|
||||
try {
|
||||
require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {});
|
||||
}
|
||||
catch (e) {
|
||||
throw Error("Unsupported Application: The module" + module.id +
|
||||
" does not support this application.");
|
||||
}
|
||||
|
||||
const { Toolbar } = require("./toolbar/model");
|
||||
require("./toolbar/view");
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ module.metadata = {
|
|||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const array = require('../util/array');
|
||||
const observers = require('../deprecated/observer-service');
|
||||
const { defer } = require('sdk/core/promise');
|
||||
|
||||
const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1'].
|
||||
|
@ -151,25 +150,6 @@ exports.getWindowLoadingContext = getWindowLoadingContext;
|
|||
const isTopLevel = window => window && getToplevelWindow(window) === window;
|
||||
exports.isTopLevel = isTopLevel;
|
||||
|
||||
/**
|
||||
* Removes given window from the application's window registry. Unless
|
||||
* `options.close` is `false` window is automatically closed on application
|
||||
* quit.
|
||||
* @params {nsIDOMWindow} window
|
||||
* @params {Boolean} options.close
|
||||
*/
|
||||
function backgroundify(window, options) {
|
||||
let base = getBaseWindow(window);
|
||||
base.visibility = false;
|
||||
base.enabled = false;
|
||||
appShellService.unregisterTopLevelWindow(getXULWindow(window));
|
||||
if (!options || options.close !== false)
|
||||
observers.add('quit-application-granted', window.close.bind(window));
|
||||
|
||||
return window;
|
||||
}
|
||||
exports.backgroundify = backgroundify;
|
||||
|
||||
/**
|
||||
* Takes hash of options and serializes it to a features string that
|
||||
* can be used passed to `window.open`. For more details on features string see:
|
||||
|
|
|
@ -2,102 +2,18 @@
|
|||
* 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";
|
||||
'use strict';
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
'stability': 'deprecated'
|
||||
};
|
||||
|
||||
// This module attempts to hide trait based nature of the worker so that
|
||||
// code depending on workers could be de-trait-ified without changing worker
|
||||
// implementation.
|
||||
const {
|
||||
requiresAddonGlobal, attach, detach, destroy, WorkerHost
|
||||
} = require('../content/utils');
|
||||
|
||||
const { Worker: WorkerTrait } = require("../content/worker");
|
||||
const { Loader } = require("../content/loader");
|
||||
const { merge } = require("../util/object");
|
||||
const { emit } = require("../event/core");
|
||||
|
||||
let assetsURI = require("../self").data.url();
|
||||
let isArray = Array.isArray;
|
||||
|
||||
function isAddonContent({ contentURL }) {
|
||||
return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
|
||||
}
|
||||
|
||||
function hasContentScript({ contentScript, contentScriptFile }) {
|
||||
return (isArray(contentScript) ? contentScript.length > 0 :
|
||||
!!contentScript) ||
|
||||
(isArray(contentScriptFile) ? contentScriptFile.length > 0 :
|
||||
!!contentScriptFile);
|
||||
}
|
||||
|
||||
function requiresAddonGlobal(model) {
|
||||
return isAddonContent(model) && !hasContentScript(model);
|
||||
}
|
||||
exports.requiresAddonGlobal = requiresAddonGlobal;
|
||||
|
||||
|
||||
const LegacyWorker = WorkerTrait.compose(Loader).resolve({
|
||||
_setListeners: "__setListeners",
|
||||
_injectInDocument: "__injectInDocument",
|
||||
contentURL: "__contentURL"
|
||||
}).compose({
|
||||
_setListeners: function() {},
|
||||
get contentURL() this._window.document.URL,
|
||||
get _injectInDocument() requiresAddonGlobal(this),
|
||||
attach: function(window) this._attach(window),
|
||||
detach: function() this._workerCleanup()
|
||||
});
|
||||
|
||||
// Weak map that stores mapping between regular worker instances and
|
||||
// legacy trait based worker instances.
|
||||
let traits = new WeakMap();
|
||||
|
||||
function traitFor(worker) traits.get(worker, null);
|
||||
|
||||
function WorkerHost(workerFor) {
|
||||
// Define worker properties that just proxy to a wrapped trait.
|
||||
return ["postMessage", "port", "url", "tab"].reduce(function(proto, name) {
|
||||
Object.defineProperty(proto, name, {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: function() traitFor(workerFor(this))[name],
|
||||
set: function(value) traitFor(workerFor(this))[name] = value
|
||||
});
|
||||
return proto;
|
||||
}, {});
|
||||
}
|
||||
exports.WorkerHost = WorkerHost;
|
||||
|
||||
// Type representing worker instance.
|
||||
function Worker(options) {
|
||||
let worker = Object.create(Worker.prototype);
|
||||
let trait = new LegacyWorker(options);
|
||||
["pageshow", "pagehide", "detach", "message", "error"].forEach(function(key) {
|
||||
trait.on(key, function() {
|
||||
emit.apply(emit, [worker, key].concat(Array.slice(arguments)));
|
||||
});
|
||||
});
|
||||
traits.set(worker, trait);
|
||||
return worker;
|
||||
}
|
||||
exports.Worker = Worker;
|
||||
|
||||
function detach(worker) {
|
||||
let trait = traitFor(worker);
|
||||
if (trait) trait.detach();
|
||||
}
|
||||
exports.detach = detach;
|
||||
|
||||
function attach(worker, window) {
|
||||
let trait = traitFor(worker);
|
||||
// Cleanup the worker before injecting the content script into a new document.
|
||||
trait.attach(window);
|
||||
}
|
||||
exports.attach = attach;
|
||||
|
||||
function destroy(worker) {
|
||||
let trait = traitFor(worker);
|
||||
if (trait) trait.destroy();
|
||||
}
|
||||
exports.destroy = destroy;
|
||||
exports.requiresAddonGlobal = requiresAddonGlobal;
|
||||
|
|
|
@ -613,8 +613,20 @@ const Require = iced(function Require(loader, requirer) {
|
|||
// We also freeze module to prevent it from further changes
|
||||
// at runtime.
|
||||
if (!(uri in modules)) {
|
||||
// Many of the loader's functionalities are dependent
|
||||
// on modules[uri] being set before loading, so we set it and
|
||||
// remove it if we have any errors.
|
||||
module = modules[uri] = Module(requirement, uri);
|
||||
freeze(load(loader, module));
|
||||
try {
|
||||
freeze(load(loader, module));
|
||||
}
|
||||
catch (e) {
|
||||
// Clear out modules cache so we can throw on a second invalid require
|
||||
delete modules[uri];
|
||||
// Also clear out the Sandbox that was created
|
||||
delete loader.sandboxes[uri];
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return module.exports;
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"loader": "sdk/loader/loader",
|
||||
"memory": "sdk/deprecated/memory",
|
||||
"namespace": "sdk/core/namespace",
|
||||
"observer-service": "sdk/deprecated/observer-service",
|
||||
"preferences-service": "sdk/preferences/service",
|
||||
"promise": "sdk/core/promise",
|
||||
"system": "sdk/system",
|
||||
|
|
|
@ -96,9 +96,6 @@ exports["test compatibility"] = function(assert) {
|
|||
assert.equal(require("xhr"),
|
||||
require("sdk/net/xhr"), "sdk/io/xhr -> xhr");
|
||||
|
||||
assert.equal(require("observer-service"),
|
||||
require("sdk/deprecated/observer-service"), "sdk/deprecated/observer-service -> observer-service");
|
||||
|
||||
assert.equal(require("private-browsing"),
|
||||
require("sdk/private-browsing"), "sdk/private-browsing -> private-browsing");
|
||||
|
||||
|
@ -147,9 +144,6 @@ exports["test compatibility"] = function(assert) {
|
|||
assert.equal(require("tabs/utils"),
|
||||
require("sdk/tabs/utils"), "sdk/tabs/utils -> tabs/utils");
|
||||
|
||||
assert.equal(require("app-strings"),
|
||||
require("sdk/deprecated/app-strings"), "sdk/deprecated/app-strings -> app-strings");
|
||||
|
||||
assert.equal(require("dom/events"),
|
||||
require("sdk/dom/events"), "sdk/dom/events -> dom/events");
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
const { data } = require("sdk/self");
|
||||
const { Symbiont } = require("sdk/content/symbiont");
|
||||
const { Symbiont } = require("sdk/deprecated/symbiont");
|
||||
|
||||
exports["test:direct communication with trusted document"] = function(assert, done) {
|
||||
let worker = Symbiont({
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
an invalid json file
|
|
@ -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/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
require('./not-found');
|
||||
}
|
||||
catch (e1) {
|
||||
exports.firstError = e1;
|
||||
// It should throw again and not be cached
|
||||
try {
|
||||
require('./not-found');
|
||||
}
|
||||
catch (e2) {
|
||||
exports.secondError = e2;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
require('./file.json');
|
||||
}
|
||||
catch (e) {
|
||||
exports.invalidJSON1 = e;
|
||||
try {
|
||||
require('./file.json');
|
||||
}
|
||||
catch (e) {
|
||||
exports.invalidJSON2 = e;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ const timer = require('sdk/timers');
|
|||
const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
|
||||
const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils');
|
||||
const { open, focus, close } = require('sdk/window/helpers');
|
||||
const { StringBundle } = require('sdk/deprecated/app-strings');
|
||||
const tabs = require('sdk/tabs');
|
||||
const { browserWindows } = require('sdk/windows');
|
||||
const { set: setPref } = require("sdk/preferences/service");
|
||||
|
@ -19,13 +18,17 @@ const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAA
|
|||
|
||||
// Bug 682681 - tab.title should never be empty
|
||||
exports.testBug682681_aboutURI = function(assert, done) {
|
||||
let tabStrings = StringBundle('chrome://browser/locale/tabbrowser.properties');
|
||||
let url = 'chrome://browser/locale/tabbrowser.properties';
|
||||
let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
|
||||
getService(Ci.nsIStringBundleService).
|
||||
createBundle(url);
|
||||
let emptyTabTitle = stringBundle.GetStringFromName('tabs.emptyTabTitle');
|
||||
|
||||
tabs.on('ready', function onReady(tab) {
|
||||
tabs.removeListener('ready', onReady);
|
||||
|
||||
assert.equal(tab.title,
|
||||
tabStrings.get('tabs.emptyTabTitle'),
|
||||
emptyTabTitle,
|
||||
"title of about: tab is not blank");
|
||||
|
||||
tab.close(done);
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
/* 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 { Cc, Ci, Cu } = require("chrome");
|
||||
const AddonInstaller = require("sdk/addon/installer");
|
||||
const observers = require("sdk/deprecated/observer-service");
|
||||
const { on, off } = require("sdk/system/events");
|
||||
const { setTimeout } = require("sdk/timers");
|
||||
const tmp = require("sdk/test/tmp-file");
|
||||
const system = require("sdk/system");
|
||||
|
@ -20,10 +19,10 @@ exports["test Install"] = function (assert, done) {
|
|||
|
||||
// Save all events distpatched by bootstrap.js of the installed addon
|
||||
let events = [];
|
||||
function eventsObserver(subject, data) {
|
||||
function eventsObserver({ data }) {
|
||||
events.push(data);
|
||||
}
|
||||
observers.add("addon-install-unit-test", eventsObserver, false);
|
||||
on("addon-install-unit-test", eventsObserver);
|
||||
|
||||
// Install the test addon
|
||||
AddonInstaller.install(ADDON_PATH).then(
|
||||
|
@ -39,13 +38,13 @@ exports["test Install"] = function (assert, done) {
|
|||
JSON.stringify(expectedEvents),
|
||||
"addon's bootstrap.js functions have been called");
|
||||
|
||||
observers.remove("addon-install-unit-test", eventsObserver);
|
||||
off("addon-install-unit-test", eventsObserver);
|
||||
done();
|
||||
});
|
||||
},
|
||||
function onFailure(code) {
|
||||
assert.fail("Install failed: "+code);
|
||||
observers.remove("addon-install-unit-test", eventsObserver);
|
||||
off("addon-install-unit-test", eventsObserver);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
@ -84,10 +83,8 @@ exports["test Update"] = function (assert, done) {
|
|||
// Save all events distpatched by bootstrap.js of the installed addon
|
||||
let events = [];
|
||||
let iteration = 1;
|
||||
function eventsObserver(subject, data) {
|
||||
events.push(data);
|
||||
}
|
||||
observers.add("addon-install-unit-test", eventsObserver);
|
||||
let eventsObserver = ({data}) => events.push(data);
|
||||
on("addon-install-unit-test", eventsObserver);
|
||||
|
||||
function onInstalled(id) {
|
||||
let prefix = "[" + iteration + "] ";
|
||||
|
@ -115,14 +112,14 @@ exports["test Update"] = function (assert, done) {
|
|||
JSON.stringify(expectedEvents),
|
||||
prefix + "addon's bootstrap.js functions have been called");
|
||||
|
||||
observers.remove("addon-install-unit-test", eventsObserver);
|
||||
off("addon-install-unit-test", eventsObserver);
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
function onFailure(code) {
|
||||
assert.fail("Install failed: "+code);
|
||||
observers.remove("addon-install-unit-test", eventsObserver);
|
||||
off("addon-install-unit-test", eventsObserver);
|
||||
done();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,31 +4,6 @@
|
|||
|
||||
const apiUtils = require("sdk/deprecated/api-utils");
|
||||
|
||||
exports.testPublicConstructor = function (assert) {
|
||||
function PrivateCtor() {}
|
||||
PrivateCtor.prototype = {};
|
||||
|
||||
let PublicCtor = apiUtils.publicConstructor(PrivateCtor);
|
||||
assert.ok(
|
||||
PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype),
|
||||
"PrivateCtor.prototype should be prototype of PublicCtor.prototype"
|
||||
);
|
||||
|
||||
function testObj(useNew) {
|
||||
let obj = useNew ? new PublicCtor() : PublicCtor();
|
||||
assert.ok(obj instanceof PublicCtor,
|
||||
"Object should be instance of PublicCtor");
|
||||
assert.ok(obj instanceof PrivateCtor,
|
||||
"Object should be instance of PrivateCtor");
|
||||
assert.ok(PublicCtor.prototype.isPrototypeOf(obj),
|
||||
"PublicCtor's prototype should be prototype of object");
|
||||
assert.equal(obj.constructor, PublicCtor,
|
||||
"Object constructor should be PublicCtor");
|
||||
}
|
||||
testObj(true);
|
||||
testObj(false);
|
||||
};
|
||||
|
||||
exports.testValidateOptionsEmpty = function (assert) {
|
||||
let val = apiUtils.validateOptions(null, {});
|
||||
|
||||
|
|
|
@ -1,68 +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";
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { StringBundle } = require("sdk/deprecated/app-strings");
|
||||
|
||||
exports.testStringBundle = function(assert) {
|
||||
let url = "chrome://global/locale/security/caps.properties";
|
||||
|
||||
let strings = StringBundle(url);
|
||||
|
||||
assert.equal(strings.url, url,
|
||||
"'url' property contains correct URL of string bundle");
|
||||
|
||||
let appLocale = Cc["@mozilla.org/intl/nslocaleservice;1"].
|
||||
getService(Ci.nsILocaleService).
|
||||
getApplicationLocale();
|
||||
|
||||
let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
|
||||
getService(Ci.nsIStringBundleService).
|
||||
createBundle(url, appLocale);
|
||||
|
||||
let (name = "CheckMessage") {
|
||||
assert.equal(strings.get(name), stringBundle.GetStringFromName(name),
|
||||
"getting a string returns the string");
|
||||
}
|
||||
|
||||
let (name = "CreateWrapperDenied", args = ["foo"]) {
|
||||
assert.equal(strings.get(name, args),
|
||||
stringBundle.formatStringFromName(name, args, args.length),
|
||||
"getting a formatted string returns the formatted string");
|
||||
}
|
||||
|
||||
assert.throws(function () strings.get("nonexistentString"),
|
||||
RegExp("String 'nonexistentString' could not be retrieved from " +
|
||||
"the bundle due to an unknown error \\(it doesn't exist\\?\\)\\."),
|
||||
"retrieving a nonexistent string throws an exception");
|
||||
|
||||
let a = [], b = [];
|
||||
let enumerator = stringBundle.getSimpleEnumeration();
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
|
||||
a.push([elem.key, elem.value]);
|
||||
}
|
||||
|
||||
for (let key in strings) {
|
||||
b.push([ key, strings.get(key) ]);
|
||||
}
|
||||
|
||||
// Sort the arrays, because we don't assume enumeration has a set order.
|
||||
// Sort compares [key, val] as string "key,val", which sorts the way we want
|
||||
// it to, so there is no need to provide a custom compare function.
|
||||
a.sort();
|
||||
b.sort();
|
||||
|
||||
assert.equal(a.length, b.length,
|
||||
"the iterator returns the correct number of items");
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
assert.equal(a[i][0], b[i][0], "the iterated string's name is correct");
|
||||
assert.equal(a[i][1], b[i][1],
|
||||
"the iterated string's value is correct");
|
||||
}
|
||||
};
|
||||
|
||||
require("sdk/test").run(exports);
|
|
@ -4,7 +4,7 @@
|
|||
"use strict";
|
||||
|
||||
const { Cc, Ci } = require('chrome');
|
||||
const { Symbiont } = require('sdk/content/symbiont');
|
||||
const { Symbiont } = require('sdk/deprecated/symbiont');
|
||||
const self = require('sdk/self');
|
||||
const fixtures = require("./fixtures");
|
||||
const { close } = require('sdk/window/helpers');
|
||||
|
|
|
@ -12,6 +12,7 @@ module.metadata = {
|
|||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { on } = require("sdk/event/core");
|
||||
const { setTimeout } = require("sdk/timers");
|
||||
const { LoaderWithHookedConsole } = require("sdk/test/loader");
|
||||
const { Worker } = require("sdk/content/worker");
|
||||
|
@ -116,6 +117,8 @@ exports["test:sample"] = WorkerTest(
|
|||
|
||||
assert.equal(worker.url, window.location.href,
|
||||
"worker.url works");
|
||||
assert.equal(worker.contentURL, window.location.href,
|
||||
"worker.contentURL works");
|
||||
worker.postMessage("hi!");
|
||||
}
|
||||
);
|
||||
|
@ -226,7 +229,7 @@ exports["test:post-json-values-only"] = WorkerTest(
|
|||
self.postMessage([ message.fun === undefined,
|
||||
typeof message.w,
|
||||
message.w && "port" in message.w,
|
||||
message.w.url,
|
||||
message.w._url,
|
||||
Array.isArray(message.array),
|
||||
JSON.stringify(message.array)]);
|
||||
});
|
||||
|
@ -247,6 +250,10 @@ exports["test:post-json-values-only"] = WorkerTest(
|
|||
"Array is correctly serialized");
|
||||
done();
|
||||
});
|
||||
// Add a new url property sa the Class function used by
|
||||
// Worker doesn't set enumerables to true for non-functions
|
||||
worker._url = DEFAULT_CONTENT_URL;
|
||||
|
||||
worker.postMessage({ fun: function () {}, w: worker, array: array });
|
||||
}
|
||||
);
|
||||
|
@ -264,7 +271,7 @@ exports["test:emit-json-values-only"] = WorkerTest(
|
|||
fun === null,
|
||||
typeof w,
|
||||
"port" in w,
|
||||
w.url,
|
||||
w._url,
|
||||
"fun" in obj,
|
||||
Object.keys(obj.dom).length,
|
||||
Array.isArray(array),
|
||||
|
@ -295,6 +302,9 @@ exports["test:emit-json-values-only"] = WorkerTest(
|
|||
fun: function () {},
|
||||
dom: browser.contentWindow.document.createElement("div")
|
||||
};
|
||||
// Add a new url property sa the Class function used by
|
||||
// Worker doesn't set enumerables to true for non-functions
|
||||
worker._url = DEFAULT_CONTENT_URL;
|
||||
worker.port.emit("addon-to-content", function () {}, worker, obj, array);
|
||||
}
|
||||
);
|
||||
|
@ -829,4 +839,37 @@ exports['test:conentScriptFile as URL instance'] = WorkerTest(
|
|||
}
|
||||
);
|
||||
|
||||
exports.testWorkerEvents = WorkerTest(DEFAULT_CONTENT_URL, function (assert, browser, done) {
|
||||
let window = browser.contentWindow;
|
||||
let events = [];
|
||||
let worker = Worker({
|
||||
window: window,
|
||||
contentScript: 'new ' + function WorkerScope() {
|
||||
self.postMessage('start');
|
||||
},
|
||||
onAttach: win => {
|
||||
events.push('attach');
|
||||
assert.pass('attach event called when attached');
|
||||
assert.equal(window, win, 'attach event passes in attached window');
|
||||
},
|
||||
onError: err => {
|
||||
assert.equal(err.message, 'Custom',
|
||||
'Error passed into error event');
|
||||
worker.detach();
|
||||
},
|
||||
onMessage: msg => {
|
||||
assert.pass('`onMessage` handles postMessage')
|
||||
throw new Error('Custom');
|
||||
},
|
||||
onDetach: _ => {
|
||||
assert.pass('`onDetach` called when worker detached');
|
||||
done();
|
||||
}
|
||||
});
|
||||
// `attach` event is called synchronously during instantiation,
|
||||
// so we can't listen to that, TODO FIX?
|
||||
// worker.on('attach', obj => console.log('attach', obj));
|
||||
});
|
||||
|
||||
|
||||
require("test").run(exports);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const { on, once, off, emit, count, amass } = require('sdk/event/core');
|
||||
const { on, once, off, emit, count } = require('sdk/event/core');
|
||||
const { LoaderWithHookedConsole } = require("sdk/test/loader");
|
||||
|
||||
exports['test add a listener'] = function(assert) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
'use strict';
|
||||
|
||||
const { on, emit } = require("sdk/event/core");
|
||||
const { filter, map, merge, expand, pipe } = require("sdk/event/utils");
|
||||
const { filter, map, merge, expand, pipe, stripListeners } = require("sdk/event/utils");
|
||||
const $ = require("./event/helpers");
|
||||
|
||||
function isEven(x) !(x % 2)
|
||||
|
@ -168,7 +168,7 @@ exports["test expand"] = function(assert) {
|
|||
exports["test pipe"] = function (assert, done) {
|
||||
let src = {};
|
||||
let dest = {};
|
||||
|
||||
|
||||
let aneventCount = 0, multiargsCount = 0;
|
||||
let wildcardCount = {};
|
||||
|
||||
|
@ -184,7 +184,7 @@ exports["test pipe"] = function (assert, done) {
|
|||
++multiargsCount;
|
||||
check();
|
||||
});
|
||||
|
||||
|
||||
on(dest, '*', (name, ...data) => {
|
||||
wildcardCount[name] = (wildcardCount[name] || 0) + 1;
|
||||
if (name === 'multiargs') {
|
||||
|
@ -201,12 +201,12 @@ exports["test pipe"] = function (assert, done) {
|
|||
|
||||
for (let i = 0; i < 3; i++)
|
||||
emit(src, 'an-event', 'my-arg');
|
||||
|
||||
|
||||
emit(src, 'multiargs', 'a', 'b', 'c');
|
||||
|
||||
function check () {
|
||||
if (aneventCount === 3 && multiargsCount === 1 &&
|
||||
wildcardCount['an-event'] === 3 &&
|
||||
wildcardCount['an-event'] === 3 &&
|
||||
wildcardCount['multiargs'] === 1)
|
||||
done();
|
||||
}
|
||||
|
@ -237,7 +237,7 @@ exports["test pipe multiple targets"] = function (assert) {
|
|||
assert.equal(middleFired, 1, 'event passes through the middle of pipe chain');
|
||||
assert.equal(destFired, 1, 'event propagates to end of pipe chain');
|
||||
assert.equal(src2Fired, 0, 'event does not fire on alternative chain routes');
|
||||
|
||||
|
||||
emit(src2, 'ev');
|
||||
assert.equal(src2Fired, 1, 'event triggers in source in pipe chain');
|
||||
assert.equal(middleFired, 2,
|
||||
|
@ -245,7 +245,7 @@ exports["test pipe multiple targets"] = function (assert) {
|
|||
assert.equal(destFired, 2,
|
||||
'event propagates to end of pipe chain from different src');
|
||||
assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes');
|
||||
|
||||
|
||||
emit(middle, 'ev');
|
||||
assert.equal(middleFired, 3,
|
||||
'event triggers in source of pipe chain');
|
||||
|
@ -255,4 +255,28 @@ exports["test pipe multiple targets"] = function (assert) {
|
|||
assert.equal(src2Fired, 1, 'event does not fire on alternative chain routes');
|
||||
};
|
||||
|
||||
exports['test stripListeners'] = function (assert) {
|
||||
var options = {
|
||||
onAnEvent: noop1,
|
||||
onMessage: noop2,
|
||||
verb: noop1,
|
||||
value: 100
|
||||
};
|
||||
|
||||
var stripped = stripListeners(options);
|
||||
assert.ok(stripped !== options, 'stripListeners should return a new object');
|
||||
assert.equal(options.onAnEvent, noop1, 'stripListeners does not affect original');
|
||||
assert.equal(options.onMessage, noop2, 'stripListeners does not affect original');
|
||||
assert.equal(options.verb, noop1, 'stripListeners does not affect original');
|
||||
assert.equal(options.value, 100, 'stripListeners does not affect original');
|
||||
|
||||
assert.ok(!stripped.onAnEvent, 'stripListeners removes `on*` values');
|
||||
assert.ok(!stripped.onMessage, 'stripListeners removes `on*` values');
|
||||
assert.equal(stripped.verb, noop1, 'stripListeners leaves not `on*` values');
|
||||
assert.equal(stripped.value, 100, 'stripListeners leaves not `on*` values');
|
||||
|
||||
function noop1 () {}
|
||||
function noop2 () {}
|
||||
};
|
||||
|
||||
require('test').run(exports);
|
||||
|
|
|
@ -101,6 +101,13 @@ exports['test syntax errors'] = function(assert) {
|
|||
}
|
||||
}
|
||||
|
||||
exports['test sandboxes are not added if error'] = function (assert) {
|
||||
let uri = root + '/fixtures/loader/missing-twice/';
|
||||
let loader = Loader({ paths: { '': uri } });
|
||||
let program = main(loader, 'main');
|
||||
assert.ok(!(uri + 'not-found.js' in loader.sandboxes), 'not-found.js not in loader.sandboxes');
|
||||
}
|
||||
|
||||
exports['test missing module'] = function(assert) {
|
||||
let uri = root + '/fixtures/loader/missing/'
|
||||
let loader = Loader({ paths: { '': uri } });
|
||||
|
@ -128,6 +135,26 @@ exports['test missing module'] = function(assert) {
|
|||
}
|
||||
}
|
||||
|
||||
exports["test invalid module not cached and throws everytime"] = function(assert) {
|
||||
let uri = root + "/fixtures/loader/missing-twice/";
|
||||
let loader = Loader({ paths: { "": uri } });
|
||||
|
||||
let { firstError, secondError, invalidJSON1, invalidJSON2 } = main(loader, "main");
|
||||
assert.equal(firstError.message, "Module `not-found` is not found at " +
|
||||
uri + "not-found.js", "throws on first invalid require");
|
||||
assert.equal(firstError.lineNumber, 8, "first error is on line 7");
|
||||
assert.equal(secondError.message, "Module `not-found` is not found at " +
|
||||
uri + "not-found.js", "throws on second invalid require");
|
||||
assert.equal(secondError.lineNumber, 14, "second error is on line 14");
|
||||
|
||||
assert.equal(invalidJSON1.message,
|
||||
"JSON.parse: unexpected character at line 1 column 1 of the JSON data",
|
||||
"throws on invalid JSON");
|
||||
assert.equal(invalidJSON2.message,
|
||||
"JSON.parse: unexpected character at line 1 column 1 of the JSON data",
|
||||
"throws on invalid JSON second time");
|
||||
};
|
||||
|
||||
exports['test exceptions in modules'] = function(assert) {
|
||||
let uri = root + '/fixtures/loader/exceptions/'
|
||||
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
/* 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 memory = require("sdk/deprecated/memory");
|
||||
const memory = require("sdk/deprecated/memory");
|
||||
const { gc } = require("sdk/test/memory");
|
||||
|
||||
exports.testMemory = function(assert) {
|
||||
assert.pass("Skipping this test until Gecko memory debugging issues " +
|
||||
"are resolved (see bug 592774).");
|
||||
return;
|
||||
|
||||
var obj = {};
|
||||
memory.track(obj, "testMemory.testObj");
|
||||
|
||||
var objs = memory.getObjects("testMemory.testObj");
|
||||
assert.equal(objs[0].weakref.get(), obj);
|
||||
obj = null;
|
||||
memory.gc();
|
||||
assert.equal(objs[0].weakref.get(), null);
|
||||
|
||||
gc().then(function() {
|
||||
assert.equal(objs[0].weakref.get(), null);
|
||||
});
|
||||
};
|
||||
|
||||
require('sdk/test').run(exports);
|
||||
|
|
|
@ -1,79 +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/. */
|
||||
|
||||
const observers = require("sdk/deprecated/observer-service");
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { LoaderWithHookedConsole2 } = require("sdk/test/loader");
|
||||
|
||||
exports.testUnloadAndErrorLogging = function(assert) {
|
||||
let { loader, messages } = LoaderWithHookedConsole2(module);
|
||||
var sbobsvc = loader.require("sdk/deprecated/observer-service");
|
||||
|
||||
var timesCalled = 0;
|
||||
var cb = function(subject, data) {
|
||||
timesCalled++;
|
||||
};
|
||||
var badCb = function(subject, data) {
|
||||
throw new Error("foo");
|
||||
};
|
||||
sbobsvc.add("blarg", cb);
|
||||
observers.notify("blarg", "yo yo");
|
||||
assert.equal(timesCalled, 1);
|
||||
sbobsvc.add("narg", badCb);
|
||||
observers.notify("narg", "yo yo");
|
||||
|
||||
assert.equal(messages[0], "console.error: " + require("sdk/self").name + ": \n");
|
||||
var lines = messages[1].split("\n");
|
||||
assert.equal(lines[0], " Message: Error: foo");
|
||||
assert.equal(lines[1], " Stack:");
|
||||
// Keep in mind to update "18" to the line of "throw new Error("foo")"
|
||||
assert.ok(lines[2].indexOf(module.uri + ":18") != -1);
|
||||
|
||||
loader.unload();
|
||||
observers.notify("blarg", "yo yo");
|
||||
assert.equal(timesCalled, 1);
|
||||
};
|
||||
|
||||
exports.testObserverService = function(assert) {
|
||||
var ios = Cc['@mozilla.org/network/io-service;1']
|
||||
.getService(Ci.nsIIOService);
|
||||
var service = Cc["@mozilla.org/observer-service;1"].
|
||||
getService(Ci.nsIObserverService);
|
||||
var uri = ios.newURI("http://www.foo.com", null, null);
|
||||
var timesCalled = 0;
|
||||
var lastSubject = null;
|
||||
var lastData = null;
|
||||
|
||||
var cb = function(subject, data) {
|
||||
timesCalled++;
|
||||
lastSubject = subject;
|
||||
lastData = data;
|
||||
};
|
||||
|
||||
observers.add("blarg", cb);
|
||||
service.notifyObservers(uri, "blarg", "some data");
|
||||
assert.equal(timesCalled, 1,
|
||||
"observer-service.add() should call callback");
|
||||
assert.equal(lastSubject, uri,
|
||||
"observer-service.add() should pass subject");
|
||||
assert.equal(lastData, "some data",
|
||||
"observer-service.add() should pass data");
|
||||
|
||||
function customSubject() {}
|
||||
function customData() {}
|
||||
observers.notify("blarg", customSubject, customData);
|
||||
assert.equal(timesCalled, 2,
|
||||
"observer-service.notify() should work");
|
||||
assert.equal(lastSubject, customSubject,
|
||||
"observer-service.notify() should pass+wrap subject");
|
||||
assert.equal(lastData, customData,
|
||||
"observer-service.notify() should pass data");
|
||||
|
||||
observers.remove("blarg", cb);
|
||||
service.notifyObservers(null, "blarg", "some data");
|
||||
assert.equal(timesCalled, 2,
|
||||
"observer-service.remove() should work");
|
||||
};
|
||||
|
||||
require('sdk/test').run(exports);
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
const { Loader } = require("sdk/test/loader");
|
||||
const { setTimeout } = require("sdk/timers");
|
||||
const { notify } = require("sdk/deprecated/observer-service");
|
||||
const { emit } = require("sdk/system/events");
|
||||
const { id } = require("sdk/self");
|
||||
const simplePrefs = require("sdk/simple-prefs");
|
||||
const { prefs: sp } = simplePrefs;
|
||||
|
@ -132,7 +132,7 @@ exports.testBtnListener = function(assert, done) {
|
|||
assert.pass("Button press event was heard");
|
||||
done();
|
||||
});
|
||||
notify((id + "-cmdPressed"), "", name);
|
||||
emit((id + "-cmdPressed"), { subject: "", data: name });
|
||||
};
|
||||
|
||||
exports.testPrefRemoveListener = function(assert, done) {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/* 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 { Cc, Ci, Cu, components } = require('chrome');
|
||||
const { gc } = require('sdk/test/memory');
|
||||
|
||||
exports.testGC = function(assert, done) {
|
||||
let weakref;
|
||||
let (tempObj = {}) {
|
||||
weakref = Cu.getWeakReference(tempObj);
|
||||
assert.equal(weakref.get(), tempObj, 'the weakref returned the tempObj');
|
||||
}
|
||||
|
||||
gc().then(function(arg) {
|
||||
assert.equal(arg, undefined, 'there is no argument');
|
||||
assert.pass('gc() returns a promise which eventually resolves');
|
||||
assert.equal(weakref.get(), undefined, 'the weakref returned undefined');
|
||||
}).then(done).then(null, assert.fail);
|
||||
};
|
||||
|
||||
require('sdk/test').run(exports);
|
|
@ -1,9 +1,11 @@
|
|||
/* 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 traceback = require("sdk/console/traceback");
|
||||
var {Cc,Ci,Cr,Cu} = require("chrome");
|
||||
const { on, off } = require("sdk/system/events");
|
||||
|
||||
function throwNsIException() {
|
||||
var ios = Cc['@mozilla.org/network/io-service;1']
|
||||
|
@ -16,7 +18,6 @@ function throwError() {
|
|||
}
|
||||
|
||||
exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
|
||||
var observers = require("sdk/deprecated/observer-service");
|
||||
["http", "https"].forEach(
|
||||
function(scheme) {
|
||||
var httpRequests = 0;
|
||||
|
@ -24,7 +25,7 @@ exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
|
|||
httpRequests++;
|
||||
}
|
||||
|
||||
observers.add("http-on-modify-request", onHttp);
|
||||
on("http-on-modify-request", onHttp);
|
||||
|
||||
try {
|
||||
var tb = [{filename: scheme + "://www.mozilla.org/",
|
||||
|
@ -35,7 +36,7 @@ exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
|
|||
assert.fail(e);
|
||||
}
|
||||
|
||||
observers.remove("http-on-modify-request", onHttp);
|
||||
off("http-on-modify-request", onHttp);
|
||||
|
||||
assert.equal(httpRequests, 0,
|
||||
"traceback.format() does not make " +
|
||||
|
|
|
@ -11,7 +11,7 @@ module.metadata = {
|
|||
};
|
||||
|
||||
const { Ci } = require('chrome');
|
||||
const { open, backgroundify, windows, isBrowser,
|
||||
const { open, windows, isBrowser,
|
||||
getXULWindow, getBaseWindow, getToplevelWindow, getMostRecentWindow,
|
||||
getMostRecentBrowserWindow } = require('sdk/window/utils');
|
||||
const { close } = require('sdk/window/helpers');
|
||||
|
@ -78,7 +78,7 @@ exports['test new top window with various URIs'] = function(assert, done) {
|
|||
}, msg);
|
||||
assert.throws(function () {
|
||||
open('https://foo');
|
||||
}, msg);
|
||||
}, msg);
|
||||
assert.throws(function () {
|
||||
open('ftp://foo');
|
||||
}, msg);
|
||||
|
@ -88,7 +88,7 @@ exports['test new top window with various URIs'] = function(assert, done) {
|
|||
|
||||
let chromeWindow = open('chrome://foo/content/');
|
||||
assert.ok(~windows().indexOf(chromeWindow), 'chrome URI works');
|
||||
|
||||
|
||||
let resourceWindow = open('resource://foo');
|
||||
assert.ok(~windows().indexOf(resourceWindow), 'resource URI works');
|
||||
|
||||
|
@ -96,22 +96,6 @@ exports['test new top window with various URIs'] = function(assert, done) {
|
|||
close(chromeWindow).then(close.bind(null, resourceWindow)).then(done);
|
||||
};
|
||||
|
||||
exports.testBackgroundify = function(assert, done) {
|
||||
let window = open('data:text/html;charset=utf-8,backgroundy');
|
||||
assert.ok(~windows().indexOf(window),
|
||||
'window is in the list of windows');
|
||||
let backgroundy = backgroundify(window);
|
||||
assert.equal(backgroundy, window, 'backgroundify returs give window back');
|
||||
assert.ok(!~windows().indexOf(window),
|
||||
'backgroundifyied window is in the list of windows');
|
||||
|
||||
// Wait for the window unload before ending test
|
||||
// backgroundified windows doesn't dispatch domwindowclosed event
|
||||
// so that we have to manually wait for unload event
|
||||
window.onunload = done;
|
||||
window.close();
|
||||
};
|
||||
|
||||
exports.testIsBrowser = function(assert) {
|
||||
// dummy window, bad type
|
||||
assert.equal(isBrowser({ document: { documentElement: { getAttribute: function() {
|
||||
|
|
|
@ -5,11 +5,7 @@
|
|||
|
||||
const { Loader } = require('sdk/test/loader');
|
||||
const { browserWindows } = require('sdk/windows');
|
||||
const { viewFor } = require('sdk/view/core');
|
||||
const { Ci } = require("chrome");
|
||||
const { isBrowser, getWindowTitle } = require("sdk/window/utils");
|
||||
const { defer } = require("sdk/lang/functional");
|
||||
|
||||
|
||||
// TEST: browserWindows Iterator
|
||||
exports.testBrowserWindowsIterator = function(assert) {
|
||||
|
@ -61,26 +57,4 @@ exports.testWindowActivateMethod_simple = function(assert) {
|
|||
'Active tab is active after window.activate() call');
|
||||
};
|
||||
|
||||
|
||||
exports["test getView(window)"] = function(assert, done) {
|
||||
browserWindows.once("open", window => {
|
||||
const view = viewFor(window);
|
||||
|
||||
assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
|
||||
assert.ok(isBrowser(view), "view is a browser window");
|
||||
assert.equal(getWindowTitle(view), window.title,
|
||||
"window has a right title");
|
||||
|
||||
window.close();
|
||||
// Defer handler cause window is destroyed after event is dispatched.
|
||||
browserWindows.once("close", defer(_ => {
|
||||
assert.equal(viewFor(window), null, "window view is gone");
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
browserWindows.open({ url: "data:text/html,<title>yo</title>" });
|
||||
};
|
||||
|
||||
require('sdk/test').run(exports);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
const { Cc, Ci } = require('chrome');
|
||||
const { setTimeout } = require('sdk/timers');
|
||||
const { Loader } = require('sdk/test/loader');
|
||||
const { onFocus, getMostRecentWindow, windows } = require('sdk/window/utils');
|
||||
const { onFocus, getMostRecentWindow, windows, isBrowser, getWindowTitle } = require('sdk/window/utils');
|
||||
const { open, close, focus } = require('sdk/window/helpers');
|
||||
const { browserWindows } = require("sdk/windows");
|
||||
const tabs = require("sdk/tabs");
|
||||
|
@ -14,6 +14,8 @@ const winUtils = require("sdk/deprecated/window-utils");
|
|||
const { WindowTracker } = winUtils;
|
||||
const { isPrivate } = require('sdk/private-browsing');
|
||||
const { isWindowPBSupported } = require('sdk/private-browsing/utils');
|
||||
const { viewFor } = require("sdk/view/core");
|
||||
const { defer } = require("sdk/lang/functional");
|
||||
|
||||
// TEST: open & close window
|
||||
exports.testOpenAndCloseWindow = function(assert, done) {
|
||||
|
@ -418,6 +420,26 @@ exports.testWindowIteratorPrivateDefault = function(assert, done) {
|
|||
|
||||
close(window).then(done);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports["test getView(window)"] = function(assert, done) {
|
||||
browserWindows.once("open", window => {
|
||||
const view = viewFor(window);
|
||||
|
||||
assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
|
||||
assert.ok(isBrowser(view), "view is a browser window");
|
||||
assert.equal(getWindowTitle(view), window.title,
|
||||
"window has a right title");
|
||||
|
||||
window.close();
|
||||
// Defer handler cause window is destroyed after event is dispatched.
|
||||
browserWindows.once("close", defer(_ => {
|
||||
assert.equal(viewFor(window), null, "window view is gone");
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
browserWindows.open({ url: "data:text/html,<title>yo</title>" });
|
||||
};
|
||||
|
||||
require('sdk/test').run(exports);
|
||||
|
|
|
@ -842,9 +842,6 @@ pref("browser.sessionstore.resume_session_once", false);
|
|||
|
||||
// minimal interval between two save operations in milliseconds
|
||||
pref("browser.sessionstore.interval", 15000);
|
||||
// maximum amount of POSTDATA to be saved in bytes per history entry (-1 = all of it)
|
||||
// (NB: POSTDATA will be saved either entirely or not at all)
|
||||
pref("browser.sessionstore.postdata", 0);
|
||||
// on which sites to save text data, POSTDATA and cookies
|
||||
// 0 = everywhere, 1 = unencrypted sites, 2 = nowhere
|
||||
pref("browser.sessionstore.privacy_level", 0);
|
||||
|
|
|
@ -89,7 +89,7 @@ let gBrowserThumbnails = {
|
|||
|
||||
filterForThumbnailExpiration:
|
||||
function Thumbnails_filterForThumbnailExpiration(aCallback) {
|
||||
aCallback([browser.currentURI.spec for (browser of gBrowser.browsers)]);
|
||||
aCallback(this._topSiteURLs);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -122,15 +122,14 @@ let gBrowserThumbnails = {
|
|||
},
|
||||
|
||||
_shouldCapture: function Thumbnails_shouldCapture(aBrowser) {
|
||||
// Capture only if it's a top site in about:newtab.
|
||||
if (!NewTabUtils.links.getLinks().some(
|
||||
(link) => link && link.url == aBrowser.currentURI.spec))
|
||||
return false;
|
||||
|
||||
// Capture only if it's the currently selected tab.
|
||||
if (aBrowser != gBrowser.selectedBrowser)
|
||||
return false;
|
||||
|
||||
// Only capture about:newtab top sites.
|
||||
if (this._topSiteURLs.indexOf(aBrowser.currentURI.spec) < 0)
|
||||
return false;
|
||||
|
||||
// Don't capture in per-window private browsing mode.
|
||||
if (PrivateBrowsingUtils.isWindowPrivate(window))
|
||||
return false;
|
||||
|
@ -190,6 +189,14 @@ let gBrowserThumbnails = {
|
|||
return true;
|
||||
},
|
||||
|
||||
get _topSiteURLs() {
|
||||
return NewTabUtils.links.getLinks().reduce((urls, link) => {
|
||||
if (link)
|
||||
urls.push(link.url);
|
||||
return urls;
|
||||
}, []);
|
||||
},
|
||||
|
||||
_clearTimeout: function Thumbnails_clearTimeout(aBrowser) {
|
||||
if (this._timeouts.has(aBrowser)) {
|
||||
aBrowser.removeEventListener("scroll", this, false);
|
||||
|
|
|
@ -903,6 +903,7 @@ chatbox:-moz-full-screen-ancestor > .chat-titlebar {
|
|||
|
||||
#customization-panelWrapper > .panel-arrowcontent {
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#customization-panelHolder > #PanelUI-mainView {
|
||||
|
@ -936,6 +937,10 @@ toolbarpaletteitem[dragover] {
|
|||
min-height: 3em;
|
||||
}
|
||||
|
||||
#customization-toolbar-visibility-button > .box-inherit > .button-menu-dropmarker {
|
||||
display: -moz-box;
|
||||
}
|
||||
|
||||
toolbarpaletteitem[place="palette"] {
|
||||
width: 10em;
|
||||
height: calc(40px + 2em);
|
||||
|
|
|
@ -4229,6 +4229,13 @@ nsBrowserAccess.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
function getTogglableToolbars() {
|
||||
let toolbarNodes = Array.slice(gNavToolbox.childNodes);
|
||||
toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
|
||||
toolbarNodes = toolbarNodes.filter(node => node.getAttribute("toolbarname"));
|
||||
return toolbarNodes;
|
||||
}
|
||||
|
||||
function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
|
||||
var popup = aEvent.target;
|
||||
if (popup != aEvent.currentTarget)
|
||||
|
@ -4243,28 +4250,24 @@ function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
|
|||
|
||||
var firstMenuItem = aInsertPoint || popup.firstChild;
|
||||
|
||||
let toolbarNodes = Array.slice(gNavToolbox.childNodes);
|
||||
toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
|
||||
let toolbarNodes = getTogglableToolbars();
|
||||
|
||||
for (let toolbar of toolbarNodes) {
|
||||
let toolbarName = toolbar.getAttribute("toolbarname");
|
||||
if (toolbarName) {
|
||||
let menuItem = document.createElement("menuitem");
|
||||
let hidingAttribute = toolbar.getAttribute("type") == "menubar" ?
|
||||
"autohide" : "collapsed";
|
||||
menuItem.setAttribute("id", "toggle_" + toolbar.id);
|
||||
menuItem.setAttribute("toolbarId", toolbar.id);
|
||||
menuItem.setAttribute("type", "checkbox");
|
||||
menuItem.setAttribute("label", toolbarName);
|
||||
menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true");
|
||||
menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
|
||||
if (popup.id != "toolbar-context-menu")
|
||||
menuItem.setAttribute("key", toolbar.getAttribute("key"));
|
||||
let menuItem = document.createElement("menuitem");
|
||||
let hidingAttribute = toolbar.getAttribute("type") == "menubar" ?
|
||||
"autohide" : "collapsed";
|
||||
menuItem.setAttribute("id", "toggle_" + toolbar.id);
|
||||
menuItem.setAttribute("toolbarId", toolbar.id);
|
||||
menuItem.setAttribute("type", "checkbox");
|
||||
menuItem.setAttribute("label", toolbar.getAttribute("toolbarname"));
|
||||
menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true");
|
||||
menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
|
||||
if (popup.id != "toolbar-context-menu")
|
||||
menuItem.setAttribute("key", toolbar.getAttribute("key"));
|
||||
|
||||
popup.insertBefore(menuItem, firstMenuItem);
|
||||
popup.insertBefore(menuItem, firstMenuItem);
|
||||
|
||||
menuItem.addEventListener("command", onViewToolbarCommand, false);
|
||||
}
|
||||
menuItem.addEventListener("command", onViewToolbarCommand, false);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,9 +4,15 @@
|
|||
|
||||
<hbox id="customization-container" flex="1" hidden="true">
|
||||
<vbox flex="1" id="customization-palette-container">
|
||||
<label id="customization-header" value="&customizeMode.menuAndToolbars.header;"/>
|
||||
<label id="customization-header">
|
||||
&customizeMode.menuAndToolbars.header;
|
||||
</label>
|
||||
<vbox id="customization-palette" flex="1"/>
|
||||
<hbox pack="start">
|
||||
<hbox>
|
||||
<button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
|
||||
<menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
|
||||
</button>
|
||||
<spacer flex="1"/>
|
||||
<button id="customization-reset-button" oncommand="gCustomizeMode.reset();" label="&customizeMode.restoreDefaults;" class="customizationmode-button"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
|
|
@ -15,6 +15,7 @@ const kAboutURI = "about:customizing";
|
|||
const kDragDataTypePrefix = "text/toolbarwrapper-id/";
|
||||
const kPlaceholderClass = "panel-customization-placeholder";
|
||||
const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
|
||||
const kToolbarVisibilityBtn = "customization-toolbar-visibility-button";
|
||||
const kMaxTransitionDurationMs = 2000;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
@ -113,6 +114,16 @@ CustomizeMode.prototype = {
|
|||
yield delayedStartupDeferred.promise;
|
||||
}
|
||||
|
||||
let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
|
||||
let togglableToolbars = window.getTogglableToolbars();
|
||||
let bookmarksToolbar = document.getElementById("PersonalToolbar");
|
||||
if (togglableToolbars.length == 0 ||
|
||||
(togglableToolbars.length == 1 && togglableToolbars[0] == bookmarksToolbar)) {
|
||||
toolbarVisibilityBtn.setAttribute("hidden", "true");
|
||||
} else {
|
||||
toolbarVisibilityBtn.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
// Disable lightweight themes while in customization mode since
|
||||
// they don't have large enough images to pad the full browser window.
|
||||
if (this.document.documentElement._lightweightTheme)
|
||||
|
@ -571,6 +582,9 @@ CustomizeMode.prototype = {
|
|||
|
||||
if (aNode.hasAttribute("flex")) {
|
||||
wrapper.setAttribute("flex", aNode.getAttribute("flex"));
|
||||
if (aPlace == "palette") {
|
||||
aNode.removeAttribute("flex");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -637,6 +651,10 @@ CustomizeMode.prototype = {
|
|||
toolbarItem.checked = true;
|
||||
}
|
||||
|
||||
if (aWrapper.hasAttribute("flex") && !toolbarItem.hasAttribute("flex")) {
|
||||
toolbarItem.setAttribute("flex", aWrapper.getAttribute("flex"));
|
||||
}
|
||||
|
||||
if (aWrapper.hasAttribute("itemcommand")) {
|
||||
let commandID = aWrapper.getAttribute("itemcommand");
|
||||
toolbarItem.setAttribute("command", commandID);
|
||||
|
@ -915,6 +933,7 @@ CustomizeMode.prototype = {
|
|||
let dt = aEvent.dataTransfer;
|
||||
let documentId = aEvent.target.ownerDocument.documentElement.id;
|
||||
let draggedItem = item.firstChild;
|
||||
let isInToolbar = CustomizableUI.getPlaceForItem(item) == "toolbar";
|
||||
|
||||
dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
|
||||
dt.effectAllowed = "move";
|
||||
|
@ -935,6 +954,9 @@ CustomizeMode.prototype = {
|
|||
item.hidden = true;
|
||||
this._showPanelCustomizationPlaceholders();
|
||||
DragPositionManager.start(this.window);
|
||||
if (!isInToolbar && item.nextSibling) {
|
||||
this._setDragActive(item.nextSibling, "before", draggedItem.id, false);
|
||||
}
|
||||
}
|
||||
this._initializeDragAfterMove = null;
|
||||
this.window.clearTimeout(this._dragInitializeTimeout);
|
||||
|
@ -1284,7 +1306,7 @@ CustomizeMode.prototype = {
|
|||
let window = aItem.ownerDocument.defaultView;
|
||||
let draggedItem = window.document.getElementById(aDraggedItemId);
|
||||
if (!aInToolbar) {
|
||||
this._setPanelDragActive(aItem, draggedItem, aValue);
|
||||
this._setGridDragActive(aItem, draggedItem, aValue);
|
||||
} else {
|
||||
// Calculate width of the item when it'd be dropped in this position
|
||||
let width = this._getDragItemSize(aItem, draggedItem).width;
|
||||
|
@ -1330,7 +1352,7 @@ CustomizeMode.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_setPanelDragActive: function(aDragOverNode, aDraggedItem, aValue) {
|
||||
_setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) {
|
||||
let targetArea = this._getCustomizableParent(aDragOverNode);
|
||||
let positionManager = DragPositionManager.getManagerForArea(targetArea);
|
||||
let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
|
||||
|
|
|
@ -157,6 +157,9 @@ AreaPositionManager.prototype = {
|
|||
if (this.__moveDown) {
|
||||
shiftDown = true;
|
||||
}
|
||||
if (!this._lastPlaceholderInsertion) {
|
||||
child.setAttribute("notransition", "true");
|
||||
}
|
||||
// Determine the CSS transform based on the next node:
|
||||
child.style.transform = this._getNextPos(child, shiftDown, aSize);
|
||||
} else {
|
||||
|
@ -164,8 +167,17 @@ AreaPositionManager.prototype = {
|
|||
child.style.transform = "";
|
||||
}
|
||||
}
|
||||
if (aContainer.lastChild && !this._lastPlaceholderInsertion) {
|
||||
// Flush layout:
|
||||
aContainer.lastChild.getBoundingClientRect();
|
||||
// then remove all the [notransition]
|
||||
for (let child of aContainer.children) {
|
||||
child.removeAttribute("notransition");
|
||||
}
|
||||
}
|
||||
delete this.__moveDown;
|
||||
delete this.__undoShift;
|
||||
this._lastPlaceholderInsertion = aBefore;
|
||||
},
|
||||
|
||||
isWide: function(aNode) {
|
||||
|
@ -196,6 +208,11 @@ AreaPositionManager.prototype = {
|
|||
child.removeAttribute("notransition");
|
||||
}
|
||||
}
|
||||
// We snapped back, so we can assume there's no more
|
||||
// "last" placeholder insertion point to keep track of.
|
||||
if (aNoTransition) {
|
||||
this._lastPlaceholderInsertion = null;
|
||||
}
|
||||
},
|
||||
|
||||
_getNextPos: function(aNode, aShiftDown, aSize) {
|
||||
|
|
|
@ -42,6 +42,8 @@ skip-if = true
|
|||
skip-if = os == "mac"
|
||||
|
||||
[browser_946320_tabs_from_other_computers.js]
|
||||
skip-if = os == "linux"
|
||||
|
||||
[browser_934951_zoom_in_toolbar.js]
|
||||
[browser_938980_navbar_collapsed.js]
|
||||
[browser_938995_indefaultstate_nonremovable.js]
|
||||
|
|
|
@ -18,6 +18,8 @@ Cu.import("resource://gre/modules/Timer.jsm", this);
|
|||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
|
||||
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FormData",
|
||||
"resource:///modules/sessionstore/FormData.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
|
||||
"resource:///modules/sessionstore/PageStyle.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
|
||||
|
@ -26,8 +28,6 @@ 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");
|
||||
|
||||
Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
|
||||
let gFrameTree = new FrameTree(this);
|
||||
|
@ -74,12 +74,9 @@ function isSessionStorageEvent(event) {
|
|||
*/
|
||||
let EventListener = {
|
||||
|
||||
DOM_EVENTS: [
|
||||
"load", "pageshow", "change", "input"
|
||||
],
|
||||
|
||||
init: function () {
|
||||
this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
|
||||
addEventListener("load", this, true);
|
||||
addEventListener("pageshow", this, true);
|
||||
},
|
||||
|
||||
handleEvent: function (event) {
|
||||
|
@ -106,10 +103,6 @@ let EventListener = {
|
|||
if (event.persisted && event.target == content.document)
|
||||
sendAsyncMessage("SessionStore:pageshow");
|
||||
break;
|
||||
case "input":
|
||||
case "change":
|
||||
sendAsyncMessage("SessionStore:input");
|
||||
break;
|
||||
default:
|
||||
debug("received unknown event '" + event.type + "'");
|
||||
break;
|
||||
|
@ -139,14 +132,6 @@ let MessageListener = {
|
|||
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;
|
||||
case "SessionStore:restoreHistory":
|
||||
|
@ -212,15 +197,7 @@ let SyncHandler = {
|
|||
},
|
||||
|
||||
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;
|
||||
return SessionHistory.collect(docShell);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -307,6 +284,51 @@ let ScrollPositionListener = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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"}}
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
let FormDataListener = {
|
||||
init: function () {
|
||||
addEventListener("input", this, true);
|
||||
addEventListener("change", this, true);
|
||||
gFrameTree.addObserver(this);
|
||||
},
|
||||
|
||||
handleEvent: function (event) {
|
||||
let frame = event.target &&
|
||||
event.target.ownerDocument &&
|
||||
event.target.ownerDocument.defaultView;
|
||||
|
||||
// Don't collect form data for frames created at or after the load event
|
||||
// as SessionStore can't restore form data for those.
|
||||
if (frame && 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
|
||||
|
@ -626,6 +648,7 @@ let MessageQueue = {
|
|||
|
||||
EventListener.init();
|
||||
MessageListener.init();
|
||||
FormDataListener.init();
|
||||
SyncHandler.init();
|
||||
ProgressListener.init();
|
||||
PageStyleListener.init();
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
/**
|
||||
* nsISessionStore keeps track of the current browsing state - i.e.
|
||||
* tab history, cookies, scroll state, form data, POSTDATA and window features
|
||||
* tab history, cookies, scroll state, form data, and window features
|
||||
* - and allows to restore everything into one window.
|
||||
*/
|
||||
|
||||
[scriptable, uuid(6c79d4c1-f071-4c5c-a7fb-676adb144584)]
|
||||
[scriptable, uuid(934697e4-3807-47f8-b6c9-6caa8d83ccd1)]
|
||||
interface nsISessionStartup: nsISupports
|
||||
{
|
||||
/**
|
||||
|
@ -62,4 +62,5 @@ interface nsISessionStartup: nsISupports
|
|||
const unsigned long DEFER_SESSION = 3;
|
||||
|
||||
readonly attribute unsigned long sessionType;
|
||||
readonly attribute bool previousSessionCrashed;
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ interface nsIDOMNode;
|
|||
|
||||
/**
|
||||
* nsISessionStore keeps track of the current browsing state - i.e.
|
||||
* tab history, cookies, scroll state, form data, POSTDATA and window features
|
||||
* 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
|
||||
|
|
|
@ -13,6 +13,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
|||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
|
||||
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FormData",
|
||||
"resource:///modules/sessionstore/FormData.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
|
||||
"resource:///modules/sessionstore/PageStyle.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
|
||||
|
@ -21,8 +23,6 @@ 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");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
|
||||
"resource:///modules/sessionstore/Utils.jsm");
|
||||
|
||||
|
@ -91,7 +91,7 @@ function ContentRestoreInternal(chromeGlobal) {
|
|||
// restoreTabContent.
|
||||
this._tabData = null;
|
||||
|
||||
// Contains {entry, pageStyle, scrollPositions}, where entry is a
|
||||
// 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;
|
||||
|
@ -207,6 +207,7 @@ ContentRestoreInternal.prototype = {
|
|||
// 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 || {}};
|
||||
|
||||
|
@ -277,7 +278,7 @@ ContentRestoreInternal.prototype = {
|
|||
if (!this._restoringDocument) {
|
||||
return;
|
||||
}
|
||||
let {entry, pageStyle, scrollPositions} = this._restoringDocument;
|
||||
let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument;
|
||||
this._restoringDocument = null;
|
||||
|
||||
let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
|
||||
|
@ -290,8 +291,24 @@ ContentRestoreInternal.prototype = {
|
|||
PageStyle.restoreTree(this.docShell, pageStyle);
|
||||
}
|
||||
|
||||
FormData.restoreTree(window, formdata);
|
||||
ScrollPosition.restoreTree(window, scrollPositions);
|
||||
TextAndScrollData.restore(frameList);
|
||||
|
||||
// We need to support the old form and scroll data for a while at least.
|
||||
for (let [frame, data] of frameList) {
|
||||
if (data.hasOwnProperty("formdata") || data.hasOwnProperty("innerHTML")) {
|
||||
let formdata = data.formdata || {};
|
||||
formdata.url = data.url;
|
||||
|
||||
if (data.hasOwnProperty("innerHTML")) {
|
||||
formdata.innerHTML = data.innerHTML;
|
||||
}
|
||||
|
||||
FormData.restore(frame, formdata);
|
||||
}
|
||||
|
||||
ScrollPosition.restore(frame, data.scroll || "");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,233 +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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "DocumentUtils" ];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm");
|
||||
|
||||
this.DocumentUtils = {
|
||||
/**
|
||||
* Obtain form data for a DOMDocument instance.
|
||||
*
|
||||
* The returned object has 2 keys, "id" and "xpath". Each key holds an object
|
||||
* which further defines form data.
|
||||
*
|
||||
* The "id" object maps element IDs to values. The "xpath" object maps the
|
||||
* XPath of an element to its value.
|
||||
*
|
||||
* @param aDocument
|
||||
* DOMDocument instance to obtain form data for.
|
||||
* @return object
|
||||
* Form data encoded in an object.
|
||||
*/
|
||||
getFormData: function DocumentUtils_getFormData(aDocument) {
|
||||
let formNodes = aDocument.evaluate(
|
||||
XPathGenerator.restorableFormNodes,
|
||||
aDocument,
|
||||
XPathGenerator.resolveNS,
|
||||
Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
|
||||
);
|
||||
|
||||
let node;
|
||||
let ret = {id: {}, xpath: {}};
|
||||
|
||||
// Limit the number of XPath expressions for performance reasons. See
|
||||
// bug 477564.
|
||||
const MAX_TRAVERSED_XPATHS = 100;
|
||||
let generatedCount = 0;
|
||||
|
||||
while (node = formNodes.iterateNext()) {
|
||||
let nId = node.id;
|
||||
let hasDefaultValue = true;
|
||||
let value;
|
||||
|
||||
// Only generate a limited number of XPath expressions for perf reasons
|
||||
// (cf. bug 477564)
|
||||
if (!nId && generatedCount > MAX_TRAVERSED_XPATHS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node instanceof Ci.nsIDOMHTMLInputElement ||
|
||||
node instanceof Ci.nsIDOMHTMLTextAreaElement ||
|
||||
node instanceof Ci.nsIDOMXULTextBoxElement) {
|
||||
switch (node.type) {
|
||||
case "checkbox":
|
||||
case "radio":
|
||||
value = node.checked;
|
||||
hasDefaultValue = value == node.defaultChecked;
|
||||
break;
|
||||
case "file":
|
||||
value = { type: "file", fileList: node.mozGetFileNameArray() };
|
||||
hasDefaultValue = !value.fileList.length;
|
||||
break;
|
||||
default: // text, textarea
|
||||
value = node.value;
|
||||
hasDefaultValue = value == node.defaultValue;
|
||||
break;
|
||||
}
|
||||
} else if (!node.multiple) {
|
||||
// <select>s without the multiple attribute are hard to determine the
|
||||
// default value, so assume we don't have the default.
|
||||
hasDefaultValue = false;
|
||||
value = { selectedIndex: node.selectedIndex, value: node.value };
|
||||
} else {
|
||||
// <select>s with the multiple attribute are easier to determine the
|
||||
// default value since each <option> has a defaultSelected
|
||||
let options = Array.map(node.options, function(aOpt, aIx) {
|
||||
let oSelected = aOpt.selected;
|
||||
hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected);
|
||||
return oSelected ? aOpt.value : -1;
|
||||
});
|
||||
value = options.filter(function(aIx) aIx !== -1);
|
||||
}
|
||||
|
||||
// In order to reduce XPath generation (which is slow), we only save data
|
||||
// for form fields that have been changed. (cf. bug 537289)
|
||||
if (!hasDefaultValue) {
|
||||
if (nId) {
|
||||
ret.id[nId] = value;
|
||||
} else {
|
||||
generatedCount++;
|
||||
ret.xpath[XPathGenerator.generate(node)] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges form data on a document from previously obtained data.
|
||||
*
|
||||
* This is the inverse of getFormData(). The data argument is the same object
|
||||
* type which is returned by getFormData(): an object containing the keys
|
||||
* "id" and "xpath" which are each objects mapping element identifiers to
|
||||
* form values.
|
||||
*
|
||||
* Where the document has existing form data for an element, the value
|
||||
* will be replaced. Where the document has a form element but no matching
|
||||
* data in the passed object, the element is untouched.
|
||||
*
|
||||
* @param aDocument
|
||||
* DOMDocument instance to which to restore form data.
|
||||
* @param aData
|
||||
* Object defining form data.
|
||||
*/
|
||||
mergeFormData: function DocumentUtils_mergeFormData(aDocument, aData) {
|
||||
if ("xpath" in aData) {
|
||||
for each (let [xpath, value] in Iterator(aData.xpath)) {
|
||||
let node = XPathGenerator.resolve(aDocument, xpath);
|
||||
|
||||
if (node) {
|
||||
this.restoreFormValue(node, value, aDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("id" in aData) {
|
||||
for each (let [id, value] in Iterator(aData.id)) {
|
||||
let node = aDocument.getElementById(id);
|
||||
|
||||
if (node) {
|
||||
this.restoreFormValue(node, value, aDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Low-level function to restore a form value to a DOMNode.
|
||||
*
|
||||
* If you want a higher-level interface, see mergeFormData().
|
||||
*
|
||||
* When the value is changed, the function will fire the appropriate DOM
|
||||
* events.
|
||||
*
|
||||
* @param aNode
|
||||
* DOMNode to set form value on.
|
||||
* @param aValue
|
||||
* Value to set form element to.
|
||||
* @param aDocument [optional]
|
||||
* DOMDocument node belongs to. If not defined, node.ownerDocument
|
||||
* is used.
|
||||
*/
|
||||
restoreFormValue: function DocumentUtils_restoreFormValue(aNode, aValue, aDocument) {
|
||||
aDocument = aDocument || aNode.ownerDocument;
|
||||
|
||||
let eventType;
|
||||
|
||||
if (typeof aValue == "string" && aNode.type != "file") {
|
||||
// Don't dispatch an input event if there is no change.
|
||||
if (aNode.value == aValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
aNode.value = aValue;
|
||||
eventType = "input";
|
||||
} else if (typeof aValue == "boolean") {
|
||||
// Don't dispatch a change event for no change.
|
||||
if (aNode.checked == aValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
aNode.checked = aValue;
|
||||
eventType = "change";
|
||||
} else if (typeof aValue == "number") {
|
||||
// handle select backwards compatibility, example { "#id" : index }
|
||||
// We saved the value blindly since selects take more work to determine
|
||||
// default values. So now we should check to avoid unnecessary events.
|
||||
if (aNode.selectedIndex == aValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aValue < aNode.options.length) {
|
||||
aNode.selectedIndex = aValue;
|
||||
eventType = "change";
|
||||
}
|
||||
} else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
|
||||
// handle select new format
|
||||
|
||||
// Don't dispatch a change event for no change
|
||||
if (aNode.options[aNode.selectedIndex].value == aValue.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find first option with matching aValue if possible
|
||||
for (let i = 0; i < aNode.options.length; i++) {
|
||||
if (aNode.options[i].value == aValue.value) {
|
||||
aNode.selectedIndex = i;
|
||||
eventType = "change";
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (aValue && aValue.fileList && aValue.type == "file" &&
|
||||
aNode.type == "file") {
|
||||
aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
|
||||
eventType = "input";
|
||||
} else if (aValue && typeof aValue.indexOf == "function" && aNode.options) {
|
||||
Array.forEach(aNode.options, function(opt, index) {
|
||||
// don't worry about malformed options with same values
|
||||
opt.selected = aValue.indexOf(opt.value) > -1;
|
||||
|
||||
// Only fire the event here if this wasn't selected by default
|
||||
if (!opt.defaultSelected) {
|
||||
eventType = "change";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fire events for this node if applicable
|
||||
if (eventType) {
|
||||
let event = aDocument.createEvent("UIEvents");
|
||||
event.initUIEvent(eventType, true, true, aDocument.defaultView, 0);
|
||||
aNode.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,364 @@
|
|||
/* 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 = ["FormData"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm");
|
||||
|
||||
/**
|
||||
* Returns whether the given URL very likely has input
|
||||
* fields that contain serialized session store data.
|
||||
*/
|
||||
function isRestorationPage(url) {
|
||||
return url == "about:sessionrestore" || url == "about:welcomeback";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given form |data| object contains nested restoration
|
||||
* data for a page like about:sessionrestore or about:welcomeback.
|
||||
*/
|
||||
function hasRestorationData(data) {
|
||||
if (isRestorationPage(data.url) && data.id) {
|
||||
return typeof(data.id.sessionData) == "object";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given document's current URI and strips
|
||||
* off the URI's anchor part, if any.
|
||||
*/
|
||||
function getDocumentURI(doc) {
|
||||
return doc.documentURI.replace(/#.*$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* The public API exported by this module that allows to collect
|
||||
* and restore form data for a document and its subframes.
|
||||
*/
|
||||
this.FormData = Object.freeze({
|
||||
collect: function (frame) {
|
||||
return FormDataInternal.collect(frame);
|
||||
},
|
||||
|
||||
restore: function (frame, data) {
|
||||
FormDataInternal.restore(frame, data);
|
||||
},
|
||||
|
||||
restoreTree: function (root, data) {
|
||||
FormDataInternal.restoreTree(root, data);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This module's internal API.
|
||||
*/
|
||||
let FormDataInternal = {
|
||||
/**
|
||||
* Collect form data for a given |frame| *not* including any subframes.
|
||||
*
|
||||
* The returned object may have an "id", "xpath", or "innerHTML" key or a
|
||||
* combination of those three. Form data stored under "id" is for input
|
||||
* fields with id attributes. Data stored under "xpath" is used for input
|
||||
* fields that don't have a unique id and need to be queried using XPath.
|
||||
* The "innerHTML" key is used for editable documents (designMode=on).
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* id: {input1: "value1", input3: "value3"},
|
||||
* xpath: {
|
||||
* "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value2",
|
||||
* "/xhtml:html/xhtml:body/xhtml:input[@name='input4']" : "value4"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param doc
|
||||
* DOMDocument instance to obtain form data for.
|
||||
* @return object
|
||||
* Form data encoded in an object.
|
||||
*/
|
||||
collect: function ({document: doc}) {
|
||||
let formNodes = doc.evaluate(
|
||||
XPathGenerator.restorableFormNodes,
|
||||
doc,
|
||||
XPathGenerator.resolveNS,
|
||||
Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
|
||||
);
|
||||
|
||||
let node;
|
||||
let ret = {};
|
||||
|
||||
// Limit the number of XPath expressions for performance reasons. See
|
||||
// bug 477564.
|
||||
const MAX_TRAVERSED_XPATHS = 100;
|
||||
let generatedCount = 0;
|
||||
|
||||
while (node = formNodes.iterateNext()) {
|
||||
let hasDefaultValue = true;
|
||||
let value;
|
||||
|
||||
// Only generate a limited number of XPath expressions for perf reasons
|
||||
// (cf. bug 477564)
|
||||
if (!node.id && generatedCount > MAX_TRAVERSED_XPATHS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node instanceof Ci.nsIDOMHTMLInputElement ||
|
||||
node instanceof Ci.nsIDOMHTMLTextAreaElement ||
|
||||
node instanceof Ci.nsIDOMXULTextBoxElement) {
|
||||
switch (node.type) {
|
||||
case "checkbox":
|
||||
case "radio":
|
||||
value = node.checked;
|
||||
hasDefaultValue = value == node.defaultChecked;
|
||||
break;
|
||||
case "file":
|
||||
value = { type: "file", fileList: node.mozGetFileNameArray() };
|
||||
hasDefaultValue = !value.fileList.length;
|
||||
break;
|
||||
default: // text, textarea
|
||||
value = node.value;
|
||||
hasDefaultValue = value == node.defaultValue;
|
||||
break;
|
||||
}
|
||||
} else if (!node.multiple) {
|
||||
// <select>s without the multiple attribute are hard to determine the
|
||||
// default value, so assume we don't have the default.
|
||||
hasDefaultValue = false;
|
||||
value = { selectedIndex: node.selectedIndex, value: node.value };
|
||||
} else {
|
||||
// <select>s with the multiple attribute are easier to determine the
|
||||
// default value since each <option> has a defaultSelected property
|
||||
let options = Array.map(node.options, opt => {
|
||||
hasDefaultValue = hasDefaultValue && (opt.selected == opt.defaultSelected);
|
||||
return opt.selected ? opt.value : -1;
|
||||
});
|
||||
value = options.filter(ix => ix > -1);
|
||||
}
|
||||
|
||||
// In order to reduce XPath generation (which is slow), we only save data
|
||||
// for form fields that have been changed. (cf. bug 537289)
|
||||
if (hasDefaultValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.id) {
|
||||
ret.id = ret.id || {};
|
||||
ret.id[node.id] = value;
|
||||
} else {
|
||||
generatedCount++;
|
||||
ret.xpath = ret.xpath || {};
|
||||
ret.xpath[XPathGenerator.generate(node)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// designMode is undefined e.g. for XUL documents (as about:config)
|
||||
if ((doc.designMode || "") == "on" && doc.body) {
|
||||
ret.innerHTML = doc.body.innerHTML;
|
||||
}
|
||||
|
||||
// Return |null| if no form data has been found.
|
||||
if (Object.keys(ret).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store the frame's current URL with its form data so that we can compare
|
||||
// it when restoring data to not inject form data into the wrong document.
|
||||
ret.url = getDocumentURI(doc);
|
||||
|
||||
// We want to avoid saving data for about:sessionrestore as a string.
|
||||
// Since it's stored in the form as stringified JSON, stringifying further
|
||||
// causes an explosion of escape characters. cf. bug 467409
|
||||
if (isRestorationPage(ret.url)) {
|
||||
ret.id.sessionData = JSON.parse(ret.id.sessionData);
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores form |data| for the given frame. The data is expected to be in
|
||||
* the same format that FormData.collect() returns.
|
||||
*
|
||||
* @param frame (DOMWindow)
|
||||
* The frame to restore form data to.
|
||||
* @param data (object)
|
||||
* An object holding form data.
|
||||
*/
|
||||
restore: function ({document: doc}, data) {
|
||||
// Don't restore any data for the given frame if the URL
|
||||
// stored in the form data doesn't match its current URL.
|
||||
if (!data.url || data.url != getDocumentURI(doc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For about:{sessionrestore,welcomeback} we saved the field as JSON to
|
||||
// avoid nested instances causing humongous sessionstore.js files.
|
||||
// cf. bug 467409
|
||||
if (hasRestorationData(data)) {
|
||||
data.id.sessionData = JSON.stringify(data.id.sessionData);
|
||||
}
|
||||
|
||||
if ("id" in data) {
|
||||
let retrieveNode = id => doc.getElementById(id);
|
||||
this.restoreManyInputValues(data.id, retrieveNode);
|
||||
}
|
||||
|
||||
if ("xpath" in data) {
|
||||
let retrieveNode = xpath => XPathGenerator.resolve(doc, xpath);
|
||||
this.restoreManyInputValues(data.xpath, retrieveNode);
|
||||
}
|
||||
|
||||
if ("innerHTML" in data) {
|
||||
// We know that the URL matches data.url right now, but the user
|
||||
// may navigate away before the setTimeout handler runs. We do
|
||||
// a simple comparison against savedURL to check for that.
|
||||
let savedURL = doc.documentURI;
|
||||
|
||||
setTimeout(() => {
|
||||
if (doc.body && doc.designMode == "on" && doc.documentURI == savedURL) {
|
||||
doc.body.innerHTML = data.innerHTML;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterates the given form data, retrieving nodes for all the keys and
|
||||
* restores their appropriate values.
|
||||
*
|
||||
* @param data (object)
|
||||
* A subset of the form data as collected by FormData.collect(). This
|
||||
* is either data stored under "id" or under "xpath".
|
||||
* @param retrieve (function)
|
||||
* The function used to retrieve the input field belonging to a key
|
||||
* in the given |data| object.
|
||||
*/
|
||||
restoreManyInputValues: function (data, retrieve) {
|
||||
for (let key of Object.keys(data)) {
|
||||
let input = retrieve(key);
|
||||
if (input) {
|
||||
this.restoreSingleInputValue(input, data[key]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores a given form value to a given DOMNode and takes care of firing
|
||||
* the appropriate DOM event should the input's value change.
|
||||
*
|
||||
* @param aNode
|
||||
* DOMNode to set form value on.
|
||||
* @param aValue
|
||||
* Value to set form element to.
|
||||
*/
|
||||
restoreSingleInputValue: function (aNode, aValue) {
|
||||
let eventType;
|
||||
|
||||
if (typeof aValue == "string" && aNode.type != "file") {
|
||||
// Don't dispatch an input event if there is no change.
|
||||
if (aNode.value == aValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
aNode.value = aValue;
|
||||
eventType = "input";
|
||||
} else if (typeof aValue == "boolean") {
|
||||
// Don't dispatch a change event for no change.
|
||||
if (aNode.checked == aValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
aNode.checked = aValue;
|
||||
eventType = "change";
|
||||
} else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
|
||||
// Don't dispatch a change event for no change
|
||||
if (aNode.options[aNode.selectedIndex].value == aValue.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find first option with matching aValue if possible
|
||||
for (let i = 0; i < aNode.options.length; i++) {
|
||||
if (aNode.options[i].value == aValue.value) {
|
||||
aNode.selectedIndex = i;
|
||||
eventType = "change";
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (aValue && aValue.fileList && aValue.type == "file" &&
|
||||
aNode.type == "file") {
|
||||
aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
|
||||
eventType = "input";
|
||||
} else if (Array.isArray(aValue) && aNode.options) {
|
||||
Array.forEach(aNode.options, function(opt, index) {
|
||||
// don't worry about malformed options with same values
|
||||
opt.selected = aValue.indexOf(opt.value) > -1;
|
||||
|
||||
// Only fire the event here if this wasn't selected by default
|
||||
if (!opt.defaultSelected) {
|
||||
eventType = "change";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fire events for this node if applicable
|
||||
if (eventType) {
|
||||
let doc = aNode.ownerDocument;
|
||||
let event = doc.createEvent("UIEvents");
|
||||
event.initUIEvent(eventType, true, true, doc.defaultView, 0);
|
||||
aNode.dispatchEvent(event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores form data for the current frame hierarchy starting at |root|
|
||||
* using the given form |data|.
|
||||
*
|
||||
* If the given |root| frame's hierarchy doesn't match that of the given
|
||||
* |data| object we will silently discard data for unreachable frames. For
|
||||
* security reasons we will never restore form data to the wrong frames as
|
||||
* we bail out silently if the stored URL doesn't match the frame's current
|
||||
* URL.
|
||||
*
|
||||
* @param root (DOMWindow)
|
||||
* @param data (object)
|
||||
* {
|
||||
* formdata: {id: {input1: "value1"}},
|
||||
* children: [
|
||||
* {formdata: {id: {input2: "value2"}}},
|
||||
* null,
|
||||
* {formdata: {xpath: { ... }}, children: [ ... ]}
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
restoreTree: function (root, data) {
|
||||
// Don't restore any data for the root frame and its subframes if there
|
||||
// is a URL stored in the form data and it doesn't match its current URL.
|
||||
if (data.url && data.url != getDocumentURI(root.document)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.url) {
|
||||
this.restore(root, data);
|
||||
}
|
||||
|
||||
if (!data.hasOwnProperty("children")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let frames = root.frames;
|
||||
for (let index of Object.keys(data.children)) {
|
||||
if (index < frames.length) {
|
||||
this.restoreTree(frames[index], data.children[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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) || {};
|
||||
}
|
||||
};
|
|
@ -15,7 +15,7 @@ const PREF_DEFERRED = "browser.sessionstore.privacy_level_deferred";
|
|||
|
||||
// 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, cookies, and POSTDATA.
|
||||
// data, and cookies.
|
||||
//
|
||||
// Collect data from all sites (http and https).
|
||||
const PRIVACY_NONE = 0;
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/* 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 = ["PrivacyLevelFilter"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
|
||||
/**
|
||||
* Returns whether the current privacy level allows saving data for the given
|
||||
* |url|.
|
||||
*
|
||||
* @param url The URL we want to save data for.
|
||||
* @param isPinned Whether the given |url| is contained in a pinned tab.
|
||||
* @return bool
|
||||
*/
|
||||
function checkPrivacyLevel(url, isPinned) {
|
||||
let isHttps = url.startsWith("https:");
|
||||
return PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.PrivacyLevelFilter = 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.
|
||||
* @param isPinned Whether the tab we collected from is pinned.
|
||||
* @return object
|
||||
*/
|
||||
filterSessionStorageData: function (data, isPinned) {
|
||||
let retval = {};
|
||||
|
||||
for (let host of Object.keys(data)) {
|
||||
if (checkPrivacyLevel(host, isPinned)) {
|
||||
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.
|
||||
* @param isPinned Whether the tab we collected from is pinned.
|
||||
* @return object
|
||||
*/
|
||||
filterFormData: function (data, isPinned) {
|
||||
// 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 && !checkPrivacyLevel(data.url, isPinned)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let retval = {};
|
||||
|
||||
for (let key of Object.keys(data)) {
|
||||
if (key === "children") {
|
||||
let recurse = child => this.filterFormData(child, isPinned);
|
||||
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;
|
||||
}
|
||||
});
|
|
@ -74,13 +74,6 @@ this.SessionFile = {
|
|||
gatherTelemetry: function(aData) {
|
||||
return SessionFileInternal.gatherTelemetry(aData);
|
||||
},
|
||||
/**
|
||||
* Writes the initial state to disk again only to change the session's load
|
||||
* state. This must only be called once, it will throw an error otherwise.
|
||||
*/
|
||||
writeLoadStateOnceAfterStartup: function (aLoadState) {
|
||||
SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
|
||||
},
|
||||
/**
|
||||
* Create a backup copy, asynchronously.
|
||||
* This is designed to perform backup on upgrade.
|
||||
|
@ -181,13 +174,6 @@ let SessionFileInternal = {
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
writeLoadStateOnceAfterStartup: function (aLoadState) {
|
||||
SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]).then(msg => {
|
||||
this._recordTelemetry(msg.telemetry);
|
||||
return msg;
|
||||
}, console.error);
|
||||
},
|
||||
|
||||
createBackupCopy: function (ext) {
|
||||
return SessionWorker.post("createBackupCopy", [ext]);
|
||||
},
|
||||
|
|
|
@ -13,8 +13,6 @@ const Ci = Components.interfaces;
|
|||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
|
||||
"resource:///modules/sessionstore/Utils.jsm");
|
||||
|
||||
|
@ -22,24 +20,12 @@ function debug(msg) {
|
|||
Services.console.logStringMessage("SessionHistory: " + msg);
|
||||
}
|
||||
|
||||
// The preference value that determines how much post data to save.
|
||||
XPCOMUtils.defineLazyGetter(this, "gPostData", function () {
|
||||
const PREF = "browser.sessionstore.postdata";
|
||||
|
||||
// Observer that updates the cached value when the preference changes.
|
||||
Services.prefs.addObserver(PREF, () => {
|
||||
this.gPostData = Services.prefs.getIntPref(PREF);
|
||||
}, false);
|
||||
|
||||
return Services.prefs.getIntPref(PREF);
|
||||
});
|
||||
|
||||
/**
|
||||
* The external API exported by this module.
|
||||
*/
|
||||
this.SessionHistory = Object.freeze({
|
||||
collect: function (docShell, includePrivateData) {
|
||||
return SessionHistoryInternal.collect(docShell, includePrivateData);
|
||||
collect: function (docShell) {
|
||||
return SessionHistoryInternal.collect(docShell);
|
||||
},
|
||||
|
||||
restore: function (docShell, tabData) {
|
||||
|
@ -56,10 +42,8 @@ let SessionHistoryInternal = {
|
|||
*
|
||||
* @param docShell
|
||||
* The docShell that owns the session history.
|
||||
* @param includePrivateData (optional)
|
||||
* True to always include private data and skip any privacy checks.
|
||||
*/
|
||||
collect: function (docShell, includePrivateData = false) {
|
||||
collect: function (docShell) {
|
||||
let data = {entries: []};
|
||||
let isPinned = docShell.isAppTab;
|
||||
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
|
@ -69,7 +53,7 @@ let SessionHistoryInternal = {
|
|||
try {
|
||||
for (let i = 0; i < history.count; i++) {
|
||||
let shEntry = history.getEntryAtIndex(i, false);
|
||||
let entry = this.serializeEntry(shEntry, includePrivateData, isPinned);
|
||||
let entry = this.serializeEntry(shEntry, isPinned);
|
||||
data.entries.push(entry);
|
||||
}
|
||||
} catch (ex) {
|
||||
|
@ -109,13 +93,11 @@ let SessionHistoryInternal = {
|
|||
*
|
||||
* @param shEntry
|
||||
* nsISHEntry instance
|
||||
* @param includePrivateData
|
||||
* Always return privacy sensitive data (use with care).
|
||||
* @param isPinned
|
||||
* The tab is pinned and should be treated differently for privacy.
|
||||
* @return object
|
||||
*/
|
||||
serializeEntry: function (shEntry, includePrivateData, isPinned) {
|
||||
serializeEntry: function (shEntry, isPinned) {
|
||||
let entry = { url: shEntry.URI.spec };
|
||||
|
||||
// Save some bytes and don't include the title property
|
||||
|
@ -156,17 +138,6 @@ let SessionHistoryInternal = {
|
|||
if (x.value != 0 || y.value != 0)
|
||||
entry.scroll = x.value + "," + y.value;
|
||||
|
||||
// Collect post data for the current history entry.
|
||||
try {
|
||||
let postdata = this.serializePostData(shEntry, isPinned);
|
||||
if (postdata) {
|
||||
entry.postdata_b64 = postdata;
|
||||
}
|
||||
} catch (ex) {
|
||||
// POSTDATA is tricky - especially since some extensions don't get it right
|
||||
debug("Failed serializing post data: " + ex);
|
||||
}
|
||||
|
||||
// Collect owner data for the current history entry.
|
||||
try {
|
||||
let owner = this.serializeOwner(shEntry);
|
||||
|
@ -203,7 +174,7 @@ let SessionHistoryInternal = {
|
|||
break;
|
||||
}
|
||||
|
||||
children.push(this.serializeEntry(child, includePrivateData, isPinned));
|
||||
children.push(this.serializeEntry(child, isPinned));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,40 +186,6 @@ let SessionHistoryInternal = {
|
|||
return entry;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize post data contained in the given session history entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* The session history entry.
|
||||
* @param isPinned
|
||||
* Whether the docShell is owned by a pinned tab.
|
||||
* @return The base64 encoded post data.
|
||||
*/
|
||||
serializePostData: function (shEntry, isPinned) {
|
||||
let isHttps = shEntry.URI.schemeIs("https");
|
||||
if (!shEntry.postData || !gPostData ||
|
||||
!PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shEntry.postData.QueryInterface(Ci.nsISeekableStream)
|
||||
.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
||||
let stream = Cc["@mozilla.org/binaryinputstream;1"]
|
||||
.createInstance(Ci.nsIBinaryInputStream);
|
||||
stream.setInputStream(shEntry.postData);
|
||||
let postBytes = stream.readByteArray(stream.available());
|
||||
let postdata = String.fromCharCode.apply(null, postBytes);
|
||||
if (gPostData != -1 &&
|
||||
postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length > gPostData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We can stop doing base64 encoding once our serialization into JSON
|
||||
// is guaranteed to handle all chars in strings, including embedded
|
||||
// nulls.
|
||||
return btoa(postdata);
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize owner data contained in the given session history entry.
|
||||
*
|
||||
|
@ -375,14 +312,6 @@ let SessionHistoryInternal = {
|
|||
shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
|
||||
}
|
||||
|
||||
if (entry.postdata_b64) {
|
||||
var postdata = atob(entry.postdata_b64);
|
||||
var stream = Cc["@mozilla.org/io/string-input-stream;1"].
|
||||
createInstance(Ci.nsIStringInputStream);
|
||||
stream.setData(postdata, postdata.length);
|
||||
shEntry.postData = stream;
|
||||
}
|
||||
|
||||
let childDocIdents = {};
|
||||
if (entry.docIdentifier) {
|
||||
// If we have a serialized document identifier, try to find an SHEntry
|
||||
|
|
|
@ -15,9 +15,6 @@ const STATE_STOPPED = 0;
|
|||
const STATE_RUNNING = 1;
|
||||
const STATE_QUITTING = -1;
|
||||
|
||||
const STATE_STOPPED_STR = "stopped";
|
||||
const STATE_RUNNING_STR = "running";
|
||||
|
||||
const TAB_STATE_NEEDS_RESTORE = 1;
|
||||
const TAB_STATE_RESTORING = 2;
|
||||
|
||||
|
@ -52,11 +49,6 @@ const WINDOW_HIDEABLE_FEATURES = [
|
|||
];
|
||||
|
||||
const MESSAGES = [
|
||||
// The content script tells us that its form data (or that of one of its
|
||||
// subframes) might have changed. This can be the contents or values of
|
||||
// standard form fields or of ContentEditables.
|
||||
"SessionStore:input",
|
||||
|
||||
// The content script has received a pageshow event. This happens when a
|
||||
// page is loaded from bfcache without any network activity, i.e. when
|
||||
// clicking the back or forward button.
|
||||
|
@ -129,6 +121,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
|
|||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "console",
|
||||
"resource://gre/modules/devtools/Console.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "GlobalState",
|
||||
"resource:///modules/sessionstore/GlobalState.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Messenger",
|
||||
"resource:///modules/sessionstore/Messenger.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
|
||||
|
@ -321,6 +315,8 @@ let SessionStoreInternal = {
|
|||
// set default load state
|
||||
_loadState: STATE_STOPPED,
|
||||
|
||||
_globalState: new GlobalState(),
|
||||
|
||||
// During the initial restore and setBrowserState calls tracks the number of
|
||||
// windows yet to be restored
|
||||
_restoreCount: -1,
|
||||
|
@ -409,7 +405,10 @@ let SessionStoreInternal = {
|
|||
this._initialized = true;
|
||||
},
|
||||
|
||||
initSession: function ssi_initSession() {
|
||||
/**
|
||||
* Initialize the session using the state provided by SessionStartup
|
||||
*/
|
||||
initSession: function () {
|
||||
let state;
|
||||
let ss = gSessionStartup;
|
||||
|
||||
|
@ -442,10 +441,7 @@ let SessionStoreInternal = {
|
|||
// restore it
|
||||
LastSession.setState(state.lastSessionState);
|
||||
|
||||
let lastSessionCrashed =
|
||||
state.session && state.session.state &&
|
||||
state.session.state == STATE_RUNNING_STR;
|
||||
if (lastSessionCrashed) {
|
||||
if (ss.previousSessionCrashed) {
|
||||
this._recentCrashes = (state.session &&
|
||||
state.session.recentCrashes || 0) + 1;
|
||||
|
||||
|
@ -614,9 +610,6 @@ let SessionStoreInternal = {
|
|||
case "SessionStore:pageshow":
|
||||
this.onTabLoad(win, browser);
|
||||
break;
|
||||
case "SessionStore:input":
|
||||
this.onTabInput(win, browser);
|
||||
break;
|
||||
case "SessionStore:loadStart":
|
||||
TabStateCache.delete(browser);
|
||||
break;
|
||||
|
@ -832,16 +825,11 @@ let SessionStoreInternal = {
|
|||
|
||||
// global data must be restored before restoreWindow is called so that
|
||||
// it happens before observers are notified
|
||||
GlobalState.setFromState(aInitialState);
|
||||
this._globalState.setFromState(aInitialState);
|
||||
|
||||
let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
|
||||
let options = {firstWindow: true, overwriteTabs: overwrite};
|
||||
this.restoreWindow(aWindow, aInitialState, options);
|
||||
|
||||
// _loadState changed from "stopped" to "running". Save the session's
|
||||
// load state immediately so that crashes happening during startup
|
||||
// are correctly counted.
|
||||
SessionFile.writeLoadStateOnceAfterStartup(STATE_RUNNING_STR);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -866,7 +854,7 @@ let SessionStoreInternal = {
|
|||
|
||||
// global data must be restored before restoreWindow is called so that
|
||||
// it happens before observers are notified
|
||||
GlobalState.setFromState(this._deferredInitialState);
|
||||
this._globalState.setFromState(this._deferredInitialState);
|
||||
|
||||
this._restoreCount = this._deferredInitialState.windows ?
|
||||
this._deferredInitialState.windows.length : 0;
|
||||
|
@ -1444,18 +1432,6 @@ let SessionStoreInternal = {
|
|||
this._updateCrashReportURL(aWindow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a browser sends the "input" notification
|
||||
* @param aWindow
|
||||
* Window reference
|
||||
* @param aBrowser
|
||||
* Browser reference
|
||||
*/
|
||||
onTabInput: function ssi_onTabInput(aWindow, aBrowser) {
|
||||
TabStateCache.delete(aBrowser);
|
||||
this.saveStateDelayed(aWindow);
|
||||
},
|
||||
|
||||
/**
|
||||
* When a tab is selected, save session data
|
||||
* @param aWindow
|
||||
|
@ -1578,7 +1554,7 @@ let SessionStoreInternal = {
|
|||
|
||||
// global data must be restored before restoreWindow is called so that
|
||||
// it happens before observers are notified
|
||||
GlobalState.setFromState(state);
|
||||
this._globalState.setFromState(state);
|
||||
|
||||
// restore to the given state
|
||||
this.restoreWindow(window, state, {overwriteTabs: true});
|
||||
|
@ -1883,16 +1859,16 @@ let SessionStoreInternal = {
|
|||
},
|
||||
|
||||
getGlobalValue: function ssi_getGlobalValue(aKey) {
|
||||
return GlobalState.get(aKey);
|
||||
return this._globalState.get(aKey);
|
||||
},
|
||||
|
||||
setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
|
||||
GlobalState.set(aKey, aStringValue);
|
||||
this._globalState.set(aKey, aStringValue);
|
||||
this.saveStateDelayed();
|
||||
},
|
||||
|
||||
deleteGlobalValue: function ssi_deleteGlobalValue(aKey) {
|
||||
GlobalState.delete(aKey);
|
||||
this._globalState.delete(aKey);
|
||||
this.saveStateDelayed();
|
||||
},
|
||||
|
||||
|
@ -1945,7 +1921,7 @@ let SessionStoreInternal = {
|
|||
|
||||
// global data must be restored before restoreWindow is called so that
|
||||
// it happens before observers are notified
|
||||
GlobalState.setFromState(lastSessionState);
|
||||
this._globalState.setFromState(lastSessionState);
|
||||
|
||||
// Restore into windows or open new ones as needed.
|
||||
for (let i = 0; i < lastSessionState.windows.length; i++) {
|
||||
|
@ -2250,7 +2226,6 @@ let SessionStoreInternal = {
|
|||
ix = -1;
|
||||
|
||||
let session = {
|
||||
state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR,
|
||||
lastUpdate: Date.now(),
|
||||
startTime: this._sessionStartTime,
|
||||
recentCrashes: this._recentCrashes
|
||||
|
@ -2265,7 +2240,7 @@ let SessionStoreInternal = {
|
|||
_closedWindows: lastClosedWindowsCopy,
|
||||
session: session,
|
||||
scratchpads: scratchpads,
|
||||
global: GlobalState.state
|
||||
global: this._globalState.getState()
|
||||
};
|
||||
|
||||
// Persist the last session if we deferred restoring it
|
||||
|
@ -2739,6 +2714,7 @@ let SessionStoreInternal = {
|
|||
TabStateCache.updatePersistent(browser, {
|
||||
scroll: tabData.scroll || null,
|
||||
storage: tabData.storage || null,
|
||||
formdata: tabData.formdata || null,
|
||||
disallow: tabData.disallow || null,
|
||||
pageStyle: tabData.pageStyle || null
|
||||
});
|
||||
|
@ -3854,62 +3830,3 @@ let LastSession = {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Module that contains global session data.
|
||||
*/
|
||||
let GlobalState = {
|
||||
|
||||
// Storage for global state.
|
||||
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) || {};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -54,13 +54,6 @@ self.onmessage = function (msg) {
|
|||
};
|
||||
|
||||
let Agent = {
|
||||
// The initial session string as read from disk.
|
||||
initialState: null,
|
||||
|
||||
// Boolean that tells whether we already wrote
|
||||
// the loadState to disk once after startup.
|
||||
hasWrittenLoadStateOnce: false,
|
||||
|
||||
// Boolean that tells whether we already made a
|
||||
// call to write(). We will only attempt to move
|
||||
// sessionstore.js to sessionstore.bak on the
|
||||
|
@ -83,10 +76,9 @@ let Agent = {
|
|||
let durationMs = Date.now();
|
||||
let bytes = File.read(path);
|
||||
durationMs = Date.now() - durationMs;
|
||||
this.initialState = Decoder.decode(bytes);
|
||||
|
||||
return {
|
||||
result: this.initialState,
|
||||
result: Decoder.decode(bytes),
|
||||
telemetry: {FX_SESSION_RESTORE_READ_FILE_MS: durationMs,
|
||||
FX_SESSION_RESTORE_FILE_SIZE_BYTES: bytes.byteLength}
|
||||
};
|
||||
|
@ -140,37 +132,6 @@ let Agent = {
|
|||
return Statistics.collect(stateString);
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes the session state to disk again but changes session.state to
|
||||
* 'running' before doing so. This is intended to be called only once, shortly
|
||||
* after startup so that we detect crashes on startup correctly.
|
||||
*/
|
||||
writeLoadStateOnceAfterStartup: function (loadState) {
|
||||
if (this.hasWrittenLoadStateOnce) {
|
||||
throw new Error("writeLoadStateOnceAfterStartup() must only be called once.");
|
||||
}
|
||||
|
||||
if (!this.initialState) {
|
||||
throw new Error("writeLoadStateOnceAfterStartup() must not be called " +
|
||||
"without a valid session state or before it has been " +
|
||||
"read from disk.");
|
||||
}
|
||||
|
||||
// Make sure we can't call this function twice.
|
||||
this.hasWrittenLoadStateOnce = true;
|
||||
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(this.initialState);
|
||||
} finally {
|
||||
this.initialState = null;
|
||||
}
|
||||
|
||||
state.session = state.session || {};
|
||||
state.session.state = loadState;
|
||||
return this._write(JSON.stringify(state));
|
||||
},
|
||||
|
||||
/**
|
||||
* Write a stateString to disk
|
||||
*/
|
||||
|
@ -352,8 +313,6 @@ let Statistics = {
|
|||
subsets.DOM_STORAGE = [];
|
||||
// The subset of sessionstore.js storing form data
|
||||
subsets.FORMDATA = [];
|
||||
// The subset of sessionstore.js storing POST data in history
|
||||
subsets.POSTDATA = [];
|
||||
// The subset of sessionstore.js storing history
|
||||
subsets.HISTORY = [];
|
||||
|
||||
|
@ -372,9 +331,6 @@ let Statistics = {
|
|||
subsets.FORMDATA.push(value);
|
||||
// Never visit formdata, it's full of weird stuff
|
||||
return false;
|
||||
case "postdata_b64":
|
||||
subsets.POSTDATA.push(value);
|
||||
return false; // Nothing to visit anyway
|
||||
case "cookies": // Don't visit these places, they are full of weird stuff
|
||||
case "extData":
|
||||
return false;
|
||||
|
|
|
@ -16,8 +16,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
|
|||
"resource://gre/modules/devtools/Console.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Messenger",
|
||||
"resource:///modules/sessionstore/Messenger.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevelFilter",
|
||||
"resource:///modules/sessionstore/PrivacyLevelFilter.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
|
||||
"resource:///modules/sessionstore/TabStateCache.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
|
||||
|
@ -166,8 +166,7 @@ let TabStateInternal = {
|
|||
let browser = tab.linkedBrowser;
|
||||
|
||||
let promise = Task.spawn(function task() {
|
||||
// Collect session history data asynchronously. Also collects
|
||||
// text and scroll data.
|
||||
// Collect session history data asynchronously.
|
||||
let history = yield Messenger.send(tab, "SessionStore:collectSessionHistory");
|
||||
|
||||
// The tab could have been closed while waiting for a response.
|
||||
|
@ -354,33 +353,27 @@ let TabStateInternal = {
|
|||
*/
|
||||
_copyFromPersistentCache: function (tab, tabData, options = {}) {
|
||||
let data = TabStateCache.getPersistent(tab.linkedBrowser);
|
||||
|
||||
// Nothing to do without any cached data.
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The caller may explicitly request to omit privacy checks.
|
||||
let includePrivateData = options && options.includePrivateData;
|
||||
|
||||
for (let key of Object.keys(data)) {
|
||||
if (key != "storage" || includePrivateData) {
|
||||
tabData[key] = data[key];
|
||||
} else {
|
||||
let storage = {};
|
||||
let isPinned = tab.pinned;
|
||||
let value = data[key];
|
||||
|
||||
// If we're not allowed to include private data, let's filter out hosts
|
||||
// based on the given tab's pinned state and the privacy level.
|
||||
for (let host of Object.keys(data.storage)) {
|
||||
let isHttps = host.startsWith("https:");
|
||||
if (PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
|
||||
storage[host] = data.storage[host];
|
||||
}
|
||||
// Filter sensitive data according to the current privacy level.
|
||||
if (!includePrivateData) {
|
||||
if (key === "storage") {
|
||||
value = PrivacyLevelFilter.filterSessionStorageData(value, tab.pinned);
|
||||
} else if (key === "formdata") {
|
||||
value = PrivacyLevelFilter.filterFormData(value, tab.pinned);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(storage).length) {
|
||||
tabData.storage = storage;
|
||||
}
|
||||
if (value) {
|
||||
tabData[key] = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,145 +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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["TextAndScrollData"];
|
||||
|
||||
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, "DocumentUtils",
|
||||
"resource:///modules/sessionstore/DocumentUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
|
||||
"resource:///modules/sessionstore/ScrollPosition.jsm");
|
||||
|
||||
/**
|
||||
* The external API exported by this module.
|
||||
*/
|
||||
this.TextAndScrollData = Object.freeze({
|
||||
updateFrame: function (entry, content, isPinned, options) {
|
||||
return TextAndScrollDataInternal.updateFrame(entry, content, isPinned, options);
|
||||
},
|
||||
|
||||
restore: function (frameList) {
|
||||
TextAndScrollDataInternal.restore(frameList);
|
||||
},
|
||||
});
|
||||
|
||||
let TextAndScrollDataInternal = {
|
||||
/**
|
||||
* Go through all subframes and store all form data, the current
|
||||
* scroll positions and innerHTML content of WYSIWYG editors.
|
||||
*
|
||||
* @param entry
|
||||
* the object into which to store the collected data
|
||||
* @param content
|
||||
* frame reference
|
||||
* @param isPinned
|
||||
* the tab is pinned and should be treated differently for privacy
|
||||
* @param includePrivateData
|
||||
* {includePrivateData:true} include privacy sensitive data (use with care)
|
||||
*/
|
||||
updateFrame: function (entry, content, isPinned, options = null) {
|
||||
let includePrivateData = options && options.includePrivateData;
|
||||
|
||||
for (let i = 0; i < content.frames.length; i++) {
|
||||
if (entry.children && entry.children[i]) {
|
||||
this.updateFrame(entry.children[i], content.frames[i], includePrivateData, isPinned);
|
||||
}
|
||||
}
|
||||
|
||||
let href = (content.parent || content).document.location.href;
|
||||
let isHttps = Services.io.newURI(href, null, null).schemeIs("https");
|
||||
let topURL = content.top.document.location.href;
|
||||
let isAboutSR = this.isAboutSessionRestore(topURL);
|
||||
if (includePrivateData || isAboutSR ||
|
||||
PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
|
||||
let formData = DocumentUtils.getFormData(content.document);
|
||||
|
||||
// We want to avoid saving data for about:sessionrestore as a string.
|
||||
// Since it's stored in the form as stringified JSON, stringifying further
|
||||
// causes an explosion of escape characters. cf. bug 467409
|
||||
if (formData && isAboutSR) {
|
||||
formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
|
||||
}
|
||||
|
||||
if (Object.keys(formData.id).length ||
|
||||
Object.keys(formData.xpath).length) {
|
||||
entry.formdata = formData;
|
||||
}
|
||||
|
||||
// designMode is undefined e.g. for XUL documents (as about:config)
|
||||
if ((content.document.designMode || "") == "on" && content.document.body) {
|
||||
entry.innerHTML = content.document.body.innerHTML;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isAboutSessionRestore: function (url) {
|
||||
return url == "about:sessionrestore" || url == "about:welcomeback";
|
||||
},
|
||||
|
||||
restore: function (frameList) {
|
||||
for (let [frame, data] of frameList) {
|
||||
this.restoreFrame(frame, data);
|
||||
}
|
||||
},
|
||||
|
||||
restoreFrame: function (content, data) {
|
||||
if (data.formdata) {
|
||||
let formdata = data.formdata;
|
||||
|
||||
// handle backwards compatibility
|
||||
// this is a migration from pre-firefox 15. cf. bug 742051
|
||||
if (!("xpath" in formdata || "id" in formdata)) {
|
||||
formdata = { xpath: {}, id: {} };
|
||||
|
||||
for each (let [key, value] in Iterator(data.formdata)) {
|
||||
if (key.charAt(0) == "#") {
|
||||
formdata.id[key.slice(1)] = value;
|
||||
} else {
|
||||
formdata.xpath[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for about:sessionrestore we saved the field as JSON to avoid
|
||||
// nested instances causing humongous sessionstore.js files.
|
||||
// cf. bug 467409
|
||||
if (this.isAboutSessionRestore(data.url) &&
|
||||
"sessionData" in formdata.id &&
|
||||
typeof formdata.id["sessionData"] == "object") {
|
||||
formdata.id["sessionData"] = JSON.stringify(formdata.id["sessionData"]);
|
||||
}
|
||||
|
||||
// update the formdata
|
||||
data.formdata = formdata;
|
||||
// merge the formdata
|
||||
DocumentUtils.mergeFormData(content.document, formdata);
|
||||
}
|
||||
|
||||
if (data.innerHTML) {
|
||||
// We know that the URL matches data.url right now, but the user
|
||||
// may navigate away before the setTimeout handler runs. We do
|
||||
// a simple comparison against savedURL to check for that.
|
||||
let savedURL = content.document.location.href;
|
||||
|
||||
setTimeout(function() {
|
||||
if (content.document.designMode == "on" &&
|
||||
content.document.location.href == savedURL &&
|
||||
content.document.body) {
|
||||
content.document.body.innerHTML = data.innerHTML;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
ScrollPosition.restore(content, data.scroll || "");
|
||||
},
|
||||
};
|
|
@ -15,11 +15,13 @@ JS_MODULES_PATH = 'modules/sessionstore'
|
|||
EXTRA_JS_MODULES = [
|
||||
'ContentRestore.jsm',
|
||||
'DocShellCapabilities.jsm',
|
||||
'DocumentUtils.jsm',
|
||||
'FormData.jsm',
|
||||
'FrameTree.jsm',
|
||||
'GlobalState.jsm',
|
||||
'Messenger.jsm',
|
||||
'PageStyle.jsm',
|
||||
'PrivacyLevel.jsm',
|
||||
'PrivacyLevelFilter.jsm',
|
||||
'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
|
||||
'ScrollPosition.jsm',
|
||||
'SessionCookies.jsm',
|
||||
|
@ -31,7 +33,6 @@ EXTRA_JS_MODULES = [
|
|||
'TabAttributes.jsm',
|
||||
'TabState.jsm',
|
||||
'TabStateCache.jsm',
|
||||
'TextAndScrollData.jsm',
|
||||
'Utils.jsm',
|
||||
'XPathGenerator.jsm',
|
||||
]
|
||||
|
|
|
@ -14,12 +14,10 @@
|
|||
* mode is active, however, the session is never restored.
|
||||
*
|
||||
* Crash Detection
|
||||
* The session file stores a session.state property, that
|
||||
* indicates whether the browser is currently running. When the browser shuts
|
||||
* down, the field is changed to "stopped". At startup, this field is read, and
|
||||
* if its value is "running", then it's assumed that the browser had previously
|
||||
* crashed, or at the very least that something bad happened, and that we should
|
||||
* restore the session.
|
||||
* 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
|
||||
|
@ -47,6 +45,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
|
|||
"resource://gre/modules/devtools/Console.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
|
||||
"resource:///modules/sessionstore/SessionFile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
|
||||
"resource://gre/modules/CrashMonitor.jsm");
|
||||
|
||||
const STATE_RUNNING_STR = "running";
|
||||
|
||||
|
@ -72,6 +72,9 @@ SessionStartup.prototype = {
|
|||
_sessionType: Ci.nsISessionStartup.NO_SESSION,
|
||||
_initialized: false,
|
||||
|
||||
// Stores whether the previous session crashed.
|
||||
_previousSessionCrashed: null,
|
||||
|
||||
/* ........ Global Event Handlers .............. */
|
||||
|
||||
/**
|
||||
|
@ -99,71 +102,78 @@ SessionStartup.prototype = {
|
|||
return string;
|
||||
},
|
||||
|
||||
_onSessionFileRead: function sss_onSessionFileRead(aStateString) {
|
||||
if (this._initialized) {
|
||||
// Initialization is complete, nothing else to do
|
||||
/**
|
||||
* Complete initialization once the Session File has been read
|
||||
*
|
||||
* @param stateString
|
||||
* string The Session State string read from disk
|
||||
*/
|
||||
_onSessionFileRead: function (stateString) {
|
||||
this._initialized = true;
|
||||
|
||||
// Let observers modify the state before it is used
|
||||
let supportsStateString = this._createSupportsString(stateString);
|
||||
Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
|
||||
stateString = supportsStateString.data;
|
||||
|
||||
// No valid session found.
|
||||
if (!stateString) {
|
||||
this._sessionType = Ci.nsISessionStartup.NO_SESSION;
|
||||
Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
|
||||
gOnceInitializedDeferred.resolve();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._initialized = true;
|
||||
|
||||
// Let observers modify the state before it is used
|
||||
let supportsStateString = this._createSupportsString(aStateString);
|
||||
Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
|
||||
aStateString = supportsStateString.data;
|
||||
this._initialState = this._parseStateString(stateString);
|
||||
|
||||
// No valid session found.
|
||||
if (!aStateString) {
|
||||
this._sessionType = Ci.nsISessionStartup.NO_SESSION;
|
||||
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)
|
||||
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.
|
||||
//
|
||||
// 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 &&
|
||||
this._initialState.session &&
|
||||
this._initialState.session.state);
|
||||
|
||||
|
||||
this._previousSessionCrashed = !stateFlagPresent ||
|
||||
(this._initialState.session.state == STATE_RUNNING_STR);
|
||||
}
|
||||
|
||||
// parse the session state into a JS object
|
||||
// remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0)
|
||||
if (aStateString.charAt(0) == '(')
|
||||
aStateString = aStateString.slice(1, -1);
|
||||
let corruptFile = false;
|
||||
try {
|
||||
this._initialState = JSON.parse(aStateString);
|
||||
}
|
||||
catch (ex) {
|
||||
debug("The session file contained un-parse-able JSON: " + ex);
|
||||
// This is not valid JSON, but this might still be valid JavaScript,
|
||||
// as used in FF2/FF3, so we need to eval.
|
||||
// evalInSandbox will throw if aStateString is not parse-able.
|
||||
try {
|
||||
var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'});
|
||||
this._initialState = Cu.evalInSandbox("(" + aStateString + ")", s);
|
||||
} catch(ex) {
|
||||
debug("The session file contained un-eval-able JSON: " + ex);
|
||||
corruptFile = true;
|
||||
}
|
||||
}
|
||||
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
|
||||
|
||||
let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
|
||||
let doResumeSession = doResumeSessionOnce ||
|
||||
Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
|
||||
|
||||
// If this is a normal restore then throw away any previous session
|
||||
if (!doResumeSessionOnce)
|
||||
delete this._initialState.lastSessionState;
|
||||
|
||||
let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
|
||||
let lastSessionCrashed =
|
||||
this._initialState && this._initialState.session &&
|
||||
this._initialState.session.state &&
|
||||
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(!lastSessionCrashed);
|
||||
Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed);
|
||||
|
||||
// set the startup type
|
||||
if (lastSessionCrashed && resumeFromCrash)
|
||||
if (this._previousSessionCrashed && resumeFromCrash)
|
||||
this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
|
||||
else if (!lastSessionCrashed && doResumeSession)
|
||||
else if (!this._previousSessionCrashed && shouldResumeSession)
|
||||
this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
|
||||
else if (this._initialState)
|
||||
this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
|
||||
|
@ -175,11 +185,33 @@ SessionStartup.prototype = {
|
|||
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
|
||||
Services.obs.addObserver(this, "browser:purge-session-history", true);
|
||||
|
||||
} finally {
|
||||
// We're ready. Notify everyone else.
|
||||
Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
|
||||
gOnceInitializedDeferred.resolve();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Convert the Session State string into a state object
|
||||
*
|
||||
* @param stateString
|
||||
* string The Session State string read from disk
|
||||
* @returns {State} a Session State object
|
||||
*/
|
||||
_parseStateString: function (stateString) {
|
||||
let state = null;
|
||||
let corruptFile = false;
|
||||
|
||||
try {
|
||||
state = JSON.parse(stateString);
|
||||
} catch (ex) {
|
||||
debug("The session file contained un-parse-able JSON: " + ex);
|
||||
corruptFile = true;
|
||||
}
|
||||
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -292,6 +324,14 @@ SessionStartup.prototype = {
|
|||
return this._sessionType;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get whether the previous session crashed.
|
||||
*/
|
||||
get previousSessionCrashed() {
|
||||
this._ensureInitialized();
|
||||
return this._previousSessionCrashed;
|
||||
},
|
||||
|
||||
// Ensure that initialization is complete. If initialization is not complete
|
||||
// yet, something is attempting to use the old synchronous initialization,
|
||||
// throw an error.
|
||||
|
|
|
@ -11,11 +11,13 @@
|
|||
support-files =
|
||||
head.js
|
||||
content.js
|
||||
content-forms.js
|
||||
browser_formdata_sample.html
|
||||
browser_formdata_xpath_sample.html
|
||||
browser_frametree_sample.html
|
||||
browser_frametree_sample_frameset.html
|
||||
browser_form_restore_events_sample.html
|
||||
browser_formdata_format_sample.html
|
||||
browser_input_sample.html
|
||||
browser_pageStyle_sample.html
|
||||
browser_pageStyle_sample_nested.html
|
||||
browser_scrollPositions_sample.html
|
||||
|
@ -23,12 +25,10 @@ support-files =
|
|||
browser_sessionStorage.html
|
||||
browser_248970_b_sample.html
|
||||
browser_339445_sample.html
|
||||
browser_346337_sample.html
|
||||
browser_423132_sample.html
|
||||
browser_447951_sample.html
|
||||
browser_454908_sample.html
|
||||
browser_456342_sample.xhtml
|
||||
browser_463205_helper.html
|
||||
browser_463205_sample.html
|
||||
browser_463206_sample.html
|
||||
browser_466937_sample.html
|
||||
|
@ -41,7 +41,6 @@ support-files =
|
|||
browser_597315_c2.html
|
||||
browser_662743_sample.html
|
||||
browser_739531_sample.html
|
||||
browser_916390_sample.html
|
||||
|
||||
#NB: the following are disabled
|
||||
# browser_464620_a.html
|
||||
|
@ -58,10 +57,11 @@ support-files =
|
|||
[browser_capabilities.js]
|
||||
[browser_dying_cache.js]
|
||||
[browser_form_restore_events.js]
|
||||
[browser_formdata.js]
|
||||
[browser_formdata_format.js]
|
||||
[browser_formdata_xpath.js]
|
||||
[browser_frametree.js]
|
||||
[browser_global_store.js]
|
||||
[browser_input.js]
|
||||
[browser_merge_closed_tabs.js]
|
||||
[browser_pageshow.js]
|
||||
[browser_pageStyle.js]
|
||||
|
@ -172,7 +172,6 @@ skip-if = true
|
|||
[browser_819510_perwindowpb.js]
|
||||
skip-if = os == "linux" # Intermittent failures, bug 894063
|
||||
[browser_833286_atomic_backup.js]
|
||||
[browser_916390_form_data_loss.js]
|
||||
|
||||
# Disabled for frequent intermittent failures
|
||||
[browser_464620_a.js]
|
||||
|
|
|
@ -1,123 +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/. */
|
||||
|
||||
function test() {
|
||||
/** Test for Bug 346337 **/
|
||||
|
||||
var file = Components.classes["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Components.interfaces.nsIProperties)
|
||||
.get("TmpD", Components.interfaces.nsILocalFile);
|
||||
file.append("346337_test1.file");
|
||||
file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
|
||||
var filePath1 = file.path;
|
||||
file = Components.classes["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Components.interfaces.nsIProperties)
|
||||
.get("TmpD", Components.interfaces.nsILocalFile);
|
||||
file.append("346337_test2.file");
|
||||
file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
|
||||
var filePath2 = 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'][1]": [filePath1],
|
||||
"//input[@type='file'][2]": [filePath1, filePath2]
|
||||
};
|
||||
|
||||
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 if (node instanceof Ci.nsIDOMHTMLInputElement && node.type == "file")
|
||||
node.mozSetFileNameArray(aValue, aValue.length);
|
||||
else
|
||||
Array.forEach(node.options, function(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) {
|
||||
if (node.type == "file") {
|
||||
let fileNames = node.mozGetFileNameArray();
|
||||
return fileNames.length == aValue.length &&
|
||||
Array.every(fileNames, function(aFile) aValue.indexOf(aFile) >= 0);
|
||||
}
|
||||
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, function(aOpt, aIx)
|
||||
(aValue.indexOf(aIx) > -1) == aOpt.selected);
|
||||
}
|
||||
|
||||
// test setup
|
||||
let tabbrowser = gBrowser;
|
||||
waitForExplicitFinish();
|
||||
|
||||
// make sure we don't save form data at all (except for tab duplication)
|
||||
gPrefService.setIntPref("browser.sessionstore.privacy_level", 2);
|
||||
|
||||
let rootDir = getRootDirectory(gTestPath);
|
||||
let testURL = rootDir + "browser_346337_sample.html";
|
||||
let tab = tabbrowser.addTab(testURL);
|
||||
whenBrowserLoaded(tab.linkedBrowser, function() {
|
||||
for (let xpath in fieldList)
|
||||
setFormValue(tab, xpath, fieldList[xpath]);
|
||||
|
||||
let tab2 = tabbrowser.duplicateTab(tab);
|
||||
whenTabRestored(tab2, function() {
|
||||
for (let xpath in fieldList)
|
||||
ok(compareFormValue(tab2, xpath, fieldList[xpath]),
|
||||
"The value for \"" + xpath + "\" was correctly restored");
|
||||
|
||||
// clean up
|
||||
tabbrowser.removeTab(tab2);
|
||||
tabbrowser.removeTab(tab);
|
||||
|
||||
tab = undoCloseTab();
|
||||
whenTabRestored(tab, function() {
|
||||
for (let xpath in fieldList)
|
||||
if (fieldList[xpath])
|
||||
ok(!compareFormValue(tab, xpath, fieldList[xpath]),
|
||||
"The value for \"" + xpath + "\" was correctly discarded");
|
||||
|
||||
if (gPrefService.prefHasUserValue("browser.sessionstore.privacy_level"))
|
||||
gPrefService.clearUserPref("browser.sessionstore.privacy_level");
|
||||
// undoCloseTab can reuse a single blank tab, so we have to
|
||||
// make sure not to close the window when closing our last tab
|
||||
if (tabbrowser.tabs.length == 1)
|
||||
tabbrowser.addTab();
|
||||
tabbrowser.removeTab(tab);
|
||||
finish();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,72 +1,70 @@
|
|||
function test() {
|
||||
/** Test for Bug 393716 **/
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
waitForExplicitFinish();
|
||||
"use strict";
|
||||
|
||||
/////////////////
|
||||
// getTabState //
|
||||
/////////////////
|
||||
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();
|
||||
let testURL = "about:config";
|
||||
|
||||
// create a new tab
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
let tab = gBrowser.addTab(URL);
|
||||
ss.setTabValue(tab, key, value);
|
||||
whenBrowserLoaded(tab.linkedBrowser, function() {
|
||||
// get the tab's state
|
||||
let state = ss.getTabState(tab);
|
||||
ok(state, "get the tab's state");
|
||||
yield promiseBrowserLoaded(tab.linkedBrowser);
|
||||
|
||||
// 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 == testURL,
|
||||
"Got the expected state object (test URL)");
|
||||
ok(state.extData && state.extData[key] == value,
|
||||
"Got the expected state object (test manually set tab value)");
|
||||
// get the tab's state
|
||||
let state = ss.getTabState(tab);
|
||||
ok(state, "get the tab's state");
|
||||
|
||||
// clean up
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
// 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)");
|
||||
|
||||
//////////////////////////////////
|
||||
// setTabState and duplicateTab //
|
||||
//////////////////////////////////
|
||||
// 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: testURL }], extData: { key2: value2 } };
|
||||
let state = { entries: [{ url: URL }], extData: { key2: value2 } };
|
||||
|
||||
// create a new tab
|
||||
let tab2 = gBrowser.addTab();
|
||||
let tab = gBrowser.addTab();
|
||||
// set the tab's state
|
||||
ss.setTabState(tab2, JSON.stringify(state));
|
||||
whenTabRestored(tab2, function() {
|
||||
// verify the correctness of the restored tab
|
||||
ok(ss.getTabValue(tab2, key2) == value2 && tab2.linkedBrowser.currentURI.spec == testURL,
|
||||
"the tab's state was correctly restored");
|
||||
ss.setTabState(tab, JSON.stringify(state));
|
||||
yield promiseBrowserLoaded(tab.linkedBrowser);
|
||||
|
||||
// add text data
|
||||
let textbox = tab2.linkedBrowser.contentDocument.getElementById("textbox");
|
||||
textbox.value = value3;
|
||||
// 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");
|
||||
|
||||
// duplicate the tab
|
||||
let duplicateTab = ss.duplicateTab(window, tab2);
|
||||
gBrowser.removeTab(tab2);
|
||||
// add text data
|
||||
yield setInputValue(tab.linkedBrowser, {id: "textbox", value: value3});
|
||||
|
||||
whenTabRestored(duplicateTab, function() {
|
||||
// verify the correctness of the duplicated tab
|
||||
ok(ss.getTabValue(duplicateTab, key2) == value2 &&
|
||||
duplicateTab.linkedBrowser.currentURI.spec == testURL,
|
||||
"correctly duplicated the tab's state");
|
||||
let textbox = duplicateTab.linkedBrowser.contentDocument.getElementById("textbox");
|
||||
is(textbox.value, value3, "also duplicated text data");
|
||||
// duplicate the tab
|
||||
let tab2 = ss.duplicateTab(window, tab);
|
||||
yield promiseTabRestored(tab2);
|
||||
|
||||
// clean up
|
||||
gBrowser.removeTab(duplicateTab);
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
// 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);
|
||||
});
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
* 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>";
|
||||
|
||||
|
@ -30,11 +32,8 @@ function test() {
|
|||
let [txt, chk] = newWin.content.document.querySelectorAll("#txt, #chk");
|
||||
txt.value = uniqueText;
|
||||
|
||||
// Toggle the checkbox to cause a SessionStore:input message to be sent.
|
||||
EventUtils.sendMouseEvent({type: "click"}, chk);
|
||||
|
||||
let browser = newWin.gBrowser.selectedBrowser;
|
||||
promiseContentMessage(browser, "SessionStore:input").then(() => {
|
||||
setInputChecked(browser, {id: "chk", checked: true}).then(() => {
|
||||
newWin.close();
|
||||
|
||||
// Now give it time to close
|
||||
|
@ -85,3 +84,7 @@ function test() {
|
|||
});
|
||||
}, TEST_URL);
|
||||
}
|
||||
|
||||
function setInputChecked(browser, data) {
|
||||
return sendMessage(browser, "ss-test:setInputChecked", data);
|
||||
}
|
||||
|
|
|
@ -1,50 +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/. */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
/** Test for Bug 454908 **/
|
||||
"use strict";
|
||||
|
||||
waitForExplicitFinish();
|
||||
let tmp = {};
|
||||
Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", tmp);
|
||||
let {SessionSaver} = tmp;
|
||||
|
||||
let fieldValues = {
|
||||
username: "User " + Math.random(),
|
||||
passwd: "pwd" + Date.now()
|
||||
};
|
||||
const URL = ROOT + "browser_454908_sample.html";
|
||||
const PASS = "pwd-" + Math.random();
|
||||
|
||||
// make sure we do save form data
|
||||
gPrefService.setIntPref("browser.sessionstore.privacy_level", 0);
|
||||
/**
|
||||
* 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");
|
||||
|
||||
let rootDir = getRootDirectory(gTestPath);
|
||||
let testURL = rootDir + "browser_454908_sample.html";
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
whenBrowserLoaded(tab.linkedBrowser, function() {
|
||||
let doc = tab.linkedBrowser.contentDocument;
|
||||
for (let id in fieldValues)
|
||||
doc.getElementById(id).value = fieldValues[id];
|
||||
// Add a tab with a password field.
|
||||
let tab = gBrowser.addTab(URL);
|
||||
let browser = tab.linkedBrowser;
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
// Fill in some values.
|
||||
let usernameValue = "User " + Math.random();
|
||||
yield setInputValue(browser, {id: "username", value: usernameValue});
|
||||
yield setInputValue(browser, {id: "passwd", value: PASS});
|
||||
|
||||
tab = undoCloseTab();
|
||||
whenTabRestored(tab, function() {
|
||||
let doc = tab.linkedBrowser.contentDocument;
|
||||
for (let id in fieldValues) {
|
||||
let node = doc.getElementById(id);
|
||||
if (node.type == "password")
|
||||
is(node.value, "", "password wasn't saved/restored");
|
||||
else
|
||||
is(node.value, fieldValues[id], "username was saved/restored");
|
||||
}
|
||||
// Close and restore the tab.
|
||||
gBrowser.removeTab(tab);
|
||||
tab = ss.undoCloseTab(window, 0);
|
||||
browser = tab.linkedBrowser;
|
||||
yield promiseTabRestored(tab);
|
||||
|
||||
// clean up
|
||||
if (gPrefService.prefHasUserValue("browser.sessionstore.privacy_level"))
|
||||
gPrefService.clearUserPref("browser.sessionstore.privacy_level");
|
||||
// undoCloseTab can reuse a single blank tab, so we have to
|
||||
// make sure not to close the window when closing our last tab
|
||||
if (gBrowser.tabs.length == 1)
|
||||
gBrowser.addTab();
|
||||
gBrowser.removeTab(tab);
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
// 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 SessionSaver.run();
|
||||
let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
|
||||
let data = yield OS.File.read(path);
|
||||
let state = new TextDecoder().decode(data);
|
||||
|
||||
// Ensure that sessionstore.js doesn't contain our password.
|
||||
is(state.indexOf(PASS), -1, "password has not been written to disk");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -1,51 +1,49 @@
|
|||
/* 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/. */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
/** Test for Bug 456342 **/
|
||||
"use strict";
|
||||
|
||||
waitForExplicitFinish();
|
||||
const URL = ROOT + "browser_456342_sample.xhtml";
|
||||
|
||||
// make sure we do save form data
|
||||
gPrefService.setIntPref("browser.sessionstore.privacy_level", 0);
|
||||
/**
|
||||
* 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);
|
||||
|
||||
let rootDir = getRootDirectory(gTestPath);
|
||||
let testURL = rootDir + "browser_456342_sample.xhtml";
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
whenBrowserLoaded(tab.linkedBrowser, function() {
|
||||
let expectedValue = "try to save me";
|
||||
// Since bug 537289 we only save non-default values, so we need to set each
|
||||
// form field's value after load.
|
||||
let formEls = tab.linkedBrowser.contentDocument.forms[0].elements;
|
||||
for (let i = 0; i < formEls.length; i++)
|
||||
formEls[i].value = expectedValue;
|
||||
// Fill in form values.
|
||||
let expectedValue = Math.random();
|
||||
yield setFormElementValues(browser, {value: expectedValue});
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
// Remove tab and check collected form data.
|
||||
gBrowser.removeTab(tab);
|
||||
let undoItems = JSON.parse(ss.getClosedTabData(window));
|
||||
let savedFormData = undoItems[0].state.formdata;
|
||||
|
||||
let undoItems = JSON.parse(ss.getClosedTabData(window));
|
||||
let savedFormData = undoItems[0].state.entries[0].formdata;
|
||||
|
||||
let countGood = 0, countBad = 0;
|
||||
for each (let value in savedFormData.id) {
|
||||
if (value == expectedValue)
|
||||
countGood++;
|
||||
else
|
||||
countBad++;
|
||||
let countGood = 0, countBad = 0;
|
||||
for (let id of Object.keys(savedFormData.id)) {
|
||||
if (savedFormData.id[id] == expectedValue) {
|
||||
countGood++;
|
||||
} else {
|
||||
countBad++;
|
||||
}
|
||||
for each (let value in savedFormData.xpath) {
|
||||
if (value == 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");
|
||||
is(countGood, 4, "Saved text for non-standard input fields");
|
||||
is(countBad, 0, "Didn't save text for ignored field types");
|
||||
});
|
||||
|
||||
// clean up
|
||||
if (gPrefService.prefHasUserValue("browser.sessionstore.privacy_level"))
|
||||
gPrefService.clearUserPref("browser.sessionstore.privacy_level");
|
||||
finish();
|
||||
});
|
||||
function setFormElementValues(browser, data) {
|
||||
return sendMessage(browser, "ss-test:setFormElementValues", data);
|
||||
}
|
||||
|
|
|
@ -1,123 +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/. */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
/** Test for Bug 463205 **/
|
||||
"use strict";
|
||||
|
||||
waitForExplicitFinish();
|
||||
const URL = ROOT + "browser_463205_sample.html";
|
||||
|
||||
let rootDir = "http://mochi.test:8888/browser/browser/components/sessionstore/test/";
|
||||
let testURL = rootDir + "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);
|
||||
|
||||
let doneURL = "done";
|
||||
// Restore form data with a valid URL.
|
||||
ss.setTabState(tab, getState(URL));
|
||||
yield promiseTabRestored(tab);
|
||||
|
||||
let mainURL = testURL;
|
||||
let frame1URL = "data:text/html;charset=utf-8,<input%20id='original'>";
|
||||
let frame2URL = rootDir + "browser_463205_helper.html";
|
||||
let frame3URL = "data:text/html;charset=utf-8,mark2";
|
||||
let value = yield getInputValue(browser, {id: "text"});
|
||||
is(value, "foobar", "value was restored");
|
||||
|
||||
let frameCount = 0;
|
||||
// Restore form data with an invalid URL.
|
||||
ss.setTabState(tab, getState("http://example.com/"));
|
||||
yield promiseTabRestored(tab);
|
||||
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
tab.linkedBrowser.addEventListener("load", function(aEvent) {
|
||||
// wait for all frames to load completely
|
||||
if (frame1URL != doneURL && aEvent.target.location.href == frame1URL) {
|
||||
frame1URL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (frame2URL != doneURL && aEvent.target.location.href == frame2URL) {
|
||||
frame2URL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (frame3URL != doneURL && aEvent.target.location.href == frame3URL) {
|
||||
frame3URL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mainURL != doneURL && aEvent.target.location.href == mainURL) {
|
||||
mainURL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (frameCount < 3) {
|
||||
return;
|
||||
}
|
||||
tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
|
||||
let value = yield getInputValue(browser, {id: "text"});
|
||||
is(value, "", "value was not restored");
|
||||
|
||||
function typeText(aTextField, aValue) {
|
||||
aTextField.value = aValue;
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
let event = aTextField.ownerDocument.createEvent("UIEvents");
|
||||
event.initUIEvent("input", true, true, aTextField.ownerDocument.defaultView, 0);
|
||||
aTextField.dispatchEvent(event);
|
||||
}
|
||||
|
||||
let uniqueValue = "Unique: " + Math.random();
|
||||
let win = tab.linkedBrowser.contentWindow;
|
||||
typeText(win.frames[0].document.getElementById("original"), uniqueValue);
|
||||
typeText(win.frames[1].document.getElementById("original"), uniqueValue);
|
||||
|
||||
mainURL = testURL;
|
||||
frame1URL = "http://mochi.test:8888/browser/" +
|
||||
"browser/components/sessionstore/test/browser_463205_helper.html";
|
||||
frame2URL = rootDir + "browser_463205_helper.html";
|
||||
frame3URL = "data:text/html;charset=utf-8,mark2";
|
||||
|
||||
frameCount = 0;
|
||||
|
||||
let tab2 = gBrowser.duplicateTab(tab);
|
||||
tab2.linkedBrowser.addEventListener("load", function(aEvent) {
|
||||
// wait for all frames to load (and reload!) completely
|
||||
if (frame1URL != doneURL && aEvent.target.location.href == frame1URL) {
|
||||
frame1URL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (frame2URL != doneURL && (aEvent.target.location.href == frame2URL ||
|
||||
aEvent.target.location.href == frame2URL + "#original")) {
|
||||
frame2URL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (frame3URL != doneURL && aEvent.target.location.href == frame3URL) {
|
||||
frame3URL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mainURL != doneURL && aEvent.target.location.href == mainURL) {
|
||||
mainURL = doneURL;
|
||||
if (frameCount++ < 3) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (frameCount < 3) {
|
||||
return;
|
||||
}
|
||||
tab2.linkedBrowser.removeEventListener("load", arguments.callee, true);
|
||||
|
||||
let win = tab2.linkedBrowser.contentWindow;
|
||||
isnot(win.frames[0].document.getElementById("original").value, uniqueValue,
|
||||
"subframes must match URL to get text restored");
|
||||
is(win.frames[0].document.getElementById("original").value, "preserve me",
|
||||
"subframes must match URL to get text restored");
|
||||
is(win.frames[1].document.getElementById("original").value, uniqueValue,
|
||||
"text still gets restored for all other subframes");
|
||||
|
||||
// clean up
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
finish();
|
||||
}, true);
|
||||
}, true);
|
||||
function getState(url) {
|
||||
return JSON.stringify({
|
||||
entries: [{url: URL}],
|
||||
formdata: {url: url, id: {text: "foobar"}}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for bug 463205 (cross domain)</title>
|
||||
|
||||
<input id="original" value="preserve me">
|
|
@ -1,24 +1,7 @@
|
|||
<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for bug 463205</title>
|
||||
<title>bug 463205</title>
|
||||
|
||||
<body onload="onLoad()">
|
||||
<iframe src="data:text/html;charset=utf-8,<input%20id='original'>"></iframe>
|
||||
<iframe src="browser_463205_helper.html"></iframe>
|
||||
<iframe src="data:text/html;charset=utf-8,mark1"></iframe>
|
||||
|
||||
<script type="application/javascript">
|
||||
function onLoad() {
|
||||
if (frames[2].document.location.href == "data:text/html;charset-utf-8,mark1") {
|
||||
frames[2].document.location = "data:text/html;charset=utf-8,mark2";
|
||||
}
|
||||
else {
|
||||
frames[1].document.location.hash = "#original";
|
||||
frames[0].document.location = "http://mochi.test:8888/browser/" +
|
||||
"browser/components/sessionstore/test/browser_463205_helper.html";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<body>
|
||||
<input type="text" id="text" />
|
||||
</body>
|
||||
|
|
|
@ -1,43 +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/. */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
/** Test for Bug 466937 **/
|
||||
"use strict";
|
||||
|
||||
waitForExplicitFinish();
|
||||
const URL = ROOT + "browser_466937_sample.html";
|
||||
|
||||
var file = Components.classes["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Components.interfaces.nsIProperties)
|
||||
.get("TmpD", Components.interfaces.nsILocalFile);
|
||||
/**
|
||||
* 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(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
|
||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
|
||||
let testPath = file.path;
|
||||
|
||||
let testURL = "http://mochi.test:8888/browser/" +
|
||||
"browser/components/sessionstore/test/browser_466937_sample.html";
|
||||
// Fill in form values.
|
||||
yield setInputValue(browser, {id: "reverse_thief", value: "/home/user/secret2"});
|
||||
yield setInputValue(browser, {id: "bystander", value: testPath});
|
||||
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
whenBrowserLoaded(tab.linkedBrowser, function() {
|
||||
let doc = tab.linkedBrowser.contentDocument;
|
||||
doc.getElementById("reverse_thief").value = "/home/user/secret2";
|
||||
doc.getElementById("bystander").value = testPath;
|
||||
// Duplicate and check form values.
|
||||
let tab2 = gBrowser.duplicateTab(tab);
|
||||
let browser2 = tab2.linkedBrowser;
|
||||
yield promiseTabRestored(tab2);
|
||||
|
||||
let tab2 = gBrowser.duplicateTab(tab);
|
||||
whenTabRestored(tab2, function() {
|
||||
doc = tab2.linkedBrowser.contentDocument;
|
||||
is(doc.getElementById("thief").value, "",
|
||||
"file path wasn't set to text field value");
|
||||
is(doc.getElementById("reverse_thief").value, "",
|
||||
"text field value wasn't set to full file path");
|
||||
is(doc.getElementById("bystander").value, testPath,
|
||||
"normal case: file path was correctly preserved");
|
||||
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");
|
||||
|
||||
// clean up
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
gBrowser.removeTab(tab2);
|
||||
});
|
||||
|
|
|
@ -1,91 +1,77 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test Summary:
|
||||
// 1. Open about:sessionrestore via setBrowserState where formdata is a JS object, not a string
|
||||
// 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 (via getBrowserState) doesn't require JSON.parse
|
||||
// 1c. Check that formdata doesn't require JSON.parse
|
||||
//
|
||||
// 2. Use the current state (currently about:sessionrestore with data) and then open than in a new instance of about:sessionrestore
|
||||
// 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 (via getBrowserState) doesn't require JSON.parse
|
||||
// 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 (via getBrowserState) doesn't require JSON.parse
|
||||
// 3c. Check that formdata doesn't require JSON.parse
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
ignoreAllUncaughtExceptions();
|
||||
|
||||
let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
|
||||
let crashState = { windows: [{ tabs: [{ entries: [{ url: "about:mozilla" }] }]}]};
|
||||
|
||||
let pagedata = { url: "about:sessionrestore",
|
||||
formdata: { id: {"sessionData": crashState } } };
|
||||
let state = { windows: [{ tabs: [{ entries: [pagedata] }] }] };
|
||||
|
||||
// test1 calls test2 calls test3 calls finish
|
||||
test1(state);
|
||||
|
||||
|
||||
function test1(aState) {
|
||||
waitForBrowserState(aState, function() {
|
||||
checkState("test1", test2);
|
||||
});
|
||||
}
|
||||
|
||||
function test2(aState) {
|
||||
let pagedata2 = { url: "about:sessionrestore",
|
||||
formdata: { id: { "sessionData": aState } } };
|
||||
let state2 = { windows: [{ tabs: [{ entries: [pagedata2] }] }] };
|
||||
|
||||
waitForBrowserState(state2, function() {
|
||||
checkState("test2", test3);
|
||||
});
|
||||
}
|
||||
|
||||
function test3(aState) {
|
||||
let pagedata3 = { url: "about:sessionrestore",
|
||||
formdata: { id: { "sessionData": JSON.stringify(crashState) } } };
|
||||
let state3 = { windows: [{ tabs: [{ entries: [pagedata3] }] }] };
|
||||
waitForBrowserState(state3, function() {
|
||||
// In theory we should do inspection of the treeview on about:sessionrestore,
|
||||
// but we don't actually need to. If we fail tests in checkState then
|
||||
// about:sessionrestore won't be able to turn the form value into a usable page.
|
||||
checkState("test3", function() waitForBrowserState(blankState, finish));
|
||||
});
|
||||
}
|
||||
|
||||
function checkState(testName, callback) {
|
||||
let curState = JSON.parse(ss.getBrowserState());
|
||||
let formdata = curState.windows[0].tabs[0].entries[0].formdata;
|
||||
|
||||
ok(formdata.id["sessionData"], testName + ": we have form data for about:sessionrestore");
|
||||
|
||||
let sessionData_raw = JSON.stringify(formdata.id["sessionData"]);
|
||||
ok(!/\\/.test(sessionData_raw), testName + ": #sessionData contains no backslashes");
|
||||
info(sessionData_raw);
|
||||
|
||||
let gotError = false;
|
||||
try {
|
||||
JSON.parse(formdata.id["sessionData"]);
|
||||
}
|
||||
catch (e) {
|
||||
info(testName + ": got error: " + e);
|
||||
gotError = true;
|
||||
}
|
||||
ok(gotError, testName + ": attempting to JSON.parse form data threw error");
|
||||
|
||||
// Panorama sticks JSON into extData, which we stringify causing the
|
||||
// naive backslash check to fail. extData doesn't matter in the grand
|
||||
// scheme here, so we'll delete the extData so doesn't end up in future states.
|
||||
delete curState.windows[0].extData;
|
||||
delete curState.windows[0].tabs[0].extData;
|
||||
callback(curState);
|
||||
}
|
||||
const CRASH_STATE = {windows: [{tabs: [{entries: [{url: "about:mozilla" }]}]}]};
|
||||
const STATE = {entries: [createEntry(CRASH_STATE)]};
|
||||
const STATE2 = {entries: [createEntry({windows: [{tabs: [STATE]}]})]};
|
||||
const STATE3 = {entries: [createEntry(JSON.stringify(CRASH_STATE))]};
|
||||
|
||||
function createEntry(sessionData) {
|
||||
return {
|
||||
url: "about:sessionrestore",
|
||||
formdata: {id: {sessionData: sessionData}}
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
ss.setTabState(tab, JSON.stringify(STATE));
|
||||
yield promiseTabRestored(tab);
|
||||
checkState("test1", tab);
|
||||
|
||||
// test 2
|
||||
ss.setTabState(tab, JSON.stringify(STATE2));
|
||||
yield promiseTabRestored(tab);
|
||||
checkState("test2", tab);
|
||||
|
||||
// test 3
|
||||
ss.setTabState(tab, JSON.stringify(STATE3));
|
||||
yield promiseTabRestored(tab);
|
||||
checkState("test3", tab);
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
function checkState(prefix, tab) {
|
||||
// Flush and query tab state.
|
||||
SyncHandlers.get(tab.linkedBrowser).flush();
|
||||
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");
|
||||
}
|
||||
|
|
|
@ -1,34 +1,37 @@
|
|||
/* 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/. */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
/** Test for Bug 485482 **/
|
||||
"use strict";
|
||||
|
||||
waitForExplicitFinish();
|
||||
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});
|
||||
|
||||
let rootDir = getRootDirectory(gTestPath);
|
||||
let testURL = rootDir + "browser_485482_sample.html";
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
whenBrowserLoaded(tab.linkedBrowser, function() {
|
||||
let doc = tab.linkedBrowser.contentDocument;
|
||||
doc.querySelector("input[type=text]").value = uniqueValue;
|
||||
doc.querySelector("input[type=checkbox]").checked = true;
|
||||
// Duplicate the tab.
|
||||
let tab2 = gBrowser.duplicateTab(tab);
|
||||
let browser2 = tab2.linkedBrowser;
|
||||
yield promiseTabRestored(tab2);
|
||||
|
||||
let tab2 = gBrowser.duplicateTab(tab);
|
||||
whenTabRestored(tab2, function() {
|
||||
doc = tab2.linkedBrowser.contentDocument;
|
||||
is(doc.querySelector("input[type=text]").value, uniqueValue,
|
||||
"generated XPath expression was valid");
|
||||
ok(doc.querySelector("input[type=checkbox]").checked,
|
||||
"generated XPath expression was valid");
|
||||
// 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");
|
||||
|
||||
// clean up
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab);
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* 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.
|
||||
|
@ -13,13 +15,6 @@ function test() {
|
|||
let formData = [
|
||||
// default case
|
||||
{ },
|
||||
// old format
|
||||
{ "#select_id" : 0 },
|
||||
{ "#select_id" : 2 },
|
||||
// invalid index
|
||||
{ "#select_id" : 8 },
|
||||
{ "/xhtml:html/xhtml:body/xhtml:select" : 5},
|
||||
{ "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : 6},
|
||||
|
||||
// new format
|
||||
// index doesn't match value (testing an option in between (two))
|
||||
|
@ -36,32 +31,17 @@ function test() {
|
|||
{ 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"} } },
|
||||
|
||||
// combinations
|
||||
{ "#select_id" : 3, id:{ "select_id": {"selectedIndex":1,"value":"val1"} } },
|
||||
{ "#select_id" : 5, xpath: { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : {"selectedIndex":4,"value":"val4"} } },
|
||||
{ "/xhtml:html/xhtml:body/xhtml:select" : 5, id:{ "select_id": {"selectedIndex":1,"value":"val1"} }},
|
||||
{ "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : 2, xpath: { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : {"selectedIndex":7,"value":"val7"} } }
|
||||
];
|
||||
|
||||
let expectedValues = [
|
||||
[ "val3"], // default value
|
||||
[ "val0"],
|
||||
[ "val2"],
|
||||
[ "val3"], // default value (invalid index)
|
||||
[ "val5"],
|
||||
[ "val6"],
|
||||
[ "val2"],
|
||||
[ "val3"], // default value (invalid value)
|
||||
[ "val5"], // value is still valid (even it has an invalid index)
|
||||
[ "val0"],
|
||||
[ "val7"],
|
||||
[ "val3"],
|
||||
[ "val4"],
|
||||
[ "val1"],
|
||||
[ "val4"],
|
||||
[ "val1"],
|
||||
[ "val7"]
|
||||
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--;
|
||||
|
@ -76,7 +56,7 @@ function test() {
|
|||
}
|
||||
}
|
||||
|
||||
function testTabRestoreData(aFormData, aExpectedValues, aCallback) {
|
||||
function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
|
||||
let testURL =
|
||||
getRootDirectory(gTestPath) + "browser_662743_sample.html";
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
|
@ -90,38 +70,40 @@ function testTabRestoreData(aFormData, aExpectedValues, aCallback) {
|
|||
let select = doc.getElementById("select_id");
|
||||
let value = select.options[select.selectedIndex].value;
|
||||
|
||||
// Flush to make sure we have the latest form data.
|
||||
SyncHandlers.get(tab.linkedBrowser).flush();
|
||||
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, aExpectedValues[0],
|
||||
is(value, aExpectedValue,
|
||||
"Select Option by selectedIndex &/or value has been restored correctly");
|
||||
|
||||
let restoredFormData = restoredTabState.formdata;
|
||||
let selectIdFormData = restoredFormData.id.select_id;
|
||||
let 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();
|
||||
});
|
||||
|
||||
tab.addEventListener("TabClose", function(aEvent) {
|
||||
tab.removeEventListener("TabClose", arguments.callee);
|
||||
let restoredTabState = JSON.parse(ss.getTabState(tab));
|
||||
let restoredFormData = restoredTabState.entries[0].formdata;
|
||||
let selectIdFormData = restoredFormData.id.select_id;
|
||||
let value = restoredFormData.id.select_id.value;
|
||||
|
||||
// test format
|
||||
ok("id" in restoredFormData && "xpath" in restoredFormData,
|
||||
"FormData format is valid");
|
||||
// validate that there are no old keys
|
||||
is(Object.keys(restoredFormData).length, 2,
|
||||
"FormData key length is valid");
|
||||
// test format
|
||||
ok("selectedIndex" in selectIdFormData && "value" in selectIdFormData,
|
||||
"select format is valid");
|
||||
// validate that there are no old keys
|
||||
is(Object.keys(selectIdFormData).length, 2,
|
||||
"select_id length is valid");
|
||||
// test set collection values
|
||||
is(value, aExpectedValues[0],
|
||||
"Collection has been saved correctly");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ function test() {
|
|||
ok(currentState.session, "session data returned by getBrowserState");
|
||||
|
||||
let keys = Object.keys(currentState.session);
|
||||
let expectedKeys = ["state", "lastUpdate", "startTime", "recentCrashes"];
|
||||
let expectedKeys = ["lastUpdate", "startTime", "recentCrashes"];
|
||||
ok(compareArray(keys.sort(), expectedKeys.sort()),
|
||||
"session object from getBrowserState has correct keys");
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
let tmp;
|
||||
Cu.import("resource:///modules/sessionstore/TabStateCache.jsm", tmp);
|
||||
let {TabStateCache} = tmp;
|
||||
|
||||
const URL = "http://mochi.test:8888/browser/" +
|
||||
"browser/components/sessionstore/test/browser_916390_sample.html";
|
||||
|
||||
function test() {
|
||||
TestRunner.run();
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
// Create a tab with some form fields.
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
yield waitForLoad(browser);
|
||||
|
||||
// Modify the text input field's state.
|
||||
browser.contentDocument.getElementById("txt").focus();
|
||||
EventUtils.synthesizeKey("m", {});
|
||||
yield waitForInput();
|
||||
|
||||
// Check that we'll save the form data state correctly.
|
||||
let state = JSON.parse(ss.getBrowserState());
|
||||
let {formdata} = state.windows[0].tabs[1].entries[0];
|
||||
is(formdata.id.txt, "m", "txt's value is correct");
|
||||
|
||||
// Change the number of session history entries to invalidate the cache.
|
||||
browser.loadURI(URL + "#");
|
||||
TabStateCache.delete(browser);
|
||||
|
||||
// Check that we'll save the form data state correctly.
|
||||
let state = JSON.parse(ss.getBrowserState());
|
||||
let {formdata} = state.windows[0].tabs[1].entries[1];
|
||||
is(formdata.id.txt, "m", "txt's value is correct");
|
||||
|
||||
// Clean up.
|
||||
gBrowser.removeTab(tab);
|
||||
}
|
||||
|
||||
function waitForLoad(aElement) {
|
||||
aElement.addEventListener("load", function onLoad() {
|
||||
aElement.removeEventListener("load", onLoad, true);
|
||||
executeSoon(next);
|
||||
}, true);
|
||||
}
|
||||
|
||||
function waitForInput() {
|
||||
let mm = gBrowser.selectedBrowser.messageManager;
|
||||
|
||||
mm.addMessageListener("SessionStore:input", function onInput() {
|
||||
mm.removeMessageListener("SessionStore:input", onInput);
|
||||
executeSoon(next);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForStorageChange() {
|
||||
let mm = gBrowser.selectedBrowser.messageManager;
|
||||
|
||||
mm.addMessageListener("SessionStore:MozStorageChanged", function onChanged() {
|
||||
mm.removeMessageListener("SessionStore:MozStorageChanged", onChanged);
|
||||
executeSoon(next);
|
||||
});
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html dir="ltr" xml:lang="en-US" lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>bug 916390</title>
|
||||
</head>
|
||||
<body>
|
||||
<input id="txt" />
|
||||
</body>
|
||||
</html>
|
|
@ -138,12 +138,7 @@ add_task(function flush_on_tabclose_racy() {
|
|||
|
||||
function promiseNewWindow() {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
whenNewWindowLoaded({private: false}, function (win) {
|
||||
win.messageManager.loadFrameScript(FRAME_SCRIPT, true);
|
||||
deferred.resolve(win);
|
||||
});
|
||||
|
||||
whenNewWindowLoaded({private: false}, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,65 +1,63 @@
|
|||
/* 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/. */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function test() {
|
||||
/** Originally a test for Bug 476161, but then expanded to include all input types in bug 640136 **/
|
||||
"use strict";
|
||||
|
||||
waitForExplicitFinish();
|
||||
const URL = ROOT + "browser_form_restore_events_sample.html";
|
||||
|
||||
let file = Components.classes["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Components.interfaces.nsIProperties)
|
||||
.get("TmpD", Components.interfaces.nsIFile);
|
||||
/**
|
||||
* 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);
|
||||
|
||||
let testURL = "http://mochi.test:8888/browser/" +
|
||||
"browser/components/sessionstore/test/browser_form_restore_events_sample.html";
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
whenBrowserLoaded(tab.linkedBrowser, function() {
|
||||
let doc = tab.linkedBrowser.contentDocument;
|
||||
// text fields
|
||||
yield setInputValue(browser, {id: "modify01", value: Math.random()});
|
||||
yield setInputValue(browser, {id: "modify02", value: Date.now()});
|
||||
|
||||
// text fields
|
||||
doc.getElementById("modify01").value += Math.random();
|
||||
doc.getElementById("modify02").value += " " + Date.now();
|
||||
// textareas
|
||||
yield setInputValue(browser, {id: "modify03", value: Math.random()});
|
||||
yield setInputValue(browser, {id: "modify04", value: Date.now()});
|
||||
|
||||
// textareas
|
||||
doc.getElementById("modify03").value += Math.random();
|
||||
doc.getElementById("modify04").value += " " + Date.now();
|
||||
// file
|
||||
let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
||||
yield setInputValue(browser, {id: "modify05", value: file.path});
|
||||
|
||||
// file
|
||||
doc.getElementById("modify05").value = file.path;
|
||||
// select
|
||||
yield setSelectedIndex(browser, {id: "modify06", index: 1});
|
||||
yield setMultipleSelected(browser, {id: "modify07", indices: [0,1,2]});
|
||||
|
||||
// select
|
||||
doc.getElementById("modify06").selectedIndex = 1;
|
||||
var multipleChange = doc.getElementById("modify07");
|
||||
Array.forEach(multipleChange.options, function(option) option.selected = true);
|
||||
// checkbox
|
||||
yield setInputChecked(browser, {id: "modify08", checked: true});
|
||||
yield setInputChecked(browser, {id: "modify09", checked: false});
|
||||
|
||||
// checkbox
|
||||
doc.getElementById("modify08").checked = true;
|
||||
doc.getElementById("modify09").checked = false;
|
||||
// radio
|
||||
yield setInputChecked(browser, {id: "modify10", checked: true});
|
||||
yield setInputChecked(browser, {id: "modify11", checked: true});
|
||||
|
||||
// radio
|
||||
// select one then another in the same group - only last one should get event on restore
|
||||
doc.getElementById("modify10").checked = true;
|
||||
doc.getElementById("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 tab2 = gBrowser.duplicateTab(tab);
|
||||
whenTabRestored(tab2, function() {
|
||||
let doc = tab2.linkedBrowser.contentDocument;
|
||||
let inputFired = doc.getElementById("inputFired").textContent.trim().split();
|
||||
let changeFired = doc.getElementById("changeFired").textContent.trim().split();
|
||||
let changeFired = yield getTextContent(browser2, {id: "changeFired"});
|
||||
changeFired = changeFired.trim().split().sort().join(" ");
|
||||
|
||||
is(inputFired.sort().join(" "), "modify01 modify02 modify03 modify04 modify05",
|
||||
"input events were only dispatched for modified input, textarea fields");
|
||||
is(inputFired, "modify01 modify02 modify03 modify04 modify05",
|
||||
"input events were only dispatched for modified input, textarea fields");
|
||||
|
||||
is(changeFired.sort().join(" "), "modify06 modify07 modify08 modify09 modify11",
|
||||
"change events were only dispatched for modified select, checkbox, radio fields");
|
||||
is(changeFired, "modify06 modify07 modify08 modify09 modify11",
|
||||
"change events were only dispatched for modified select, checkbox, radio fields");
|
||||
|
||||
// clean up
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
gBrowser.removeTab(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();
|
||||
let [{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();
|
||||
let [{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 we maintain backwards compatibility with the form
|
||||
* data format used pre Fx 29.
|
||||
*/
|
||||
add_task(function test_old_format() {
|
||||
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);
|
||||
|
||||
// Check that the form value is restored.
|
||||
let state = {entries: [{url: URL, formdata: {id: {input: VALUE}}}]};
|
||||
ss.setTabState(tab, JSON.stringify(state));
|
||||
yield promiseTabRestored(tab);
|
||||
is((yield getInputValue(browser, "input")), VALUE, "form data restored");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
/**
|
||||
* This test ensures that we maintain backwards compatibility with the form
|
||||
* data form used pre Fx 29, esp. the .innerHTML property for editable docs.
|
||||
*/
|
||||
add_task(function test_old_format_inner_html() {
|
||||
const URL = "data:text/html;charset=utf-8,<h1>mozilla</h1>" +
|
||||
"<script>document.designMode='on'</script>";
|
||||
const VALUE = "<h1>value-" + Math.random() + "</h1>";
|
||||
|
||||
// Create a tab with an iframe containing an input field.
|
||||
let tab = gBrowser.addTab(URL);
|
||||
let browser = tab.linkedBrowser;
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
// Restore the tab state.
|
||||
let state = {entries: [{url: URL, innerHTML: VALUE}]};
|
||||
ss.setTabState(tab, JSON.stringify(state));
|
||||
yield promiseTabRestored(tab);
|
||||
|
||||
// Check that the innerHTML value was restored.
|
||||
let html = yield getInnerHTML(browser);
|
||||
is(html, VALUE, "editable document has been restored correctly");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
ss.setTabState(tab, JSON.stringify(state));
|
||||
return promiseTabRestored(tab).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.
|
||||
gBrowser.removeTab(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.
|
||||
let tab = ss.undoCloseTab(window, 0);
|
||||
let browser = tab.linkedBrowser;
|
||||
yield promiseTabRestored(tab);
|
||||
|
||||
// Check that the input field has the right value.
|
||||
SyncHandlers.get(browser).flush();
|
||||
let {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"});
|
||||
|
||||
// Duplicate the modified tab.
|
||||
let tab2 = gBrowser.duplicateTab(tab);
|
||||
yield promiseTabRestored(tab2);
|
||||
|
||||
// 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");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab2);
|
||||
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"});
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
function test() {
|
||||
/** Tests formdata format **/
|
||||
waitForExplicitFinish();
|
||||
|
@ -66,50 +68,46 @@ function test() {
|
|||
}
|
||||
|
||||
function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
|
||||
let testURL =
|
||||
getRootDirectory(gTestPath) + "browser_formdata_format_sample.html";
|
||||
let tab = gBrowser.addTab(testURL);
|
||||
let tabState = { entries: [{ url: testURL, formdata: aFormData}] };
|
||||
let URL = ROOT + "browser_formdata_format_sample.html";
|
||||
let tab = gBrowser.addTab("about:blank");
|
||||
let browser = tab.linkedBrowser;
|
||||
let tabState = { entries: [{ url: URL, formdata: aFormData}] };
|
||||
|
||||
let browserLoadedCallback = function(aEvent) {
|
||||
let tabStateCallback = function(aEvent) {
|
||||
Task.spawn(function () {
|
||||
yield promiseBrowserLoaded(tab.linkedBrowser);
|
||||
|
||||
ss.setTabState(tab, JSON.stringify(tabState));
|
||||
yield promiseTabRestored(tab);
|
||||
|
||||
SyncHandlers.get(tab.linkedBrowser).flush();
|
||||
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]");
|
||||
let saveStateCallback = function(aEvent) {
|
||||
let restoredTabState = JSON.parse(ss.getTabState(tab));
|
||||
let restoredFormData = restoredTabState.entries[0].formdata;
|
||||
|
||||
if (restoredFormData) {
|
||||
// test format
|
||||
ok("id" in restoredFormData && "xpath" in restoredFormData,
|
||||
"FormData format is valid: " + restoredFormData);
|
||||
// validate that there are no old keys
|
||||
is(Object.keys(restoredFormData).length, 2,
|
||||
"FormData key length is valid");
|
||||
// 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");
|
||||
// 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);
|
||||
aCallback();
|
||||
};
|
||||
// clean up
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
waitForSaveState(saveStateCallback);
|
||||
|
||||
// force a change event to recollect the formdata
|
||||
let changeEvent = document.createEvent("Events");
|
||||
changeEvent.initEvent("change", true, true);
|
||||
input1.dispatchEvent(changeEvent);
|
||||
};
|
||||
|
||||
waitForTabState(tab, tabState, tabStateCallback);
|
||||
};
|
||||
|
||||
whenBrowserLoaded(tab.linkedBrowser, browserLoadedCallback);
|
||||
// This test might time out if the task fails.
|
||||
}).then(aCallback);
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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.
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(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.
|
||||
gBrowser.removeTab(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");
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const ROOT = getRootDirectory(gTestPath);
|
||||
"use strict";
|
||||
|
||||
const URL = ROOT + "browser_frametree_sample.html";
|
||||
const URL_FRAMESET = ROOT + "browser_frametree_sample_frameset.html";
|
||||
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче