This commit is contained in:
Ryan VanderMeulen 2014-01-17 15:30:54 -05:00
Родитель 054e29e2b0 b989384c30
Коммит b75c40bb3e
299 изменённых файлов: 9199 добавлений и 4770 удалений

Просмотреть файл

@ -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({

1
addon-sdk/source/test/fixtures/loader/missing-twice/file.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
an invalid json file

32
addon-sdk/source/test/fixtures/loader/missing-twice/main.js поставляемый Normal 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";

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше