/* ***** BEGIN LICENSE BLOCK ***** * Copyright (c) 2009-2010 the Mozilla Foundation * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the Mozilla Foundation nor the names * of its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ "use strict"; (function(global) { const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; const Cr = Components.results; var exports = {}; var ios = Cc['@mozilla.org/network/io-service;1'] .getService(Ci.nsIIOService); var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] .createInstance(Ci.nsIPrincipal); // Even though manifest.py does some dependency scanning, that // scan is done as part of an evaluation of what the add-on needs // for security purposes. The following regexps are used to scan for // dependencies inside a simplified define() callback: // define(function(require, exports, module){ var a = require('a'); }); // and are used at runtime ensure the dependencies needed by // the define factory function are already evaluated and ready. // Even though this loader is a sync loader, and could fetch the module // as the require() call happens, it would differ in behavior as // compared to the async browser case, which would make sure to execute // the dependencies first before executing the define() factory function. // So this dependency scanning and evaluation is kept to match the // async behavior. var commentRegExp = /(\/\*([\s\S]*?)\*\/|\/\/(.*)$)/mg; var cjsRequireRegExp = /require\(["']([\w\!\-_\.\/]+)["']\)/g; var cjsStandardDeps = ['require', 'exports', 'module']; function resolvePrincipal(principal, defaultPrincipal) { if (principal === undefined) return defaultPrincipal; if (principal == "system") return systemPrincipal; return principal; } // The base URI to we use when we're given relative URLs, if any. var baseURI = null; if (global.window) baseURI = ios.newURI(global.location.href, null, null); exports.baseURI = baseURI; // The "parent" chrome URI to use if we're loading code that // needs chrome privileges but may not have a filename that // matches any of SpiderMonkey's defined system filename prefixes. // The latter is needed so that wrappers can be automatically // made for the code. For more information on this, see // bug 418356: // // https://bugzilla.mozilla.org/show_bug.cgi?id=418356 var parentChromeURIString; if (baseURI) // We're being loaded from a chrome-privileged document, so // use its URL as the parent string. parentChromeURIString = baseURI.spec; else // We're being loaded from a chrome-privileged JS module or // SecurableModule, so use its filename (which may itself // contain a reference to a parent). parentChromeURIString = Components.stack.filename; function maybeParentifyFilename(filename) { var doParentifyFilename = true; try { // TODO: Ideally we should just make // nsIChromeRegistry.wrappersEnabled() available from script // and use it here. Until that's in the platform, though, // we'll play it safe and parentify the filename unless // we're absolutely certain things will be ok if we don't. var filenameURI = ios.newURI(options.filename, null, baseURI); if (filenameURI.scheme == 'chrome' && filenameURI.path.indexOf('/content/') == 0) // Content packages will always have wrappers made for them; // if automatic wrappers have been disabled for the // chrome package via a chrome manifest flag, then // this still works too, to the extent that the // content package is insecure anyways. doParentifyFilename = false; } catch (e) {} if (doParentifyFilename) return parentChromeURIString + " -> " + filename; return filename; } function getRootDir(urlStr) { // TODO: This feels hacky, and like there will be edge cases. return urlStr.slice(0, urlStr.lastIndexOf("/") + 1); } exports.SandboxFactory = function SandboxFactory(defaultPrincipal) { // Unless specified otherwise, use a principal with limited // privileges. this._defaultPrincipal = resolvePrincipal(defaultPrincipal, "http://www.mozilla.org"); }, exports.SandboxFactory.prototype = { createSandbox: function createSandbox(options) { var principal = resolvePrincipal(options.principal, this._defaultPrincipal); return { _sandbox: new Cu.Sandbox(principal, options.filename ? { sandboxName: options.filename } : { } ), _principal: principal, get globalScope() { return this._sandbox; }, defineProperty: function defineProperty(name, value) { this._sandbox[name] = value; }, getProperty: function getProperty(name) { return this._sandbox[name]; }, evaluate: function evaluate(options) { if (typeof(options) == 'string') options = {contents: options}; options = {__proto__: options}; if (typeof(options.contents) != 'string') throw new Error('Expected string for options.contents'); if (options.lineNo === undefined) options.lineNo = 1; if (options.jsVersion === undefined) options.jsVersion = "1.8"; if (typeof(options.filename) != 'string') options.filename = ''; if (this._principal == systemPrincipal) options.filename = maybeParentifyFilename(options.filename); return Cu.evalInSandbox(options.contents, this._sandbox, options.jsVersion, options.filename, options.lineNo); } }; } }; exports.Loader = function Loader(options) { options = {__proto__: options}; if (options.fs === undefined) { var rootPaths = options.rootPath || options.rootPaths; if (rootPaths) { if (rootPaths.constructor.name != "Array") rootPaths = [rootPaths]; var fses = [new exports.LocalFileSystem(path) for each (path in rootPaths)]; options.fs = new exports.CompositeFileSystem({ fses: fses, metadata: options.metadata, uriPrefix: options.uriPrefix, name: options.name }); } else options.fs = new exports.LocalFileSystem(); } if (options.sandboxFactory === undefined) options.sandboxFactory = new exports.SandboxFactory( options.defaultPrincipal ); if ('modules' in options) throw new Error('options.modules is no longer supported'); // pathAccessed used to know if a module was accessed/required // by another module, and in that case, assigning the module value // via a define callback is not allowed. if (options.pathAccessed === undefined) options.pathAccessed = {}; if (options.globals === undefined) options.globals = {}; this.fs = options.fs; this.sandboxFactory = options.sandboxFactory; this.sandboxes = {}; this.modules = {}; this.pathAccessed = options.pathAccessed; this.defineUsed = {}; this.globals = options.globals; this.getModuleExports = options.getModuleExports; this.modifyModuleSandbox = options.modifyModuleSandbox; this.manifest = options.manifest || {}; }; exports.Loader.prototype = { _makeApi: function _makeApi(basePath) { /* * _makeApi() creates a pair of specialized require()/define() * functions for use by the code that comes from 'basePath' (which is * a resource: URI pointing to some module, e.g. main.js). This * require/define pair knows what main.js is allowed to import, in * particular it knows what the link-time module search algorithm has * found for each imported name (so if they require "panel", they'll * get the one from addon-kit, not from some other package). * * When some other module (e.g. panel.js) is loaded, they'll get a * different require/define pair, specialized for them. */ var self = this; let reqs; if (basePath && (basePath in self.manifest)) reqs = self.manifest[basePath].requirements; function syncRequire(module) { if (reqs) { // if we know about you, you must follow the manifest if (module in reqs) return loadMaybeMagicModule(module, reqs[module]); // if you invoke chrome, you can go off-manifest and search if ("chrome" in reqs) return loadMaybeMagicModule(module, null); throw new Error("Module at "+basePath+" not allowed to require"+"("+module+")"); } else { // if we don't know about you, you can do anything you want. // You're going to have to search for your own modules, though. return loadMaybeMagicModule(module, null); } } function loadMaybeMagicModule(moduleName, moduleData) { /* * If we get here, we're allowed to import this module, we just have * to figure out how. * * 'moduleName' is the unmodified argument passed to require(), * so it might be "panel" or "pkg/foo" or even "./bar" for relative * imports. 'moduleData' is the manifest entry that tells us how * we're supposed to import this module: usually it's an object with * a .uri, but for certain "magic" modules it might be empty. If * it's 'null' then we're supposed to search all known packages for * it. */ if (self.getModuleExports) { /* this currently handles 'chrome' */ let exports = self.getModuleExports(basePath, moduleName); if (exports) return exports; } if (moduleName == "self") { /* The 'self' module is magic: when someone requires 'self', the * module they get is supposed to be specialized for the *package* * that they live in (so pkg1/foo.js will get 'self' for pkg1, * while pkg2/bar.js will get a 'self' for pkg2). To accomplish * this, we don't give them the real self.js module directly: * instead, we load self.js and invoke its makeSelfModule() * function, passing in the manifest's moduleData, which will * include enough information to create the specialized module. */ if (!moduleData) { // we don't know where you live, so we must search for your data // resource://api-utils-api-utils-tests/test-self.js // make a prefix of resource://api-utils-api-utils-data/ let doubleslash = basePath.indexOf("//"); let prefix = basePath.slice(0, doubleslash+2); let rest = basePath.slice(doubleslash+2); let slash = rest.indexOf("/"); prefix = prefix + rest.slice(0, slash); prefix = prefix.slice(0, prefix.lastIndexOf("-")) + "-data/"; moduleData = { "dataURIPrefix": prefix }; // moduleData also wants mapName and mapSHA256, but they're // currently unused } if (false) // force scanner to copy self-maker.js into the XPI require("./self-maker"); let makerModData = {uri: self.fs.resolveModule(null, "self-maker")}; if (!makerModData.uri) throw new Error("Unable to find self-maker, from "+basePath); let selfMod = loadFromModuleData(makerModData, "self-maker"); // selfMod is not cached return selfMod.makeSelfModule(moduleData); } if (!moduleData) { // search let path = self.fs.resolveModule(basePath, moduleName); if (!path) throw new Error('Module "' + moduleName + '" not found'); moduleData = {uri: path}; } // Track accesses to this module via its normalized path. This lets // us detect cases where foo.js uses define() with a callback that // wants to return a new value for the 'foo' module, but something // inside that callback (probably in some sub-function) references // 'foo' too early. If this happens, we throw an exception when the // callback finishes. The code for that is in define() below: search // for self.pathAccessed . if (!self.pathAccessed[moduleData.uri]) { self.pathAccessed[moduleData.uri] = 0; } self.pathAccessed[moduleData.uri] += 1; if (moduleData.uri in self.modules) { // already loaded: return from cache return self.modules[moduleData.uri]; } return loadFromModuleData(moduleData, moduleName); // adds to cache } function loadFromModuleData(moduleData, moduleName) { // moduleName is passed solely for error messages: by this point, // everything is controlled by moduleData if (!moduleData.uri) { throw new Error("loadFromModuleData with null URI, from basePath " +basePath+" importing ("+moduleName+")"); } // any manifest-based permission checks have already been done let path = moduleData.uri; let moduleContents = self.fs.getFile(path); var sandbox = self.sandboxFactory.createSandbox(moduleContents); self.sandboxes[path] = sandbox; for (let name in self.globals) sandbox.defineProperty(name, self.globals[name]); var api = self._makeApi(path); sandbox.defineProperty('require', api.require); sandbox.defineProperty('define', api.define); if (self.modifyModuleSandbox) self.modifyModuleSandbox(sandbox, moduleContents); /* set up an environment in which module code can use CommonJS patterns like: module.exports = newobj; module.setExports(newobj); if (module.id == "main") stuff(); define("async", function() {return newobj}); */ sandbox.evaluate("var module = {exports: {}};"); sandbox.evaluate("module.setExports = function(obj) {module.exports = obj; return obj;};"); sandbox.evaluate("var exports = module.exports;"); sandbox.evaluate("module.id = '" + path + "';"); var preeval_exports = sandbox.getProperty("exports"); self.modules[path] = sandbox.getProperty("exports"); sandbox.evaluate(moduleContents); // We need to duplicate `exports` as Object.freeze throws an exception // on objects coming from another sandbox. Bug 677768. sandbox.evaluate( "if (typeof module.exports === 'object')\n" + " module.exports = " + " Object.prototype.isPrototypeOf(module.exports) ? " + " Object.freeze(module.exports) : " + " Object.freeze(Object.create(module.exports));"); var posteval_exports = sandbox.getProperty("module").exports; if (posteval_exports !== preeval_exports) { /* if they used module.exports= or module.setExports(), get the new value now. If they used define(), we must be careful to leave self.modules[path] alone, as it will have been modified in the asyncMain() callback-handling code, fired during sandbox.evaluate(). */ if (self.defineUsed[path]) { // you can do one or the other, not both throw new Error("define() was used, so module.exports= and " + "module.setExports() may not be used: " + path); } self.modules[path] = posteval_exports; } return self.modules[path]; } // START support Async module-style require and define calls. // If the only argument to require is a string, then the module that // is represented by that string is fetched for the appropriate context. // // If the first argument is an array, then it will be treated as an array // of dependency string names to fetch. An optional function callback can // be specified to execute when all of those dependencies are available. function asyncRequire(deps, callback) { if (typeof deps === "undefined" && typeof callback === "undefined") { // If we could require() the traceback module here, we could // probably show the source linenumber. But really that should be // part of the stack trace. throw new Error("you must provide a module name when calling require() from "+basePath); } else if (typeof deps === "string" && !callback) { // Just return the module wanted via sync require. return syncRequire(deps); } else { asyncMain(null, basePath, null, deps, callback); return undefined; } } // The function that handles definitions of modules. Differs from // require() in that a string for the module should be the first // argument, and the function to execute after dependencies are loaded // should return a value to define the module corresponding to the first // argument's name. function define (name, deps, callback) { // Only allow one call to define per module/file. if (self.defineUsed[basePath]) { throw new Error("Only one call to define() allowed per file: " + basePath); } else { self.defineUsed[basePath] = true; } // For anonymous modules, the namePath is the basePath var namePath = basePath, exports = {}, exported; // Adjust args if an anonymous module if (typeof name !== 'string') { callback = deps; deps = name; name = null; } // If just a define({}) call (no dependencies), // adjust args accordingly. if (!Array.isArray(deps)) { callback = deps; deps = null; } // If the callback is not an actual function, it means it already // has the definition of the module as a literal value. if (!deps && callback && typeof callback !== 'function') { self.modules[namePath] = callback; return; } // Set the exports value now in case other modules need a handle // on it for cyclical cases. self.modules[namePath] = exports; // Load dependencies and call the module's definition function. exported = asyncMain(name, namePath, exports, deps, callback); // Assign output of function to name, if exports was not // in play (which asyncMain already figured out). if (exported !== undefined) { if (self.pathAccessed[namePath] > 1) { // Another module already accessed the exported value, // need to throw to avoid nasty circular dependency weirdness throw new Error('Module "' + (name || namePath) + '" cannot use ' + 'return from define to define the module ' + 'after another module has referenced its ' + 'exported value.'); } else { self.modules[namePath] = exported; } } } // The function that handles the main async module work, for both // require([], function(){}) calls and define calls. // It makes sure all the dependencies exist before calling the // callback function. It will return the result of the callback // function if "exports" is not a dependency. function asyncMain (name, namePath, exports, deps, callback) { if (typeof deps === 'function') { callback = deps; deps = null; } if (!deps) { deps = []; // The shortened form of the async wrapper for CommonJS modules: // define(function (require, exports, module) {}); // require calls could be inside the function, so toString it // and pull out the dependencies. // Remove comments from the callback string, // look for require calls, and pull them into the dependencies. // The comment regexp is not very robust, but good enough to // avoid commented out require calls and to find normal, sync // require calls in the function. callback .toString() .replace(commentRegExp, "") .replace(cjsRequireRegExp, function (match, dep) { deps.push(dep); }); // Prepend standard require, exports, and module dependencies // (and in that *exact* order per spec), but only add as many as // was asked for via the callback's function argument length. // In particular, do *not* pass exports if it was not asked for. // By asking for exports as a dependency the rest of this // asyncRequire code assumes then that the return value from the // function should not be used as the exported module value. deps = cjsStandardDeps.slice(0, callback.length).concat(deps); } var depModules = [], usesExports = false, exported; // Load all the dependencies, with the "require", "exports" and // "module" ones getting special handling to match the traditional // CommonJS sync module expectations. deps.forEach(function (dep) { if (dep === "require") { depModules.push(asyncRequire); } else if (dep === "module") { depModules.push({ id: name }); } else if (dep === "exports") { usesExports = true; depModules.push(exports); } else { depModules.push(syncRequire(dep)); } }); // Execute the function. if (callback) { exported = callback.apply(null, depModules); } if (exported !== undefined) { if (usesExports) { throw new Error('Inside "' + namePath + '", cannot use exports ' + 'and also return a value from a define ' + 'definition function'); } else { return exported; } } return undefined; }; return { require: asyncRequire, define: define }; // END support for Async module-style }, // This is only really used by unit tests and other // development-related facilities, allowing access to symbols // defined in the global scope of a module. findSandboxForModule: function findSandboxForModule(module) { var path = this.fs.resolveModule(null, module); if (!path) throw new Error('Module "' + module + '" not found'); if (!(path in this.sandboxes)) this.require(module); if (!(path in this.sandboxes)) throw new Error('Internal error: path not in sandboxes: ' + path); return this.sandboxes[path]; }, require: function require(module, callback) { return (this._makeApi(null).require)(module, callback); }, runScript: function runScript(options, extraOutput) { if (typeof(options) == 'string') options = {contents: options}; options = {__proto__: options}; var sandbox = this.sandboxFactory.createSandbox(options); if (extraOutput) extraOutput.sandbox = sandbox; for (let name in this.globals) sandbox.defineProperty(name, this.globals[name]); var api = this._makeApi(null); sandbox.defineProperty('require', api.require); sandbox.defineProperty('define', api.define); return sandbox.evaluate(options); } }; // this is more of a resolver than a filesystem, but test-securable-module // wants to override the getFile() function to avoid using real URIs exports.CompositeFileSystem = function CompositeFileSystem(options) { // We sort file systems in alphabetical order of a package name. this.fses = options.fses.sort(function(a, b) a.root > b.root); this.uriPrefix = options.uriPrefix; this.name = options.name; this.packages = options.metadata || {}; }; function isRelative(path) path.charAt(0) === "." function isNested(path) ~path.indexOf("/") function normalizePath(path) path.substr(-3) === ".js" ? path : path + ".js" function relatifyPath(path) isRelative(path) ? path : "./" + path function getPackageName(path) path.substr(0, path.indexOf("/")) function getInPackagePath(path) path.substr(path.indexOf("/") + 1) function isRelativeTo(path, base) 0 === path.indexOf(base) function resolveTo(path, base) "." + path.substr(base.length) exports.CompositeFileSystem.prototype = { getPackageURI: function getPackageURI(name) { let uri = this.uriPrefix + name + "-lib/"; return ios.newURI(uri, null, null).spec; }, resolveModule: function resolveModule(base, path) { // If it is relative path we don't need to search anything // as it should be module from the same package. if (isRelative(path)) { // If base is not provided then it's a main module with a relative // path to we use `packageURI` as a base to resolve. base = base || this.getPackageURI(this.name); return this.resolveRelative(base, path); } // If path contains only one part then we treat if it as // require(PCKG/{{(package.json).main}} if (!isNested(path)) return this.resolveMain(path) || this.searchModule(path); // If path contains more then one part than we try to interpret that // as `require(PCKG/module)` first and fall back to search. return this.resolveModuleFromPackage(path) || this.searchModule(path); }, resolveRelative: function resolveRelative(base, path) { path = normalizePath(path); let uri = ios.newURI(path, null, ios.newURI(base, null, null)); try { let channel = ios.newChannelFromURI(uri); channel.open().close(); } catch (e) { return null; } return uri.spec; }, searchModule: function seachModule(path) { for each (let fs in this.fses) { let id = fs.resolveModule(null, path); if (id) return id; } return null; }, resolveModuleFromPackage: function resolveModuleFromPackage(path) { let name = getPackageName(path); if (name in this.packages) { let base = this.getPackageURI(name); return this.resolveRelative(base, getInPackagePath(path)); } return null; }, resolveMain: function resolveMain(name) { if (name in this.packages) { let base = this.getPackageURI(name); let path = relatifyPath(this.packages[name].main || "main"); // We need to make sure to strip out directory from the main if it // contains "lib" part. Unfortunately if main out of the lib folder // requiring main module will fail as it will be out of the mapped // resource URI. let dirs = this.packages[name].directories; let lib = relatifyPath(dirs ? dirs.lib || "./lib" : "./lib"); if (isRelativeTo(path, lib)) path = resolveTo(path, lib); return this.resolveRelative(base, path); } return null; }, getFile: function getFile(path) { return loadFile(path); } }; exports.LocalFileSystem = function LocalFileSystem(root) { if (root === undefined) { if (!baseURI) throw new Error("Need a root path for module filesystem"); root = baseURI; } if (typeof(root) == 'string') root = ios.newURI(root, null, baseURI); if (root instanceof Ci.nsIFile) root = ios.newFileURI(root); if (!(root instanceof Ci.nsIURI)) throw new Error('Expected nsIFile, nsIURI, or string for root'); this.root = root.spec; this._rootURI = root; this._rootURIDir = getRootDir(root.spec); }; exports.LocalFileSystem.prototype = { resolveModule: function resolveModule(base, path) { path = normalizePath(path); var baseURI; if (!base || path.charAt(0) != '.') baseURI = this._rootURI; else baseURI = ios.newURI(base, null, null); var newURI = ios.newURI(path, null, baseURI); if (newURI.spec.indexOf(this._rootURIDir) == 0) { var channel = ios.newChannelFromURI(newURI); try { channel.open().close(); } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { return null; } return newURI.spec; } return null; }, getFile: function getFile(path) { return loadFile(path); } }; function loadFile(path) { var channel = ios.newChannel(path, null, null); var iStream = channel.open(); var ciStream = Cc["@mozilla.org/intl/converter-input-stream;1"]. createInstance(Ci.nsIConverterInputStream); var bufLen = 0x8000; ciStream.init(iStream, "UTF-8", bufLen, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); var chunk = {}; var data = ""; while (ciStream.readString(bufLen, chunk) > 0) data += chunk.value; ciStream.close(); iStream.close(); return {contents: data, filename: path}; }; if (global.window) { // We're being loaded in a chrome window, or a web page with // UniversalXPConnect privileges. global.SecurableModule = exports; } else if (global.exports) { // We're being loaded in a SecurableModule. for (let name in exports) { global.exports[name] = exports[name]; } } else { // We're being loaded in a JS module. global.EXPORTED_SYMBOLS = []; for (let name in exports) { global.EXPORTED_SYMBOLS.push(name); global[name] = exports[name]; } } })(this);