c0aab70e03
And include ChromeUtils
1250 lines
41 KiB
JavaScript
1250 lines
41 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
;((factory) => { // Module boilerplate :(
|
|
if (typeof(require) === 'function') { // CommonJS
|
|
require("chrome").Cu.import(module.uri, exports);
|
|
}
|
|
else if (typeof Components != 'undefined') { // JSM
|
|
let module = { uri: __URI__, id: "toolkit/loader", exports: Object.create(null) }
|
|
factory(module);
|
|
Object.assign(this, module.exports);
|
|
this.EXPORTED_SYMBOLS = Object.getOwnPropertyNames(module.exports);
|
|
}
|
|
else {
|
|
throw Error("Loading environment is not supported");
|
|
}
|
|
})(module => {
|
|
|
|
'use strict';
|
|
|
|
module.metadata = {
|
|
"stability": "unstable"
|
|
};
|
|
|
|
const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
|
|
results: Cr, manager: Cm } = Components;
|
|
const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
|
|
const { loadSubScript } = Cc['@mozilla.org/moz/jssubscript-loader;1'].
|
|
getService(Ci.mozIJSSubScriptLoader);
|
|
const { addObserver, notifyObservers } = Cc['@mozilla.org/observer-service;1'].
|
|
getService(Ci.nsIObserverService);
|
|
const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs", {});
|
|
const { NetUtil } = ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs", {});
|
|
|
|
|
|
|
|
|
|
// These are used for manipulating jar entry paths, which always use Unix
|
|
// separators (originally copied from `ospath_unix.jsm` as part of the "OS.Path
|
|
// to PathUtils" migration).
|
|
|
|
/**
|
|
* Join path components.
|
|
* This is the recommended manner of getting the path of a file/subdirectory
|
|
* in a directory.
|
|
*
|
|
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
|
|
* var tmpDir = OS.Constants.Path.tmpDir;
|
|
* var path = OS.Path.join(tmpDir, "foo", "bar");
|
|
*
|
|
* Under Unix, this will return "/tmp/foo/bar".
|
|
*
|
|
* Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the
|
|
* same as `OS.Path.join("foo", "bar")`.
|
|
*/
|
|
var pathJoin = function(...path) {
|
|
// If there is a path that starts with a "/", eliminate everything before
|
|
let paths = [];
|
|
for (let subpath of path) {
|
|
if (subpath == null) {
|
|
throw new TypeError("invalid path component");
|
|
}
|
|
if (!subpath.length) {
|
|
continue;
|
|
} else if (subpath[0] == "/") {
|
|
paths = [subpath];
|
|
} else {
|
|
paths.push(subpath);
|
|
}
|
|
}
|
|
return paths.join("/");
|
|
};
|
|
|
|
/**
|
|
* Normalize a path by removing any unneeded ".", "..", "//".
|
|
*/
|
|
var normalize = function(path) {
|
|
let stack = [];
|
|
let absolute;
|
|
if (path.length >= 0 && path[0] == "/") {
|
|
absolute = true;
|
|
} else {
|
|
absolute = false;
|
|
}
|
|
path.split("/").forEach(function(v) {
|
|
switch (v) {
|
|
case "":
|
|
case ".": // fallthrough
|
|
break;
|
|
case "..":
|
|
if (!stack.length) {
|
|
if (absolute) {
|
|
throw new Error("Path is ill-formed: attempting to go past root");
|
|
} else {
|
|
stack.push("..");
|
|
}
|
|
} else if (stack[stack.length - 1] == "..") {
|
|
stack.push("..");
|
|
} else {
|
|
stack.pop();
|
|
}
|
|
break;
|
|
default:
|
|
stack.push(v);
|
|
}
|
|
});
|
|
let string = stack.join("/");
|
|
return absolute ? "/" + string : string;
|
|
};
|
|
|
|
/**
|
|
* Return the directory part of the path.
|
|
* The directory part of the path is everything before the last
|
|
* "/". If the last few characters of this part are also "/",
|
|
* they are ignored.
|
|
*
|
|
* If the path contains no directory, return ".".
|
|
*/
|
|
function dirname(path) {
|
|
let index = path.lastIndexOf("/");
|
|
if (index == -1) {
|
|
return ".";
|
|
}
|
|
while (index >= 0 && path[index] == "/") {
|
|
--index;
|
|
}
|
|
return path.slice(0, index + 1);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "resProto",
|
|
"@mozilla.org/network/protocol;1?name=resource",
|
|
"nsIResProtocolHandler");
|
|
XPCOMUtils.defineLazyServiceGetter(this, "zipCache",
|
|
"@mozilla.org/libjar/zip-reader-cache;1",
|
|
"nsIZipReaderCache");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "XulApp", () => {
|
|
let xulappURI = module.uri.replace("toolkit/loader.js",
|
|
"sdk/system/xul-app.jsm");
|
|
return Cu.import(xulappURI, {});
|
|
});
|
|
|
|
// Define some shortcuts.
|
|
const bind = Function.call.bind(Function.bind);
|
|
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
const prototypeOf = Object.getPrototypeOf;
|
|
const getOwnIdentifiers = x => [...Object.getOwnPropertyNames(x),
|
|
...Object.getOwnPropertySymbols(x)];
|
|
|
|
const NODE_MODULES = new Set([
|
|
"assert",
|
|
"buffer_ieee754",
|
|
"buffer",
|
|
"child_process",
|
|
"cluster",
|
|
"console",
|
|
"constants",
|
|
"crypto",
|
|
"_debugger",
|
|
"dgram",
|
|
"dns",
|
|
"domain",
|
|
"events",
|
|
"freelist",
|
|
"fs",
|
|
"http",
|
|
"https",
|
|
"_linklist",
|
|
"module",
|
|
"net",
|
|
"os",
|
|
"path",
|
|
"punycode",
|
|
"querystring",
|
|
"readline",
|
|
"repl",
|
|
"stream",
|
|
"string_decoder",
|
|
"sys",
|
|
"timers",
|
|
"tls",
|
|
"tty",
|
|
"url",
|
|
"util",
|
|
"vm",
|
|
"zlib",
|
|
]);
|
|
|
|
const COMPONENT_ERROR = '`Components` is not available in this context.\n' +
|
|
'Functionality provided by Components may be available in an SDK\n' +
|
|
'module: https://developer.mozilla.org/en-US/Add-ons/SDK \n\n' +
|
|
'However, if you still need to import Components, you may use the\n' +
|
|
'`chrome` module\'s properties for shortcuts to Component properties:\n\n' +
|
|
'Shortcuts: \n' +
|
|
' Cc = Components' + '.classes \n' +
|
|
' Ci = Components' + '.interfaces \n' +
|
|
' Cu = Components' + '.utils \n' +
|
|
' CC = Components' + '.Constructor \n' +
|
|
'Example: \n' +
|
|
' let { Cc, Ci } = require(\'chrome\');\n';
|
|
|
|
// Workaround for bug 674195. Freezing objects from other compartments fail,
|
|
// so we use `Object.freeze` from the same component instead.
|
|
function freeze(object) {
|
|
if (prototypeOf(object) === null) {
|
|
Object.freeze(object);
|
|
}
|
|
else {
|
|
prototypeOf(prototypeOf(object.isPrototypeOf)).
|
|
constructor. // `Object` from the owner compartment.
|
|
freeze(object);
|
|
}
|
|
return object;
|
|
}
|
|
|
|
// Returns map of given `object`-s own property descriptors.
|
|
const descriptor = iced(function descriptor(object) {
|
|
let value = {};
|
|
getOwnIdentifiers(object).forEach(function(name) {
|
|
value[name] = getOwnPropertyDescriptor(object, name)
|
|
});
|
|
return value;
|
|
});
|
|
Loader.descriptor = descriptor;
|
|
|
|
// Freeze important built-ins so they can't be used by untrusted code as a
|
|
// message passing channel.
|
|
freeze(Object);
|
|
freeze(Object.prototype);
|
|
freeze(Function);
|
|
freeze(Function.prototype);
|
|
freeze(Array);
|
|
freeze(Array.prototype);
|
|
freeze(String);
|
|
freeze(String.prototype);
|
|
|
|
// This function takes `f` function sets it's `prototype` to undefined and
|
|
// freezes it. We need to do this kind of deep freeze with all the exposed
|
|
// functions so that untrusted code won't be able to use them a message
|
|
// passing channel.
|
|
function iced(f) {
|
|
if (!Object.isFrozen(f)) {
|
|
f.prototype = undefined;
|
|
}
|
|
return freeze(f);
|
|
}
|
|
|
|
// Defines own properties of given `properties` object on the given
|
|
// target object overriding any existing property with a conflicting name.
|
|
// Returns `target` object. Note we only export this function because it's
|
|
// useful during loader bootstrap when other util modules can't be used &
|
|
// thats only case where this export should be used.
|
|
const override = iced(function override(target, source) {
|
|
let properties = descriptor(target)
|
|
let extension = descriptor(source || {})
|
|
getOwnIdentifiers(extension).forEach(function(name) {
|
|
properties[name] = extension[name];
|
|
});
|
|
return Object.defineProperties({}, properties);
|
|
});
|
|
Loader.override = override;
|
|
|
|
function sourceURI(uri) { return String(uri).split(" -> ").pop(); }
|
|
Loader.sourceURI = iced(sourceURI);
|
|
|
|
function isntLoaderFrame(frame) { return frame.fileName !== module.uri }
|
|
|
|
function parseURI(uri) { return String(uri).split(" -> ").pop(); }
|
|
Loader.parseURI = parseURI;
|
|
|
|
function parseStack(stack) {
|
|
let lines = String(stack).split("\n");
|
|
return lines.reduce(function(frames, line) {
|
|
if (line) {
|
|
let atIndex = line.indexOf("@");
|
|
let columnIndex = line.lastIndexOf(":");
|
|
let lineIndex = line.lastIndexOf(":", columnIndex - 1);
|
|
let fileName = parseURI(line.slice(atIndex + 1, lineIndex));
|
|
let lineNumber = parseInt(line.slice(lineIndex + 1, columnIndex));
|
|
let columnNumber = parseInt(line.slice(columnIndex + 1));
|
|
let name = line.slice(0, atIndex).split("(").shift();
|
|
frames.unshift({
|
|
fileName: fileName,
|
|
name: name,
|
|
lineNumber: lineNumber,
|
|
columnNumber: columnNumber
|
|
});
|
|
}
|
|
return frames;
|
|
}, []);
|
|
}
|
|
Loader.parseStack = parseStack;
|
|
|
|
function serializeStack(frames) {
|
|
return frames.reduce(function(stack, frame) {
|
|
return frame.name + "@" +
|
|
frame.fileName + ":" +
|
|
frame.lineNumber + ":" +
|
|
frame.columnNumber + "\n" +
|
|
stack;
|
|
}, "");
|
|
}
|
|
Loader.serializeStack = serializeStack;
|
|
|
|
class DefaultMap extends Map {
|
|
constructor(createItem, items = undefined) {
|
|
super(items);
|
|
|
|
this.createItem = createItem;
|
|
}
|
|
|
|
get(key) {
|
|
if (!this.has(key)) {
|
|
this.set(key, this.createItem(key));
|
|
}
|
|
|
|
return super.get(key);
|
|
}
|
|
}
|
|
|
|
const urlCache = {
|
|
/**
|
|
* Returns a list of fully-qualified URLs for entries within the zip
|
|
* file at the given URI which are either directories or files with a
|
|
* .js or .json extension.
|
|
*
|
|
* @param {nsIJARURI} uri
|
|
* @param {string} baseURL
|
|
* The original base URL, prior to resolution.
|
|
*
|
|
* @returns {Set<string>}
|
|
*/
|
|
getZipFileContents(uri, baseURL) {
|
|
// Make sure the path has a trailing slash, and strip off the leading
|
|
// slash, so that we can easily check whether it is a path prefix.
|
|
let basePath = addTrailingSlash(uri.JAREntry).slice(1);
|
|
let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file;
|
|
|
|
let enumerator = zipCache.getZip(file).findEntries("(*.js|*.json|*/)");
|
|
|
|
let results = new Set();
|
|
for (let entry of XPCOMUtils.IterStringEnumerator(enumerator)) {
|
|
if (entry.startsWith(basePath)) {
|
|
let path = entry.slice(basePath.length);
|
|
|
|
results.add(baseURL + path);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
},
|
|
|
|
zipContentsCache: new DefaultMap(baseURL => {
|
|
let uri = NetUtil.newURI(baseURL);
|
|
|
|
if (baseURL.startsWith("resource:")) {
|
|
uri = NetUtil.newURI(resProto.resolveURI(uri));
|
|
}
|
|
|
|
if (uri instanceof Ci.nsIJARURI) {
|
|
return urlCache.getZipFileContents(uri, baseURL);
|
|
}
|
|
|
|
return null;
|
|
}),
|
|
|
|
filesCache: new DefaultMap(url => {
|
|
try {
|
|
let uri = NetUtil.newURI(url).QueryInterface(Ci.nsIFileURL);
|
|
|
|
return uri.file.exists();
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}),
|
|
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsISupportsWeakReference]),
|
|
|
|
observe() {
|
|
// Clear any module resolution caches when the startup cache is flushed,
|
|
// since it probably means we're loading new copies of extensions.
|
|
this.zipContentsCache.clear();
|
|
this.filesCache.clear();
|
|
},
|
|
|
|
/**
|
|
* Returns the base URL for the given URL, if one can be determined. For
|
|
* a resource: URL, this is the root of the resource package. For a jar:
|
|
* URL, it is the root of the JAR file. Otherwise, null is returned.
|
|
*
|
|
* @param {string} url
|
|
* @returns {string?}
|
|
*/
|
|
getBaseURL(url) {
|
|
// By using simple string matching for the common case of resource: URLs
|
|
// backed by jar: URLs, we can avoid creating any nsIURI objects for the
|
|
// common case where the JAR contents are already cached.
|
|
if (url.startsWith("resource://")) {
|
|
return /^resource:\/\/[^\/]+\//.exec(url)[0];
|
|
}
|
|
|
|
let uri = NetUtil.newURI(url);
|
|
if (uri instanceof Ci.nsIJARURI) {
|
|
return `jar:${uri.JARFile.spec}!/`;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Returns true if the target of the given URL exists as a local file,
|
|
* or as an entry in a local zip file.
|
|
*
|
|
* @param {string} url
|
|
* @returns {boolean}
|
|
*/
|
|
exists(url) {
|
|
if (!/\.(?:js|json)$/.test(url)) {
|
|
url = addTrailingSlash(url);
|
|
}
|
|
|
|
let baseURL = this.getBaseURL(url);
|
|
let scripts = baseURL && this.zipContentsCache.get(baseURL);
|
|
if (scripts) {
|
|
return scripts.has(url);
|
|
}
|
|
|
|
return this.filesCache.get(url);
|
|
},
|
|
}
|
|
addObserver(urlCache, "startupcache-invalidate", true);
|
|
|
|
function readURI(uri) {
|
|
let nsURI = NetUtil.newURI(uri);
|
|
if (nsURI.scheme == "resource") {
|
|
// Resolve to a real URI, this will catch any obvious bad paths without
|
|
// logging assertions in debug builds, see bug 1135219
|
|
uri = resProto.resolveURI(nsURI);
|
|
}
|
|
|
|
let stream = NetUtil.newChannel({
|
|
uri: NetUtil.newURI(uri, 'UTF-8'),
|
|
loadUsingSystemPrincipal: true}
|
|
).open2();
|
|
let count = stream.available();
|
|
let data = NetUtil.readInputStreamToString(stream, count, {
|
|
charset: 'UTF-8'
|
|
});
|
|
|
|
stream.close();
|
|
|
|
return data;
|
|
}
|
|
|
|
// Combines all arguments into a resolved, normalized path
|
|
function join(base, ...paths) {
|
|
// If this is an absolute URL, we need to normalize only the path portion,
|
|
// or we wind up stripping too many slashes and producing invalid URLs.
|
|
let match = /^((?:resource|file|chrome)\:\/\/[^\/]*|jar:[^!]+!)(.*)/.exec(base);
|
|
if (match) {
|
|
return match[1] + normalize(pathJoin(match[2], ...paths));
|
|
}
|
|
|
|
return normalize(pathJoin(base, ...paths));
|
|
}
|
|
Loader.join = join;
|
|
|
|
// Function takes set of options and returns a JS sandbox. Function may be
|
|
// passed set of options:
|
|
// - `name`: A string value which identifies the sandbox in about:memory. Will
|
|
// throw exception if omitted.
|
|
// - `principal`: String URI or `nsIPrincipal` for the sandbox. Defaults to
|
|
// system principal.
|
|
// - `prototype`: Ancestor for the sandbox that will be created. Defaults to
|
|
// `{}`.
|
|
// - `wantXrays`: A Boolean value indicating whether code outside the sandbox
|
|
// wants X-ray vision with respect to objects inside the sandbox. Defaults
|
|
// to `true`.
|
|
// - `sandbox`: A sandbox to share JS compartment with. If omitted new
|
|
// compartment will be created.
|
|
// - `metadata`: A metadata object associated with the sandbox. It should
|
|
// be JSON-serializable.
|
|
// For more details see:
|
|
// https://developer.mozilla.org/en/Components.utils.Sandbox
|
|
const Sandbox = iced(function Sandbox(options) {
|
|
// Normalize options and rename to match `Cu.Sandbox` expectations.
|
|
options = {
|
|
// Do not expose `Components` if you really need them (bad idea!) you
|
|
// still can expose via prototype.
|
|
wantComponents: false,
|
|
sandboxName: options.name,
|
|
principal: 'principal' in options ? options.principal : systemPrincipal,
|
|
wantXrays: 'wantXrays' in options ? options.wantXrays : true,
|
|
wantGlobalProperties: 'wantGlobalProperties' in options ?
|
|
options.wantGlobalProperties : [],
|
|
sandboxPrototype: 'prototype' in options ? options.prototype : {},
|
|
invisibleToDebugger: 'invisibleToDebugger' in options ?
|
|
options.invisibleToDebugger : false,
|
|
metadata: 'metadata' in options ? options.metadata : {},
|
|
waiveIntereposition: !!options.waiveIntereposition
|
|
};
|
|
|
|
if (options.metadata && options.metadata.addonID) {
|
|
options.addonId = options.metadata.addonID;
|
|
}
|
|
|
|
let sandbox = Cu.Sandbox(options.principal, options);
|
|
|
|
// Each sandbox at creation gets set of own properties that will be shadowing
|
|
// ones from it's prototype. We override delete such `sandbox` properties
|
|
// to avoid shadowing.
|
|
delete sandbox.Iterator;
|
|
delete sandbox.Components;
|
|
delete sandbox.importFunction;
|
|
delete sandbox.debug;
|
|
|
|
return sandbox;
|
|
});
|
|
Loader.Sandbox = Sandbox;
|
|
|
|
// Evaluates code from the given `uri` into given `sandbox`. If
|
|
// `options.source` is passed, then that code is evaluated instead.
|
|
// Optionally following options may be given:
|
|
// - `options.encoding`: Source encoding, defaults to 'UTF-8'.
|
|
// - `options.line`: Line number to start count from for stack traces.
|
|
// Defaults to 1.
|
|
// - `options.version`: Version of JS used, defaults to '1.8'.
|
|
const evaluate = iced(function evaluate(sandbox, uri, options) {
|
|
let { source, line, version, encoding } = override({
|
|
encoding: 'UTF-8',
|
|
line: 1,
|
|
version: '1.8',
|
|
source: null
|
|
}, options);
|
|
|
|
return source ? Cu.evalInSandbox(source, sandbox, version, uri, line)
|
|
: loadSubScript(uri, sandbox, encoding);
|
|
});
|
|
Loader.evaluate = evaluate;
|
|
|
|
// Populates `exports` of the given CommonJS `module` object, in the context
|
|
// of the given `loader` by evaluating code associated with it.
|
|
const load = iced(function load(loader, module) {
|
|
let { sandboxes, globals, loadModuleHook } = loader;
|
|
let require = Require(loader, module);
|
|
|
|
// We expose set of properties defined by `CommonJS` specification via
|
|
// prototype of the sandbox. Also globals are deeper in the prototype
|
|
// chain so that each module has access to them as well.
|
|
let descriptors = descriptor({
|
|
require: require,
|
|
module: module,
|
|
exports: module.exports,
|
|
get Components() {
|
|
// Expose `Components` property to throw error on usage with
|
|
// additional information
|
|
throw new ReferenceError(COMPONENT_ERROR);
|
|
}
|
|
});
|
|
|
|
let sandbox;
|
|
if ((loader.useSharedGlobalSandbox || isSystemURI(module.uri)) &&
|
|
loader.sharedGlobalBlocklist.indexOf(module.id) == -1) {
|
|
// Create a new object in this sandbox, that will be used as
|
|
// the scope object for this particular module
|
|
sandbox = new loader.sharedGlobalSandbox.Object();
|
|
// Inject all expected globals in the scope object
|
|
getOwnIdentifiers(globals).forEach(function(name) {
|
|
descriptors[name] = getOwnPropertyDescriptor(globals, name)
|
|
descriptors[name].configurable = true;
|
|
});
|
|
Object.defineProperties(sandbox, descriptors);
|
|
}
|
|
else {
|
|
sandbox = Sandbox({
|
|
name: module.uri,
|
|
prototype: Object.create(globals, descriptors),
|
|
wantXrays: false,
|
|
wantGlobalProperties: module.id == "sdk/indexed-db" ? ["indexedDB"] : [],
|
|
invisibleToDebugger: loader.invisibleToDebugger,
|
|
metadata: {
|
|
addonID: loader.id,
|
|
URI: module.uri
|
|
}
|
|
});
|
|
}
|
|
sandboxes[module.uri] = sandbox;
|
|
|
|
try {
|
|
evaluate(sandbox, module.uri);
|
|
}
|
|
catch (error) {
|
|
let { message, fileName, lineNumber } = error;
|
|
let stack = error.stack || Error().stack;
|
|
let frames = parseStack(stack).filter(isntLoaderFrame);
|
|
let toString = String(error);
|
|
let file = sourceURI(fileName);
|
|
|
|
// Note that `String(error)` where error is from subscript loader does
|
|
// not puts `:` after `"Error"` unlike regular errors thrown by JS code.
|
|
// If there is a JS stack then this error has already been handled by an
|
|
// inner module load.
|
|
if (/^Error opening input stream/.test(String(error))) {
|
|
let caller = frames.slice(0).pop();
|
|
fileName = caller.fileName;
|
|
lineNumber = caller.lineNumber;
|
|
message = "Module `" + module.id + "` is not found at " + module.uri;
|
|
toString = message;
|
|
}
|
|
// Workaround for a Bug 910653. Errors thrown by subscript loader
|
|
// do not include `stack` field and above created error won't have
|
|
// fileName or lineNumber of the module being loaded, so we ensure
|
|
// it does.
|
|
else if (frames[frames.length - 1].fileName !== file) {
|
|
frames.push({ fileName: file, lineNumber: lineNumber, name: "" });
|
|
}
|
|
|
|
let prototype = typeof(error) === "object" ? error.constructor.prototype :
|
|
Error.prototype;
|
|
|
|
throw Object.create(prototype, {
|
|
message: { value: message, writable: true, configurable: true },
|
|
fileName: { value: fileName, writable: true, configurable: true },
|
|
lineNumber: { value: lineNumber, writable: true, configurable: true },
|
|
stack: { value: serializeStack(frames), writable: true, configurable: true },
|
|
toString: { value: () => toString, writable: true, configurable: true },
|
|
});
|
|
}
|
|
|
|
if (loadModuleHook) {
|
|
module = loadModuleHook(module, require);
|
|
}
|
|
|
|
if (loader.checkCompatibility) {
|
|
let err = XulApp.incompatibility(module);
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
if (module.exports && typeof(module.exports) === 'object')
|
|
freeze(module.exports);
|
|
|
|
return module;
|
|
});
|
|
Loader.load = load;
|
|
|
|
// Utility function to normalize module `uri`s so they have `.js` extension.
|
|
function normalizeExt(uri) {
|
|
return isJSURI(uri) ? uri :
|
|
isJSONURI(uri) ? uri :
|
|
isJSMURI(uri) ? uri :
|
|
uri + '.js';
|
|
}
|
|
|
|
// Strips `rootURI` from `string` -- used to remove absolute resourceURI
|
|
// from a relative path
|
|
function stripBase(rootURI, string) {
|
|
return string.replace(rootURI, './');
|
|
}
|
|
|
|
// Utility function to join paths. In common case `base` is a
|
|
// `requirer.uri` but in some cases it may be `baseURI`. In order to
|
|
// avoid complexity we require `baseURI` with a trailing `/`.
|
|
const resolve = iced(function resolve(id, base) {
|
|
if (!isRelative(id))
|
|
return id;
|
|
|
|
let baseDir = dirname(base);
|
|
if (!baseDir)
|
|
return normalize(id);
|
|
|
|
let resolved = join(baseDir, id);
|
|
|
|
// Joining and normalizing removes the './' from relative files.
|
|
// We need to ensure the resolution still has the root
|
|
if (isRelative(base))
|
|
resolved = './' + resolved;
|
|
|
|
return resolved;
|
|
});
|
|
Loader.resolve = resolve;
|
|
|
|
// Attempts to load `path` and then `path.js`
|
|
// Returns `path` with valid file, or `undefined` otherwise
|
|
function resolveAsFile(path) {
|
|
// Append '.js' to path name unless it's another support filetype
|
|
path = normalizeExt(path);
|
|
if (urlCache.exists(path)) {
|
|
return path;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Attempts to load `path/package.json`'s `main` entry,
|
|
// followed by `path/index.js`, or `undefined` otherwise
|
|
function resolveAsDirectory(path) {
|
|
try {
|
|
// If `path/package.json` exists, parse the `main` entry
|
|
// and attempt to load that
|
|
let manifestPath = addTrailingSlash(path) + 'package.json';
|
|
|
|
let main = (urlCache.exists(manifestPath) &&
|
|
getManifestMain(JSON.parse(readURI(manifestPath))));
|
|
if (main) {
|
|
let found = resolveAsFile(join(path, main));
|
|
if (found) {
|
|
return found
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
return resolveAsFile(addTrailingSlash(path) + 'index.js');
|
|
}
|
|
|
|
function resolveRelative(rootURI, modulesDir, id) {
|
|
let fullId = join(rootURI, modulesDir, id);
|
|
|
|
let resolvedPath = (resolveAsFile(fullId) ||
|
|
resolveAsDirectory(fullId));
|
|
if (resolvedPath) {
|
|
return stripBase(rootURI, resolvedPath);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// From `resolve` module
|
|
// https://github.com/substack/node-resolve/blob/master/lib/node-modules-paths.js
|
|
function* getNodeModulePaths(rootURI, start) {
|
|
let moduleDir = 'node_modules';
|
|
|
|
let parts = start.split('/');
|
|
while (parts.length) {
|
|
let leaf = parts.pop();
|
|
let path = join(...parts, leaf, moduleDir);
|
|
if (leaf !== moduleDir && urlCache.exists(join(rootURI, path))) {
|
|
yield path;
|
|
}
|
|
}
|
|
|
|
if (urlCache.exists(join(rootURI, moduleDir))) {
|
|
yield moduleDir;
|
|
}
|
|
}
|
|
|
|
// Node-style module lookup
|
|
// Takes an id and path and attempts to load a file using node's resolving
|
|
// algorithm.
|
|
// `id` should already be resolved relatively at this point.
|
|
// http://nodejs.org/api/modules.html#modules_all_together
|
|
const nodeResolve = iced(function nodeResolve(id, requirer, { rootURI }) {
|
|
// Resolve again
|
|
id = Loader.resolve(id, requirer);
|
|
|
|
// If this is already an absolute URI then there is no resolution to do
|
|
if (isAbsoluteURI(id)) {
|
|
return null;
|
|
}
|
|
|
|
// we assume that extensions are correct, i.e., a directory doesnt't have '.js'
|
|
// and a js file isn't named 'file.json.js'
|
|
let resolvedPath;
|
|
|
|
if ((resolvedPath = resolveRelative(rootURI, "", id))) {
|
|
return resolvedPath;
|
|
}
|
|
|
|
// If the requirer is an absolute URI then the node module resolution below
|
|
// won't work correctly as we prefix everything with rootURI
|
|
if (isAbsoluteURI(requirer)) {
|
|
return null;
|
|
}
|
|
|
|
// If manifest has dependencies, attempt to look up node modules
|
|
// in the `dependencies` list
|
|
for (let modulesDir of getNodeModulePaths(rootURI, dirname(requirer))) {
|
|
if ((resolvedPath = resolveRelative(rootURI, modulesDir, id))) {
|
|
return resolvedPath;
|
|
}
|
|
}
|
|
|
|
// We would not find lookup for things like `sdk/tabs`, as that's part of
|
|
// the alias mapping. If during `generateMap`, the runtime lookup resolves
|
|
// with `resolveURI` -- if during runtime, then `resolve` will throw.
|
|
return null;
|
|
});
|
|
|
|
Loader.nodeResolve = nodeResolve;
|
|
|
|
function addTrailingSlash(path) {
|
|
return path.replace(/\/*$/, "/");
|
|
}
|
|
|
|
const resolveURI = iced(function resolveURI(id, mapping) {
|
|
// Do not resolve if already a resource URI
|
|
if (isAbsoluteURI(id))
|
|
return normalizeExt(id);
|
|
|
|
for (let [path, uri] of mapping) {
|
|
// Strip off any trailing slashes to make comparisons simpler
|
|
let stripped = path.replace(/\/+$/, "");
|
|
|
|
// We only want to match path segments explicitly. Examples:
|
|
// * "foo/bar" matches for "foo/bar"
|
|
// * "foo/bar" matches for "foo/bar/baz"
|
|
// * "foo/bar" does not match for "foo/bar-1"
|
|
// * "foo/bar/" does not match for "foo/bar"
|
|
// * "foo/bar/" matches for "foo/bar/baz"
|
|
//
|
|
// Check for an empty path, an exact match, or a substring match
|
|
// with the next character being a forward slash.
|
|
if(stripped === "" || id === stripped || id.startsWith(stripped + "/")) {
|
|
return normalizeExt(id.replace(path, uri));
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
Loader.resolveURI = resolveURI;
|
|
|
|
// Creates version of `require` that will be exposed to the given `module`
|
|
// in the context of the given `loader`. Each module gets own limited copy
|
|
// of `require` that is allowed to load only a modules that are associated
|
|
// with it during link time.
|
|
const Require = iced(function Require(loader, requirer) {
|
|
let {
|
|
modules, mapping, resolve: loaderResolve, load,
|
|
manifest, rootURI, isNative, requireMap,
|
|
requireHook
|
|
} = loader;
|
|
|
|
if (isSystemURI(requirer.uri)) {
|
|
// Built-in modules don't require the expensive module resolution
|
|
// algorithm used by SDK add-ons, so give them the more efficient standard
|
|
// resolve instead.
|
|
isNative = false;
|
|
loaderResolve = Loader.resolve;
|
|
}
|
|
|
|
function require(id) {
|
|
if (!id) // Throw if `id` is not passed.
|
|
throw Error('You must provide a module name when calling require() from '
|
|
+ requirer.id, requirer.uri);
|
|
|
|
if (requireHook) {
|
|
return requireHook(id, _require);
|
|
}
|
|
|
|
return _require(id);
|
|
}
|
|
|
|
function _require(id) {
|
|
// Fix require() from react-autosuggest
|
|
if (id == 'React') id = 'react';
|
|
|
|
let { uri, requirement } = getRequirements(id);
|
|
let module = null;
|
|
// If module is already cached by loader then just use it.
|
|
if (uri in modules) {
|
|
module = modules[uri];
|
|
}
|
|
else if (isJSMURI(uri)) {
|
|
module = modules[uri] = Module(requirement, uri);
|
|
module.exports = Cu.import(uri, {});
|
|
freeze(module);
|
|
}
|
|
else if (isJSONURI(uri)) {
|
|
let data;
|
|
|
|
// First attempt to load and parse json uri
|
|
// ex: `test.json`
|
|
// If that doesn't exist, check for `test.json.js`
|
|
// for node parity
|
|
try {
|
|
data = JSON.parse(readURI(uri));
|
|
module = modules[uri] = Module(requirement, uri);
|
|
module.exports = data;
|
|
freeze(module);
|
|
}
|
|
catch (err) {
|
|
// If error thrown from JSON parsing, throw that, do not
|
|
// attempt to find .json.js file
|
|
if (err && /JSON\.parse/.test(err.message))
|
|
throw err;
|
|
uri = uri + '.js';
|
|
}
|
|
}
|
|
|
|
// If not yet cached, load and cache it.
|
|
// 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);
|
|
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;
|
|
}
|
|
|
|
// Resolution function taking a module name/path and
|
|
// returning a resourceURI and a `requirement` used by the loader.
|
|
// Used by both `require` and `require.resolve`.
|
|
function getRequirements(id) {
|
|
if (!id) // Throw if `id` is not passed.
|
|
throw Error('you must provide a module name when calling require() from '
|
|
+ requirer.id, requirer.uri);
|
|
|
|
let requirement, uri;
|
|
|
|
// TODO should get native Firefox modules before doing node-style lookups
|
|
// to save on loading time
|
|
if (isNative) {
|
|
// If a requireMap is available from `generateMap`, use that to
|
|
// immediately resolve the node-style mapping.
|
|
// TODO: write more tests for this use case
|
|
if (requireMap && requireMap[requirer.id])
|
|
requirement = requireMap[requirer.id][id];
|
|
|
|
let { overrides } = manifest.jetpack;
|
|
for (let key in overrides) {
|
|
// ignore any overrides using relative keys
|
|
if (/^[.\/]/.test(key)) {
|
|
continue;
|
|
}
|
|
|
|
// If the override is for x -> y,
|
|
// then using require("x/lib/z") to get reqire("y/lib/z")
|
|
// should also work
|
|
if (id == key || id.startsWith(key + "/")) {
|
|
id = overrides[key] + id.substr(key.length);
|
|
id = id.replace(/^[.\/]+/, "");
|
|
}
|
|
}
|
|
|
|
// For native modules, we want to check if it's a module specified
|
|
// in 'modules', like `chrome`, or `@loader` -- if it exists,
|
|
// just set the uri to skip resolution
|
|
if (!requirement && modules[id])
|
|
uri = requirement = id;
|
|
|
|
// If no requireMap was provided, or resolution not found in
|
|
// the requireMap, and not a npm dependency, attempt a runtime lookup
|
|
if (!requirement && !NODE_MODULES.has(id)) {
|
|
// If `isNative` defined, this is using the new, native-style
|
|
// loader, not cuddlefish, so lets resolve using node's algorithm
|
|
// and get back a path that needs to be resolved via paths mapping
|
|
// in `resolveURI`
|
|
requirement = loaderResolve(id, requirer.id, {
|
|
manifest: manifest,
|
|
rootURI: rootURI
|
|
});
|
|
}
|
|
|
|
// If not found in the map, not a node module, and wasn't able to be
|
|
// looked up, it's something
|
|
// found in the paths most likely, like `sdk/tabs`, which should
|
|
// be resolved relatively if needed using traditional resolve
|
|
if (!requirement) {
|
|
requirement = isRelative(id) ? Loader.resolve(id, requirer.id) : id;
|
|
}
|
|
}
|
|
else if (modules[id]) {
|
|
uri = requirement = id;
|
|
}
|
|
else if (requirer) {
|
|
// Resolve `id` to its requirer if it's relative.
|
|
requirement = loaderResolve(id, requirer.id);
|
|
}
|
|
else {
|
|
requirement = id;
|
|
}
|
|
|
|
// Resolves `uri` of module using loaders resolve function.
|
|
uri = uri || resolveURI(requirement, mapping);
|
|
|
|
// Throw if `uri` can not be resolved.
|
|
if (!uri) {
|
|
throw Error('Module: Can not resolve "' + id + '" module required by ' +
|
|
requirer.id + ' located at ' + requirer.uri, requirer.uri);
|
|
}
|
|
|
|
return { uri: uri, requirement: requirement };
|
|
}
|
|
|
|
// Expose the `resolve` function for this `Require` instance
|
|
require.resolve = _require.resolve = function resolve(id) {
|
|
let { uri } = getRequirements(id);
|
|
return uri;
|
|
}
|
|
|
|
// This is like webpack's require.context. It returns a new require
|
|
// function that prepends the prefix to any requests.
|
|
require.context = prefix => {
|
|
return id => {
|
|
return require(prefix + id);
|
|
};
|
|
};
|
|
|
|
// Make `require.main === module` evaluate to true in main module scope.
|
|
require.main = loader.main === requirer ? requirer : undefined;
|
|
return iced(require);
|
|
});
|
|
Loader.Require = Require;
|
|
|
|
const main = iced(function main(loader, id) {
|
|
// If no main entry provided, and native loader is used,
|
|
// read the entry in the manifest
|
|
if (!id && loader.isNative)
|
|
id = getManifestMain(loader.manifest);
|
|
let uri = resolveURI(id, loader.mapping);
|
|
let module = loader.main = loader.modules[uri] = Module(id, uri);
|
|
return loader.load(loader, module).exports;
|
|
});
|
|
Loader.main = main;
|
|
|
|
// Makes module object that is made available to CommonJS modules when they
|
|
// are evaluated, along with `exports` and `require`.
|
|
const Module = iced(function Module(id, uri) {
|
|
return Object.create(null, {
|
|
id: { enumerable: true, value: id },
|
|
exports: { enumerable: true, writable: true, value: Object.create(null),
|
|
configurable: true },
|
|
uri: { value: uri }
|
|
});
|
|
});
|
|
Loader.Module = Module;
|
|
|
|
// Takes `loader`, and unload `reason` string and notifies all observers that
|
|
// they should cleanup after them-self.
|
|
const unload = iced(function unload(loader, reason) {
|
|
// subject is a unique object created per loader instance.
|
|
// This allows any code to cleanup on loader unload regardless of how
|
|
// it was loaded. To handle unload for specific loader subject may be
|
|
// asserted against loader.destructor or require('@loader/unload')
|
|
// Note: We don not destroy loader's module cache or sandboxes map as
|
|
// some modules may do cleanup in subsequent turns of event loop. Destroying
|
|
// cache may cause module identity problems in such cases.
|
|
let subject = { wrappedJSObject: loader.destructor };
|
|
notifyObservers(subject, 'sdk:loader:destroy', reason);
|
|
});
|
|
Loader.unload = unload;
|
|
|
|
// Function makes new loader that can be used to load CommonJS modules
|
|
// described by a given `options.manifest`. Loader takes following options:
|
|
// - `globals`: Optional map of globals, that all module scopes will inherit
|
|
// from. Map is also exposed under `globals` property of the returned loader
|
|
// so it can be extended further later. Defaults to `{}`.
|
|
// - `modules` Optional map of built-in module exports mapped by module id.
|
|
// These modules will incorporated into module cache. Each module will be
|
|
// frozen.
|
|
// - `resolve` Optional module `id` resolution function. If given it will be
|
|
// used to resolve module URIs, by calling it with require term, requirer
|
|
// module object (that has `uri` property) and `baseURI` of the loader.
|
|
// If `resolve` does not returns `uri` string exception will be thrown by
|
|
// an associated `require` call.
|
|
function Loader(options) {
|
|
if (options.sharedGlobalBlacklist && !options.sharedGlobalBlocklist) {
|
|
options.sharedGlobalBlocklist = options.sharedGlobalBlacklist;
|
|
}
|
|
let {
|
|
modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative,
|
|
metadata, sharedGlobal, sharedGlobalBlocklist, checkCompatibility, waiveIntereposition
|
|
} = override({
|
|
paths: {},
|
|
modules: {},
|
|
globals: {
|
|
get console() {
|
|
// Import Console.jsm from here to prevent loading it until someone uses it
|
|
let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm");
|
|
let console = new ConsoleAPI({
|
|
consoleID: options.id ? "addon/" + options.id : ""
|
|
});
|
|
Object.defineProperty(this, "console", { value: console });
|
|
return this.console;
|
|
}
|
|
},
|
|
checkCompatibility: false,
|
|
resolve: options.isNative ?
|
|
// Make the returned resolve function have the same signature
|
|
(id, requirer) => Loader.nodeResolve(id, requirer, { rootURI: rootURI }) :
|
|
Loader.resolve,
|
|
sharedGlobalBlocklist: ["sdk/indexed-db"],
|
|
waiveIntereposition: false
|
|
}, options);
|
|
|
|
// Create overrides defaults, none at the moment
|
|
if (typeof manifest != "object" || !manifest) {
|
|
manifest = {};
|
|
}
|
|
if (typeof manifest.jetpack != "object" || !manifest.jetpack) {
|
|
manifest.jetpack = {
|
|
overrides: {}
|
|
};
|
|
}
|
|
if (typeof manifest.jetpack.overrides != "object" || !manifest.jetpack.overrides) {
|
|
manifest.jetpack.overrides = {};
|
|
}
|
|
|
|
// We create an identity object that will be dispatched on an unload
|
|
// event as subject. This way unload listeners will be able to assert
|
|
// which loader is unloaded. Please note that we intentionally don't
|
|
// use `loader` as subject to prevent a loader access leakage through
|
|
// observer notifications.
|
|
let destructor = freeze(Object.create(null));
|
|
|
|
// Make mapping array that is sorted from longest path to shortest path.
|
|
let mapping = Object.keys(paths)
|
|
.sort((a, b) => b.length - a.length)
|
|
.map(path => [path, paths[path]]);
|
|
|
|
// Define pseudo modules.
|
|
modules = override({
|
|
'@loader/unload': destructor,
|
|
'@loader/options': options,
|
|
'chrome': { Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
|
|
CC: bind(CC, Components), components: Components,
|
|
// `ChromeWorker` has to be inject in loader global scope.
|
|
// It is done by bootstrap.js:loadSandbox for the SDK.
|
|
ChromeWorker,
|
|
ChromeUtils,
|
|
}
|
|
}, modules);
|
|
|
|
const builtinModuleExports = modules;
|
|
modules = {};
|
|
for (let id of Object.keys(builtinModuleExports)) {
|
|
// We resolve `uri` from `id` since modules are cached by `uri`.
|
|
let uri = resolveURI(id, mapping);
|
|
// In native loader, the mapping will not contain values for
|
|
// pseudomodules -- store them as their ID rather than the URI
|
|
if (isNative && !uri)
|
|
uri = id;
|
|
let module = Module(id, uri);
|
|
|
|
// Lazily expose built-in modules in order to
|
|
// allow them to be loaded lazily.
|
|
Object.defineProperty(module, "exports", {
|
|
enumerable: true,
|
|
get: function() {
|
|
return builtinModuleExports[id];
|
|
}
|
|
});
|
|
|
|
modules[uri] = freeze(module);
|
|
}
|
|
|
|
// Create the unique sandbox we will be using for all modules,
|
|
// so that we prevent creating a new comportment per module.
|
|
// The side effect is that all modules will share the same
|
|
// global objects.
|
|
let sharedGlobalSandbox = Sandbox({
|
|
name: "Addon-SDK",
|
|
wantXrays: false,
|
|
wantGlobalProperties: [],
|
|
invisibleToDebugger: options.invisibleToDebugger || false,
|
|
metadata: {
|
|
addonID: options.id,
|
|
URI: "Addon-SDK"
|
|
},
|
|
prototype: options.sandboxPrototype || {}
|
|
});
|
|
|
|
// Loader object is just a representation of a environment
|
|
// state. We freeze it and mark make it's properties non-enumerable
|
|
// as they are pure implementation detail that no one should rely upon.
|
|
let returnObj = {
|
|
destructor: { enumerable: false, value: destructor },
|
|
globals: { enumerable: false, value: globals },
|
|
mapping: { enumerable: false, value: mapping },
|
|
// Map of module objects indexed by module URIs.
|
|
modules: { enumerable: false, value: modules },
|
|
metadata: { enumerable: false, value: metadata },
|
|
useSharedGlobalSandbox: { enumerable: false, value: !!sharedGlobal },
|
|
sharedGlobalSandbox: { enumerable: false, value: sharedGlobalSandbox },
|
|
sharedGlobalBlocklist: { enumerable: false, value: sharedGlobalBlocklist },
|
|
sharedGlobalBlacklist: { enumerable: false, value: sharedGlobalBlocklist },
|
|
// Map of module sandboxes indexed by module URIs.
|
|
sandboxes: { enumerable: false, value: {} },
|
|
resolve: { enumerable: false, value: resolve },
|
|
// ID of the addon, if provided.
|
|
id: { enumerable: false, value: options.id },
|
|
// Whether the modules loaded should be ignored by the debugger
|
|
invisibleToDebugger: { enumerable: false,
|
|
value: options.invisibleToDebugger || false },
|
|
load: { enumerable: false, value: options.load || load },
|
|
checkCompatibility: { enumerable: false, value: checkCompatibility },
|
|
requireHook: { enumerable: false, value: options.requireHook },
|
|
loadModuleHook: { enumerable: false, value: options.loadModuleHook },
|
|
// Main (entry point) module, it can be set only once, since loader
|
|
// instance can have only one main module.
|
|
main: new function() {
|
|
let main;
|
|
return {
|
|
enumerable: false,
|
|
get: function() { return main; },
|
|
// Only set main if it has not being set yet!
|
|
set: function(module) { main = main || module; }
|
|
}
|
|
}
|
|
};
|
|
|
|
if (isNative) {
|
|
returnObj.isNative = { enumerable: false, value: true };
|
|
returnObj.manifest = { enumerable: false, value: manifest };
|
|
returnObj.requireMap = { enumerable: false, value: requireMap };
|
|
returnObj.rootURI = { enumerable: false, value: addTrailingSlash(rootURI) };
|
|
}
|
|
|
|
return freeze(Object.create(null, returnObj));
|
|
};
|
|
Loader.Loader = Loader;
|
|
|
|
var isSystemURI = uri => /^resource:\/\/(gre|devtools|testing-common)\//.test(uri);
|
|
|
|
var isJSONURI = uri => uri.endsWith('.json');
|
|
var isJSMURI = uri => uri.endsWith('.jsm');
|
|
var isJSURI = uri => uri.endsWith('.js');
|
|
var isAbsoluteURI = uri => uri.startsWith("resource://") ||
|
|
uri.startsWith("chrome://") ||
|
|
uri.startsWith("file://");
|
|
var isRelative = id => id.startsWith(".");
|
|
|
|
// Default `main` entry to './index.js' and ensure is relative,
|
|
// since node allows 'lib/index.js' without relative `./`
|
|
function getManifestMain(manifest) {
|
|
let main = manifest.main || './index.js';
|
|
return isRelative(main) ? main : './' + main;
|
|
}
|
|
|
|
module.exports = iced(Loader);
|
|
});
|
|
|