diff options
Diffstat (limited to 'js/xpconnect/loader')
-rw-r--r-- | js/xpconnect/loader/ISO8601DateUtils.jsm | 144 | ||||
-rw-r--r-- | js/xpconnect/loader/XPCOMUtils.jsm | 471 | ||||
-rw-r--r-- | js/xpconnect/loader/moz.build | 28 | ||||
-rw-r--r-- | js/xpconnect/loader/mozJSComponentLoader.cpp | 1437 | ||||
-rw-r--r-- | js/xpconnect/loader/mozJSComponentLoader.h | 161 | ||||
-rw-r--r-- | js/xpconnect/loader/mozJSLoaderUtils.cpp | 85 | ||||
-rw-r--r-- | js/xpconnect/loader/mozJSLoaderUtils.h | 37 | ||||
-rw-r--r-- | js/xpconnect/loader/mozJSSubScriptLoader.cpp | 929 | ||||
-rw-r--r-- | js/xpconnect/loader/mozJSSubScriptLoader.h | 53 |
9 files changed, 3345 insertions, 0 deletions
diff --git a/js/xpconnect/loader/ISO8601DateUtils.jsm b/js/xpconnect/loader/ISO8601DateUtils.jsm new file mode 100644 index 000000000..3eff78501 --- /dev/null +++ b/js/xpconnect/loader/ISO8601DateUtils.jsm @@ -0,0 +1,144 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 HOURS_TO_MINUTES = 60; +const MINUTES_TO_SECONDS = 60; +const SECONDS_TO_MILLISECONDS = 1000; +const MINUTES_TO_MILLISECONDS = MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS; +const HOURS_TO_MILLISECONDS = HOURS_TO_MINUTES * MINUTES_TO_MILLISECONDS; + +this.EXPORTED_SYMBOLS = ["ISO8601DateUtils"]; + +debug("*** loading ISO8601DateUtils\n"); + +this.ISO8601DateUtils = { + + /** + * XXX Thunderbird's W3C-DTF function + * + * Converts a W3C-DTF (subset of ISO 8601) date string to a Javascript + * date object. W3C-DTF is described in this note: + * http://www.w3.org/TR/NOTE-datetime IETF is obtained via the Date + * object's toUTCString() method. The object's toString() method is + * insufficient because it spells out timezones on Win32 + * (f.e. "Pacific Standard Time" instead of "PST"), which Mail doesn't + * grok. For info, see + * http://lxr.mozilla.org/mozilla/source/js/src/jsdate.c#1526. + */ + parse: function ISO8601_parse(aDateString) { + var dateString = aDateString; + if (!dateString.match('-')) { + // Workaround for server sending + // dates such as: 20030530T11:18:50-08:00 + // instead of: 2003-05-30T11:18:50-08:00 + var year = dateString.slice(0, 4); + var month = dateString.slice(4, 6); + var rest = dateString.slice(6, dateString.length); + dateString = year + "-" + month + "-" + rest; + } + + var parts = dateString.match(/(\d{4})(-(\d{2,3}))?(-(\d{2}))?(T(\d{2}):(\d{2})(:(\d{2})(\.(\d+))?)?(Z|([+-])(\d{2}):(\d{2}))?)?/); + + // Here's an example of a W3C-DTF date string and what .match returns for it. + // + // date: 2003-05-30T11:18:50.345-08:00 + // date.match returns array values: + // + // 0: 2003-05-30T11:18:50-08:00, + // 1: 2003, + // 2: -05, + // 3: 05, + // 4: -30, + // 5: 30, + // 6: T11:18:50-08:00, + // 7: 11, + // 8: 18, + // 9: :50, + // 10: 50, + // 11: .345, + // 12: 345, + // 13: -08:00, + // 14: -, + // 15: 08, + // 16: 00 + + // Create a Date object from the date parts. Note that the Date + // object apparently can't deal with empty string parameters in lieu + // of numbers, so optional values (like hours, minutes, seconds, and + // milliseconds) must be forced to be numbers. + var date = new Date(parts[1], parts[3] - 1, parts[5], parts[7] || 0, + parts[8] || 0, parts[10] || 0, parts[12] || 0); + + // We now have a value that the Date object thinks is in the local + // timezone but which actually represents the date/time in the + // remote timezone (f.e. the value was "10:00 EST", and we have + // converted it to "10:00 PST" instead of "07:00 PST"). We need to + // correct that. To do so, we're going to add the offset between + // the remote timezone and UTC (to convert the value to UTC), then + // add the offset between UTC and the local timezone //(to convert + // the value to the local timezone). + + // Ironically, W3C-DTF gives us the offset between UTC and the + // remote timezone rather than the other way around, while the + // getTimezoneOffset() method of a Date object gives us the offset + // between the local timezone and UTC rather than the other way + // around. Both of these are the additive inverse (i.e. -x for x) + // of what we want, so we have to invert them to use them by + // multipying by -1 (f.e. if "the offset between UTC and the remote + // timezone" is -5 hours, then "the offset between the remote + // timezone and UTC" is -5*-1 = 5 hours). + + // Note that if the timezone portion of the date/time string is + // absent (which violates W3C-DTF, although ISO 8601 allows it), we + // assume the value to be in UTC. + + // The offset between the remote timezone and UTC in milliseconds. + var remoteToUTCOffset = 0; + if (parts[13] && parts[13] != "Z") { + var direction = (parts[14] == "+" ? 1 : -1); + if (parts[15]) + remoteToUTCOffset += direction * parts[15] * HOURS_TO_MILLISECONDS; + if (parts[16]) + remoteToUTCOffset += direction * parts[16] * MINUTES_TO_MILLISECONDS; + } + remoteToUTCOffset = remoteToUTCOffset * -1; // invert it + + // The offset between UTC and the local timezone in milliseconds. + var UTCToLocalOffset = date.getTimezoneOffset() * MINUTES_TO_MILLISECONDS; + UTCToLocalOffset = UTCToLocalOffset * -1; // invert it + date.setTime(date.getTime() + remoteToUTCOffset + UTCToLocalOffset); + + return date; + }, + + create: function ISO8601_create(aDate) { + function zeropad (s, l) { + s = s.toString(); // force it to a string + while (s.length < l) { + s = '0' + s; + } + return s; + } + + var myDate; + // if d is a number, turn it into a date + if (typeof aDate == 'number') { + myDate = new Date() + myDate.setTime(aDate); + } else { + myDate = aDate; + } + + // YYYY-MM-DDThh:mm:ssZ + var result = zeropad(myDate.getUTCFullYear (), 4) + + zeropad(myDate.getUTCMonth () + 1, 2) + + zeropad(myDate.getUTCDate (), 2) + 'T' + + zeropad(myDate.getUTCHours (), 2) + ':' + + zeropad(myDate.getUTCMinutes (), 2) + ':' + + zeropad(myDate.getUTCSeconds (), 2) + 'Z'; + + return result; + } +} diff --git a/js/xpconnect/loader/XPCOMUtils.jsm b/js/xpconnect/loader/XPCOMUtils.jsm new file mode 100644 index 000000000..eb35258de --- /dev/null +++ b/js/xpconnect/loader/XPCOMUtils.jsm @@ -0,0 +1,471 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et filetype=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/. */ + +/** + * Utilities for JavaScript components loaded by the JS component + * loader. + * + * Import into a JS component using + * 'Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");' + * + * Exposing a JS 'class' as a component using these utility methods consists + * of several steps: + * 0. Import XPCOMUtils, as described above. + * 1. Declare the 'class' (or multiple classes) implementing the component(s): + * function MyComponent() { + * // constructor + * } + * MyComponent.prototype = { + * // properties required for XPCOM registration: + * classID: Components.ID("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"), + * + * // [optional] custom factory (an object implementing nsIFactory). If not + * // provided, the default factory is used, which returns + * // |(new MyComponent()).QueryInterface(iid)| in its createInstance(). + * _xpcom_factory: { ... }, + * + * // QueryInterface implementation, e.g. using the generateQI helper + * QueryInterface: XPCOMUtils.generateQI( + * [Components.interfaces.nsIObserver, + * Components.interfaces.nsIMyInterface, + * "nsIFoo", + * "nsIBar" ]), + * + * // [optional] classInfo implementation, e.g. using the generateCI helper. + * // Will be automatically returned from QueryInterface if that was + * // generated with the generateQI helper. + * classInfo: XPCOMUtils.generateCI( + * {classID: Components.ID("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"), + * contractID: "@example.com/xxx;1", + * classDescription: "unique text description", + * interfaces: [Components.interfaces.nsIObserver, + * Components.interfaces.nsIMyInterface, + * "nsIFoo", + * "nsIBar"], + * flags: Ci.nsIClassInfo.SINGLETON}), + * + * // The following properties were used prior to Mozilla 2, but are no + * // longer supported. They may still be included for compatibility with + * // prior versions of XPCOMUtils. In Mozilla 2, this information is + * // included in the .manifest file which registers this JS component. + * classDescription: "unique text description", + * contractID: "@example.com/xxx;1", + * + * // [optional] an array of categories to register this component in. + * _xpcom_categories: [{ + * // Each object in the array specifies the parameters to pass to + * // nsICategoryManager.addCategoryEntry(). 'true' is passed for + * // both aPersist and aReplace params. + * category: "some-category", + * // optional, defaults to the object's classDescription + * entry: "entry name", + * // optional, defaults to the object's contractID (unless + * // 'service' is specified) + * value: "...", + * // optional, defaults to false. When set to true, and only if 'value' + * // is not specified, the concatenation of the string "service," and the + * // object's contractID is passed as aValue parameter of addCategoryEntry. + * service: true, + * // optional, it can be an array of applications' IDs in the form: + * // [ "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}", ... ] + * // If defined the component will be registered in this category only for + * // the provided applications. + * apps: [...] + * }], + * + * // ...component implementation... + * }; + * + * 2. Create an array of component constructors (like the one + * created in step 1): + * var components = [MyComponent]; + * + * 3. Define the NSGetFactory entry point: + * this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); + */ + + +this.EXPORTED_SYMBOLS = [ "XPCOMUtils" ]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +this.XPCOMUtils = { + /** + * Generate a QueryInterface implementation. The returned function must be + * assigned to the 'QueryInterface' property of a JS object. When invoked on + * that object, it checks if the given iid is listed in the |interfaces| + * param, and if it is, returns |this| (the object it was called on). + * If the JS object has a classInfo property it'll be returned for the + * nsIClassInfo IID, generateCI can be used to generate the classInfo + * property. + */ + generateQI: function XPCU_generateQI(interfaces) { + /* Note that Ci[Ci.x] == Ci.x for all x */ + let a = []; + if (interfaces) { + for (let i = 0; i < interfaces.length; i++) { + let iface = interfaces[i]; + if (Ci[iface]) { + a.push(Ci[iface].name); + } + } + } + return makeQI(a); + }, + + /** + * Generate a ClassInfo implementation for a component. The returned object + * must be assigned to the 'classInfo' property of a JS object. The first and + * only argument should be an object that contains a number of optional + * properties: "interfaces", "contractID", "classDescription", "classID" and + * "flags". The values of the properties will be returned as the values of the + * various properties of the nsIClassInfo implementation. + */ + generateCI: function XPCU_generateCI(classInfo) + { + if (QueryInterface in classInfo) + throw Error("In generateCI, don't use a component for generating classInfo"); + /* Note that Ci[Ci.x] == Ci.x for all x */ + let _interfaces = []; + for (let i = 0; i < classInfo.interfaces.length; i++) { + let iface = classInfo.interfaces[i]; + if (Ci[iface]) { + _interfaces.push(Ci[iface]); + } + } + return { + getInterfaces: function XPCU_getInterfaces(countRef) { + countRef.value = _interfaces.length; + return _interfaces; + }, + getScriptableHelper: function XPCU_getScriptableHelper() { + return null; + }, + contractID: classInfo.contractID, + classDescription: classInfo.classDescription, + classID: classInfo.classID, + flags: classInfo.flags, + QueryInterface: this.generateQI([Ci.nsIClassInfo]) + }; + }, + + /** + * Generate a NSGetFactory function given an array of components. + */ + generateNSGetFactory: function XPCU_generateNSGetFactory(componentsArray) { + let classes = {}; + for (let i = 0; i < componentsArray.length; i++) { + let component = componentsArray[i]; + if (!(component.prototype.classID instanceof Components.ID)) + throw Error("In generateNSGetFactory, classID missing or incorrect for component " + component); + + classes[component.prototype.classID] = this._getFactory(component); + } + return function NSGetFactory(cid) { + let cidstring = cid.toString(); + if (cidstring in classes) + return classes[cidstring]; + throw Cr.NS_ERROR_FACTORY_NOT_REGISTERED; + } + }, + + /** + * Defines a getter on a specified object that will be created upon first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject. + * @param aLambda + * A function that returns what the getter should return. This will + * only ever be called once. + */ + defineLazyGetter: function XPCU_defineLazyGetter(aObject, aName, aLambda) + { + Object.defineProperty(aObject, aName, { + get: function () { + // Redefine this accessor property as a data property. + // Delete it first, to rule out "too much recursion" in case aObject is + // a proxy whose defineProperty handler might unwittingly trigger this + // getter again. + delete aObject[aName]; + let value = aLambda.apply(aObject); + Object.defineProperty(aObject, aName, { + value, + writable: true, + configurable: true, + enumerable: true + }); + return value; + }, + configurable: true, + enumerable: true + }); + }, + + /** + * Defines a getter on a specified object for a service. The service will not + * be obtained until first use. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject for the service. + * @param aContract + * The contract used to obtain the service. + * @param aInterfaceName + * The name of the interface to query the service to. + */ + defineLazyServiceGetter: function XPCU_defineLazyServiceGetter(aObject, aName, + aContract, + aInterfaceName) + { + this.defineLazyGetter(aObject, aName, function XPCU_serviceLambda() { + return Cc[aContract].getService(Ci[aInterfaceName]); + }); + }, + + /** + * Defines a getter on a specified object for a module. The module will not + * be imported until first use. The getter allows to execute setup and + * teardown code (e.g. to register/unregister to services) and accepts + * a proxy object which acts on behalf of the module until it is imported. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter to define on aObject for the module. + * @param aResource + * The URL used to obtain the module. + * @param aSymbol + * The name of the symbol exported by the module. + * This parameter is optional and defaults to aName. + * @param aPreLambda + * A function that is executed when the proxy is set up. + * This will only ever be called once. + * @param aPostLambda + * A function that is executed when the module has been imported to + * run optional teardown procedures on the proxy object. + * This will only ever be called once. + * @param aProxy + * An object which acts on behalf of the module to be imported until + * the module has been imported. + */ + defineLazyModuleGetter: function XPCU_defineLazyModuleGetter( + aObject, aName, aResource, aSymbol, + aPreLambda, aPostLambda, aProxy) + { + let proxy = aProxy || {}; + + if (typeof(aPreLambda) === "function") { + aPreLambda.apply(proxy); + } + + this.defineLazyGetter(aObject, aName, function XPCU_moduleLambda() { + var temp = {}; + try { + Cu.import(aResource, temp); + + if (typeof(aPostLambda) === "function") { + aPostLambda.apply(proxy); + } + } catch (ex) { + Cu.reportError("Failed to load module " + aResource + "."); + throw ex; + } + return temp[aSymbol || aName]; + }); + }, + + /** + * Defines a getter on a specified object for preference value. The + * preference is read the first time that the property is accessed, + * and is thereafter kept up-to-date using a preference observer. + * + * @param aObject + * The object to define the lazy getter on. + * @param aName + * The name of the getter property to define on aObject. + * @param aPreference + * The name of the preference to read. + * @param aDefaultValue + * The default value to use, if the preference is not defined. + */ + defineLazyPreferenceGetter: function XPCU_defineLazyPreferenceGetter( + aObject, aName, aPreference, aDefaultValue = null) + { + // Note: We need to keep a reference to this observer alive as long + // as aObject is alive. This means that all of our getters need to + // explicitly close over the variable that holds the object, and we + // cannot define a value in place of a getter after we read the + // preference. + let observer = { + QueryInterface: this.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + value: undefined, + + observe(subject, topic, data) { + if (data == aPreference) { + this.value = undefined; + } + }, + } + + let defineGetter = get => { + Object.defineProperty(aObject, aName, { + configurable: true, + enumerable: true, + get, + }); + }; + + function lazyGetter() { + if (observer.value === undefined) { + observer.value = Preferences.get(aPreference, aDefaultValue); + } + return observer.value; + } + + defineGetter(() => { + Services.prefs.addObserver(aPreference, observer, true); + + defineGetter(lazyGetter); + return lazyGetter(); + }); + }, + + /** + * Helper which iterates over a nsISimpleEnumerator. + * @param e The nsISimpleEnumerator to iterate over. + * @param i The expected interface for each element. + */ + IterSimpleEnumerator: function* XPCU_IterSimpleEnumerator(e, i) + { + while (e.hasMoreElements()) + yield e.getNext().QueryInterface(i); + }, + + /** + * Helper which iterates over a string enumerator. + * @param e The string enumerator (nsIUTF8StringEnumerator or + * nsIStringEnumerator) over which to iterate. + */ + IterStringEnumerator: function* XPCU_IterStringEnumerator(e) + { + while (e.hasMore()) + yield e.getNext(); + }, + + /** + * Helper which iterates over the entries in a category. + * @param aCategory The name of the category over which to iterate. + */ + enumerateCategoryEntries: function* XPCOMUtils_enumerateCategoryEntries(aCategory) + { + let category = this.categoryManager.enumerateCategory(aCategory); + for (let entry of this.IterSimpleEnumerator(category, Ci.nsISupportsCString)) { + yield [entry.data, this.categoryManager.getCategoryEntry(aCategory, entry.data)]; + } + }, + + /** + * Returns an nsIFactory for |component|. + */ + _getFactory: function XPCOMUtils__getFactory(component) { + var factory = component.prototype._xpcom_factory; + if (!factory) { + factory = { + createInstance: function(outer, iid) { + if (outer) + throw Cr.NS_ERROR_NO_AGGREGATION; + return (new component()).QueryInterface(iid); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) + } + } + return factory; + }, + + /** + * Allows you to fake a relative import. Expects the global object from the + * module that's calling us, and the relative filename that we wish to import. + */ + importRelative: function XPCOMUtils__importRelative(that, path, scope) { + if (!("__URI__" in that)) + throw Error("importRelative may only be used from a JSM, and its first argument "+ + "must be that JSM's global object (hint: use this)"); + let uri = that.__URI__; + let i = uri.lastIndexOf("/"); + Components.utils.import(uri.substring(0, i+1) + path, scope || that); + }, + + /** + * generates a singleton nsIFactory implementation that can be used as + * the _xpcom_factory of the component. + * @param aServiceConstructor + * Constructor function of the component. + */ + generateSingletonFactory: + function XPCOMUtils_generateSingletonFactory(aServiceConstructor) { + return { + _instance: null, + createInstance: function XPCU_SF_createInstance(aOuter, aIID) { + if (aOuter !== null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + if (this._instance === null) { + this._instance = new aServiceConstructor(); + } + return this._instance.QueryInterface(aIID); + }, + lockFactory: function XPCU_SF_lockFactory(aDoLock) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) + }; + }, + + /** + * Defines a non-writable property on an object. + */ + defineConstant: function XPCOMUtils__defineConstant(aObj, aName, aValue) { + Object.defineProperty(aObj, aName, { + value: aValue, + enumerable: true, + writable: false + }); + }, +}; + +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyServiceGetter(XPCOMUtils, "categoryManager", + "@mozilla.org/categorymanager;1", + "nsICategoryManager"); + +/** + * Helper for XPCOMUtils.generateQI to avoid leaks - see bug 381651#c1 + */ +function makeQI(interfaceNames) { + return function XPCOMUtils_QueryInterface(iid) { + if (iid.equals(Ci.nsISupports)) + return this; + if (iid.equals(Ci.nsIClassInfo) && "classInfo" in this) + return this.classInfo; + for (let i = 0; i < interfaceNames.length; i++) { + if (Ci[interfaceNames[i]].equals(iid)) + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }; +} diff --git a/js/xpconnect/loader/moz.build b/js/xpconnect/loader/moz.build new file mode 100644 index 000000000..3dc03d6db --- /dev/null +++ b/js/xpconnect/loader/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# These files cannot be built in unified mode because they rely on plarena.h +SOURCES += [ + 'mozJSComponentLoader.cpp', + 'mozJSLoaderUtils.cpp', + 'mozJSSubScriptLoader.cpp', +] + +EXTRA_JS_MODULES += [ + 'ISO8601DateUtils.jsm', + 'XPCOMUtils.jsm', +] + +FINAL_LIBRARY = 'xul' + +LOCAL_INCLUDES += [ + '../src', + '../wrappers', + '/dom/base', +] + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-shadow'] diff --git a/js/xpconnect/loader/mozJSComponentLoader.cpp b/js/xpconnect/loader/mozJSComponentLoader.cpp new file mode 100644 index 000000000..95c214867 --- /dev/null +++ b/js/xpconnect/loader/mozJSComponentLoader.cpp @@ -0,0 +1,1437 @@ + +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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/. */ + +#include "mozilla/Attributes.h" + +#include <cstdarg> + +#include "mozilla/Logging.h" +#ifdef ANDROID +#include <android/log.h> +#endif +#ifdef XP_WIN +#include <windows.h> +#endif + +#include "jsapi.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" +#include "nsIComponentManager.h" +#include "mozilla/Module.h" +#include "nsIFile.h" +#include "mozJSComponentLoader.h" +#include "mozJSLoaderUtils.h" +#include "nsIXPConnect.h" +#include "nsIObserverService.h" +#include "nsIScriptSecurityManager.h" +#include "nsIFileURL.h" +#include "nsIJARURI.h" +#include "nsNetUtil.h" +#include "jsprf.h" +#include "nsJSPrincipals.h" +#include "nsJSUtils.h" +#include "xpcprivate.h" +#include "xpcpublic.h" +#include "nsContentUtils.h" +#include "nsXULAppAPI.h" +#include "WrapperFactory.h" + +#include "mozilla/AddonPathService.h" +#include "mozilla/scache/StartupCache.h" +#include "mozilla/scache/StartupCacheUtils.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::scache; +using namespace xpc; +using namespace JS; + +// This JSClass exists to trick silly code that expects toString()ing the +// global in a component scope to return something with "BackstagePass" in it +// to continue working. +static const JSClass kFakeBackstagePassJSClass = { "FakeBackstagePass" }; + +static const char kXPConnectServiceContractID[] = "@mozilla.org/js/xpc/XPConnect;1"; +static const char kObserverServiceContractID[] = "@mozilla.org/observer-service;1"; +static const char kJSCachePrefix[] = "jsloader"; + +#define HAVE_PR_MEMMAP + +/** + * Buffer sizes for serialization and deserialization of scripts. + * FIXME: bug #411579 (tune this macro!) Last updated: Jan 2008 + */ +#define XPC_SERIALIZATION_BUFFER_SIZE (64 * 1024) +#define XPC_DESERIALIZATION_BUFFER_SIZE (12 * 8192) + +// MOZ_LOG=JSComponentLoader:5 +static LazyLogModule gJSCLLog("JSComponentLoader"); + +#define LOG(args) MOZ_LOG(gJSCLLog, mozilla::LogLevel::Debug, args) + +// Components.utils.import error messages +#define ERROR_SCOPE_OBJ "%s - Second argument must be an object." +#define ERROR_NOT_PRESENT "%s - EXPORTED_SYMBOLS is not present." +#define ERROR_NOT_AN_ARRAY "%s - EXPORTED_SYMBOLS is not an array." +#define ERROR_GETTING_ARRAY_LENGTH "%s - Error getting array length of EXPORTED_SYMBOLS." +#define ERROR_ARRAY_ELEMENT "%s - EXPORTED_SYMBOLS[%d] is not a string." +#define ERROR_GETTING_SYMBOL "%s - Could not get symbol '%s'." +#define ERROR_SETTING_SYMBOL "%s - Could not set symbol '%s' on target object." + +static bool +Dump(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) + return true; + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) + return false; + + JSAutoByteString utf8str; + if (!utf8str.encodeUtf8(cx, str)) + return false; + +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", utf8str.ptr()); +#endif +#ifdef XP_WIN + if (IsDebuggerPresent()) { + nsAutoJSString wstr; + if (!wstr.init(cx, str)) + return false; + OutputDebugStringW(wstr.get()); + } +#endif + fputs(utf8str.ptr(), stdout); + fflush(stdout); + return true; +} + +static bool +Debug(JSContext* cx, unsigned argc, Value* vp) +{ +#ifdef DEBUG + return Dump(cx, argc, vp); +#else + return true; +#endif +} + +static const JSFunctionSpec gGlobalFun[] = { + JS_FS("dump", Dump, 1,0), + JS_FS("debug", Debug, 1,0), + JS_FS("atob", Atob, 1,0), + JS_FS("btoa", Btoa, 1,0), + JS_FS_END +}; + +class MOZ_STACK_CLASS JSCLContextHelper +{ +public: + explicit JSCLContextHelper(JSContext* aCx); + ~JSCLContextHelper(); + + void reportErrorAfterPop(char* buf); + +private: + JSContext* mContext; + char* mBuf; + + // prevent copying and assignment + JSCLContextHelper(const JSCLContextHelper&) = delete; + const JSCLContextHelper& operator=(const JSCLContextHelper&) = delete; +}; + +static nsresult +MOZ_FORMAT_PRINTF(2, 3) +ReportOnCallerUTF8(JSContext* callerContext, + const char* format, ...) { + if (!callerContext) { + return NS_ERROR_FAILURE; + } + + va_list ap; + va_start(ap, format); + + char* buf = JS_vsmprintf(format, ap); + if (!buf) { + va_end(ap); + return NS_ERROR_OUT_OF_MEMORY; + } + + JS_ReportErrorUTF8(callerContext, "%s", buf); + JS_smprintf_free(buf); + + va_end(ap); + return NS_OK; +} + +static nsresult +MOZ_FORMAT_PRINTF(2, 3) +ReportOnCallerUTF8(JSCLContextHelper& helper, + const char* format, ...) +{ + va_list ap; + va_start(ap, format); + + char* buf = JS_vsmprintf(format, ap); + if (!buf) { + va_end(ap); + return NS_ERROR_OUT_OF_MEMORY; + } + + helper.reportErrorAfterPop(buf); + va_end(ap); + return NS_OK; +} + +mozJSComponentLoader::mozJSComponentLoader() + : mModules(16), + mImports(16), + mInProgressImports(16), + mInitialized(false), + mReuseLoaderGlobal(false) +{ + MOZ_ASSERT(!sSelf, "mozJSComponentLoader should be a singleton"); + + sSelf = this; +} + +#define ENSURE_DEP(name) { nsresult rv = Ensure##name(); NS_ENSURE_SUCCESS(rv, rv); } +#define ENSURE_DEPS(...) MOZ_FOR_EACH(ENSURE_DEP, (), (__VA_ARGS__)); +#define BEGIN_ENSURE(self, ...) { \ + if (m##self) \ + return NS_OK; \ + ENSURE_DEPS(__VA_ARGS__); \ +} + +class MOZ_STACK_CLASS ComponentLoaderInfo { + public: + explicit ComponentLoaderInfo(const nsACString& aLocation) : mLocation(aLocation) {} + + nsIIOService* IOService() { MOZ_ASSERT(mIOService); return mIOService; } + nsresult EnsureIOService() { + if (mIOService) + return NS_OK; + nsresult rv; + mIOService = do_GetIOService(&rv); + return rv; + } + + nsIURI* URI() { MOZ_ASSERT(mURI); return mURI; } + nsresult EnsureURI() { + BEGIN_ENSURE(URI, IOService); + return mIOService->NewURI(mLocation, nullptr, nullptr, getter_AddRefs(mURI)); + } + + nsIChannel* ScriptChannel() { MOZ_ASSERT(mScriptChannel); return mScriptChannel; } + nsresult EnsureScriptChannel() { + BEGIN_ENSURE(ScriptChannel, IOService, URI); + return NS_NewChannel(getter_AddRefs(mScriptChannel), + mURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_SCRIPT, + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, + mIOService); + } + + nsIURI* ResolvedURI() { MOZ_ASSERT(mResolvedURI); return mResolvedURI; } + nsresult EnsureResolvedURI() { + BEGIN_ENSURE(ResolvedURI, ScriptChannel); + return mScriptChannel->GetURI(getter_AddRefs(mResolvedURI)); + } + + nsAutoCString& Key() { return *mKey; } + nsresult EnsureKey() { + ENSURE_DEPS(ResolvedURI); + mKey.emplace(); + return mResolvedURI->GetSpec(*mKey); + } + + MOZ_MUST_USE nsresult GetLocation(nsCString& aLocation) { + nsresult rv = EnsureURI(); + NS_ENSURE_SUCCESS(rv, rv); + return mURI->GetSpec(aLocation); + } + + private: + const nsACString& mLocation; + nsCOMPtr<nsIIOService> mIOService; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIChannel> mScriptChannel; + nsCOMPtr<nsIURI> mResolvedURI; + Maybe<nsAutoCString> mKey; // This is safe because we're MOZ_STACK_CLASS +}; + +#undef BEGIN_ENSURE +#undef ENSURE_DEPS +#undef ENSURE_DEP + +mozJSComponentLoader::~mozJSComponentLoader() +{ + if (mInitialized) { + NS_ERROR("'xpcom-shutdown-loaders' was not fired before cleaning up mozJSComponentLoader"); + UnloadModules(); + } + + sSelf = nullptr; +} + +mozJSComponentLoader* +mozJSComponentLoader::sSelf; + +NS_IMPL_ISUPPORTS(mozJSComponentLoader, + mozilla::ModuleLoader, + xpcIJSModuleLoader, + nsIObserver) + +nsresult +mozJSComponentLoader::ReallyInit() +{ + nsresult rv; + + mReuseLoaderGlobal = Preferences::GetBool("jsloader.reuseGlobal"); + + // XXXkhuey B2G child processes have some sort of preferences race that + // results in getting the wrong value. + // But we don't want that on Firefox Mulet as it break most Firefox JSMs... + // Also disable on debug builds to break js components that rely on this. +#if defined(MOZ_B2G) && !defined(MOZ_MULET) && !defined(DEBUG) + mReuseLoaderGlobal = true; +#endif + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID); + if (!secman) + return NS_ERROR_FAILURE; + + rv = secman->GetSystemPrincipal(getter_AddRefs(mSystemPrincipal)); + if (NS_FAILED(rv) || !mSystemPrincipal) + return NS_ERROR_FAILURE; + + nsCOMPtr<nsIObserverService> obsSvc = + do_GetService(kObserverServiceContractID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->AddObserver(this, "xpcom-shutdown-loaders", false); + NS_ENSURE_SUCCESS(rv, rv); + + mInitialized = true; + + return NS_OK; +} + +// For terrible compatibility reasons, we need to consider both the global +// lexical environment and the global of modules when searching for exported +// symbols. +static JSObject* +ResolveModuleObjectProperty(JSContext* aCx, HandleObject aModObj, const char* name) +{ + if (JS_HasExtensibleLexicalEnvironment(aModObj)) { + RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj)); + bool found; + if (!JS_HasOwnProperty(aCx, lexical, name, &found)) { + return nullptr; + } + if (found) { + return lexical; + } + } + return aModObj; +} + +const mozilla::Module* +mozJSComponentLoader::LoadModule(FileLocation& aFile) +{ + if (!NS_IsMainThread()) { + MOZ_ASSERT(false, "Don't use JS components off the main thread"); + return nullptr; + } + + nsCOMPtr<nsIFile> file = aFile.GetBaseFile(); + + nsCString spec; + aFile.GetURIString(spec); + ComponentLoaderInfo info(spec); + nsresult rv = info.EnsureURI(); + NS_ENSURE_SUCCESS(rv, nullptr); + + if (!mInitialized) { + rv = ReallyInit(); + if (NS_FAILED(rv)) + return nullptr; + } + + ModuleEntry* mod; + if (mModules.Get(spec, &mod)) + return mod; + + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + nsAutoPtr<ModuleEntry> entry(new ModuleEntry(RootingContext::get(cx))); + RootedValue dummy(cx); + rv = ObjectForLocation(info, file, &entry->obj, &entry->thisObjectKey, + &entry->location, false, &dummy); + if (NS_FAILED(rv)) { + return nullptr; + } + + nsCOMPtr<nsIXPConnect> xpc = do_GetService(kXPConnectServiceContractID, + &rv); + if (NS_FAILED(rv)) + return nullptr; + + nsCOMPtr<nsIComponentManager> cm; + rv = NS_GetComponentManager(getter_AddRefs(cm)); + if (NS_FAILED(rv)) + return nullptr; + + JSAutoCompartment ac(cx, entry->obj); + RootedObject entryObj(cx, entry->obj); + + RootedObject NSGetFactoryHolder(cx, ResolveModuleObjectProperty(cx, entryObj, "NSGetFactory")); + RootedValue NSGetFactory_val(cx); + if (!NSGetFactoryHolder || + !JS_GetProperty(cx, NSGetFactoryHolder, "NSGetFactory", &NSGetFactory_val) || + NSGetFactory_val.isUndefined()) + { + return nullptr; + } + + if (JS_TypeOfValue(cx, NSGetFactory_val) != JSTYPE_FUNCTION) { + /* + * spec's encoding is ASCII unless it's zip file, otherwise it's + * random encoding. Latin1 variant is safe for random encoding. + */ + JS_ReportErrorLatin1(cx, "%s has NSGetFactory property that is not a function", + spec.get()); + return nullptr; + } + + RootedObject jsGetFactoryObj(cx); + if (!JS_ValueToObject(cx, NSGetFactory_val, &jsGetFactoryObj) || + !jsGetFactoryObj) { + /* XXX report error properly */ + return nullptr; + } + + rv = xpc->WrapJS(cx, jsGetFactoryObj, + NS_GET_IID(xpcIJSGetFactory), getter_AddRefs(entry->getfactoryobj)); + if (NS_FAILED(rv)) { + /* XXX report error properly */ +#ifdef DEBUG + fprintf(stderr, "mJCL: couldn't get nsIModule from jsval\n"); +#endif + return nullptr; + } + + // Cache this module for later + mModules.Put(spec, entry); + + // Set the location information for the new global, so that tools like + // about:memory may use that information + if (!mReuseLoaderGlobal) { + xpc::SetLocationForGlobal(entryObj, spec); + } + + // The hash owns the ModuleEntry now, forget about it + return entry.forget(); +} + +nsresult +mozJSComponentLoader::FindTargetObject(JSContext* aCx, + MutableHandleObject aTargetObject) +{ + aTargetObject.set(nullptr); + + RootedObject targetObject(aCx); + if (mReuseLoaderGlobal) { + JSFunction* fun = js::GetOutermostEnclosingFunctionOfScriptedCaller(aCx); + if (fun) { + JSObject* funParent = js::GetNearestEnclosingWithEnvironmentObjectForFunction(fun); + if (JS_GetClass(funParent) == &kFakeBackstagePassJSClass) + targetObject = funParent; + } + } + + // The above could fail, even if mReuseLoaderGlobal, if the scripted + // caller is not a component/JSM (it could be a DOM scope, for + // instance). + if (!targetObject) { + // Our targetObject is the caller's global object. Let's get it. + targetObject = CurrentGlobalOrNull(aCx); + } + + aTargetObject.set(targetObject); + return NS_OK; +} + +// This requires that the keys be strings and the values be pointers. +template <class Key, class Data, class UserData> +static size_t +SizeOfTableExcludingThis(const nsBaseHashtable<Key, Data, UserData>& aTable, + MallocSizeOf aMallocSizeOf) +{ + size_t n = aTable.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto iter = aTable.ConstIter(); !iter.Done(); iter.Next()) { + n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += iter.Data()->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +size_t +mozJSComponentLoader::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) +{ + size_t n = aMallocSizeOf(this); + n += SizeOfTableExcludingThis(mModules, aMallocSizeOf); + n += SizeOfTableExcludingThis(mImports, aMallocSizeOf); + n += SizeOfTableExcludingThis(mInProgressImports, aMallocSizeOf); + return n; +} + +// Some stack based classes for cleaning up on early return +#ifdef HAVE_PR_MEMMAP +class FileAutoCloser +{ + public: + explicit FileAutoCloser(PRFileDesc* file) : mFile(file) {} + ~FileAutoCloser() { PR_Close(mFile); } + private: + PRFileDesc* mFile; +}; + +class FileMapAutoCloser +{ + public: + explicit FileMapAutoCloser(PRFileMap* map) : mMap(map) {} + ~FileMapAutoCloser() { PR_CloseFileMap(mMap); } + private: + PRFileMap* mMap; +}; +#else +class ANSIFileAutoCloser +{ + public: + explicit ANSIFileAutoCloser(FILE* file) : mFile(file) {} + ~ANSIFileAutoCloser() { fclose(mFile); } + private: + FILE* mFile; +}; +#endif + +JSObject* +mozJSComponentLoader::PrepareObjectForLocation(JSContext* aCx, + nsIFile* aComponentFile, + nsIURI* aURI, + bool aReuseLoaderGlobal, + bool* aRealFile) +{ + nsCOMPtr<nsIXPConnectJSObjectHolder> holder; + if (aReuseLoaderGlobal) { + holder = mLoaderGlobal; + } + + nsresult rv = NS_OK; + nsCOMPtr<nsIXPConnect> xpc = + do_GetService(kXPConnectServiceContractID, &rv); + NS_ENSURE_SUCCESS(rv, nullptr); + bool createdNewGlobal = false; + + if (!mLoaderGlobal) { + RefPtr<BackstagePass> backstagePass; + rv = NS_NewBackstagePass(getter_AddRefs(backstagePass)); + NS_ENSURE_SUCCESS(rv, nullptr); + + CompartmentOptions options; + + options.creationOptions() + .setZone(SystemZone) + .setAddonId(aReuseLoaderGlobal ? nullptr : MapURIToAddonID(aURI)); + + options.behaviors().setVersion(JSVERSION_LATEST); + + if (xpc::SharedMemoryEnabled()) + options.creationOptions().setSharedMemoryAndAtomicsEnabled(true); + + // Defer firing OnNewGlobalObject until after the __URI__ property has + // been defined so the JS debugger can tell what module the global is + // for + rv = xpc->InitClassesWithNewWrappedGlobal(aCx, + static_cast<nsIGlobalObject*>(backstagePass), + mSystemPrincipal, + nsIXPConnect::DONT_FIRE_ONNEWGLOBALHOOK, + options, + getter_AddRefs(holder)); + NS_ENSURE_SUCCESS(rv, nullptr); + createdNewGlobal = true; + + RootedObject global(aCx, holder->GetJSObject()); + NS_ENSURE_TRUE(global, nullptr); + + backstagePass->SetGlobalObject(global); + + JSAutoCompartment ac(aCx, global); + if (!JS_DefineFunctions(aCx, global, gGlobalFun) || + !JS_DefineProfilingFunctions(aCx, global)) { + return nullptr; + } + + if (aReuseLoaderGlobal) { + mLoaderGlobal = holder; + } + } + + RootedObject obj(aCx, holder->GetJSObject()); + NS_ENSURE_TRUE(obj, nullptr); + + JSAutoCompartment ac(aCx, obj); + + if (aReuseLoaderGlobal) { + // If we're reusing the loader global, we don't actually use the + // global, but rather we use a different object as the 'this' object. + obj = JS_NewObject(aCx, &kFakeBackstagePassJSClass); + NS_ENSURE_TRUE(obj, nullptr); + } + + *aRealFile = false; + + // need to be extra careful checking for URIs pointing to files + // EnsureFile may not always get called, especially on resource URIs + // so we need to call GetFile to make sure this is a valid file + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(aURI, &rv); + nsCOMPtr<nsIFile> testFile; + if (NS_SUCCEEDED(rv)) { + fileURL->GetFile(getter_AddRefs(testFile)); + } + + if (testFile) { + *aRealFile = true; + + if (XRE_IsParentProcess()) { + RootedObject locationObj(aCx); + + rv = xpc->WrapNative(aCx, obj, aComponentFile, + NS_GET_IID(nsIFile), + locationObj.address()); + NS_ENSURE_SUCCESS(rv, nullptr); + NS_ENSURE_TRUE(locationObj, nullptr); + + if (!JS_DefineProperty(aCx, obj, "__LOCATION__", locationObj, 0)) + return nullptr; + } + } + + nsAutoCString nativePath; + rv = aURI->GetSpec(nativePath); + NS_ENSURE_SUCCESS(rv, nullptr); + + // Expose the URI from which the script was imported through a special + // variable that we insert into the JSM. + RootedString exposedUri(aCx, JS_NewStringCopyN(aCx, nativePath.get(), nativePath.Length())); + NS_ENSURE_TRUE(exposedUri, nullptr); + + if (!JS_DefineProperty(aCx, obj, "__URI__", exposedUri, 0)) + return nullptr; + + if (createdNewGlobal) { + // AutoEntryScript required to invoke debugger hook, which is a + // Gecko-specific concept at present. + dom::AutoEntryScript aes(holder->GetJSObject(), + "component loader report global"); + RootedObject global(aes.cx(), holder->GetJSObject()); + JS_FireOnNewGlobalObject(aes.cx(), global); + } + + return obj; +} + +nsresult +mozJSComponentLoader::ObjectForLocation(ComponentLoaderInfo& aInfo, + nsIFile* aComponentFile, + MutableHandleObject aObject, + MutableHandleScript aTableScript, + char** aLocation, + bool aPropagateExceptions, + MutableHandleValue aException) +{ + MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread."); + + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + bool realFile = false; + nsresult rv = aInfo.EnsureURI(); + NS_ENSURE_SUCCESS(rv, rv); + RootedObject obj(cx, PrepareObjectForLocation(cx, aComponentFile, aInfo.URI(), + mReuseLoaderGlobal, &realFile)); + NS_ENSURE_TRUE(obj, NS_ERROR_FAILURE); + MOZ_ASSERT(JS_IsGlobalObject(obj) == !mReuseLoaderGlobal); + + JSAutoCompartment ac(cx, obj); + + RootedScript script(cx); + RootedFunction function(cx); + + nsAutoCString nativePath; + rv = aInfo.URI()->GetSpec(nativePath); + NS_ENSURE_SUCCESS(rv, rv); + + // Before compiling the script, first check to see if we have it in + // the startupcache. Note: as a rule, startupcache errors are not fatal + // to loading the script, since we can always slow-load. + + bool writeToCache = false; + StartupCache* cache = StartupCache::GetSingleton(); + + nsAutoCString cachePath(kJSCachePrefix); + rv = PathifyURI(aInfo.URI(), cachePath); + NS_ENSURE_SUCCESS(rv, rv); + + if (cache) { + if (!mReuseLoaderGlobal) { + rv = ReadCachedScript(cache, cachePath, cx, mSystemPrincipal, &script); + } else { + rv = ReadCachedFunction(cache, cachePath, cx, mSystemPrincipal, + function.address()); + } + + if (NS_SUCCEEDED(rv)) { + LOG(("Successfully loaded %s from startupcache\n", nativePath.get())); + } else { + // This is ok, it just means the script is not yet in the + // cache. Could mean that the cache was corrupted and got removed, + // but either way we're going to write this out. + writeToCache = true; + // ReadCachedScript and ReadCachedFunction may have set a pending + // exception. + JS_ClearPendingException(cx); + } + } + + if (!script && !function) { + // The script wasn't in the cache , so compile it now. + LOG(("Slow loading %s\n", nativePath.get())); + + // Use lazy source if both of these conditions hold: + // + // (1) mReuseLoaderGlobal is false. If mReuseLoaderGlobal is true, we + // can't do lazy source because we compile things as functions + // (rather than script), and lazy source isn't supported in that + // configuration. That's ok though, because we only do + // mReuseLoaderGlobal on b2g, where we invoke setDiscardSource(true) + // on the entire global. + // + // (2) We're using the startup cache. Non-lazy source + startup cache + // regresses installer size (due to source code stored in XDR + // encoded modules in omni.ja). Also, XDR decoding is relatively + // fast. Content processes don't use the startup cache, so we want + // them to use non-lazy source code to enable lazy parsing. + // See bug 1303754. + CompileOptions options(cx); + options.setNoScriptRval(mReuseLoaderGlobal ? false : true) + .setVersion(JSVERSION_LATEST) + .setFileAndLine(nativePath.get(), 1) + .setSourceIsLazy(!mReuseLoaderGlobal && !!cache); + + if (realFile) { +#ifdef HAVE_PR_MEMMAP + int64_t fileSize; + rv = aComponentFile->GetFileSize(&fileSize); + if (NS_FAILED(rv)) { + return rv; + } + + int64_t maxSize = UINT32_MAX; + if (fileSize > maxSize) { + NS_ERROR("file too large"); + return NS_ERROR_FAILURE; + } + + PRFileDesc* fileHandle; + rv = aComponentFile->OpenNSPRFileDesc(PR_RDONLY, 0, &fileHandle); + if (NS_FAILED(rv)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + // Make sure the file is closed, no matter how we return. + FileAutoCloser fileCloser(fileHandle); + + // We don't provide the file size here. If we did, PR_CreateFileMap + // would simply stat() the file to verify that the size we provided + // didn't require extending the file. We know that the file doesn't + // need to be extended, so skip the extra work by not providing the + // size. + PRFileMap* map = PR_CreateFileMap(fileHandle, 0, PR_PROT_READONLY); + if (!map) { + NS_ERROR("Failed to create file map"); + return NS_ERROR_FAILURE; + } + + // Make sure the file map is closed, no matter how we return. + FileMapAutoCloser mapCloser(map); + + uint32_t fileSize32 = fileSize; + + char* buf = static_cast<char*>(PR_MemMap(map, 0, fileSize32)); + if (!buf) { + NS_WARNING("Failed to map file"); + return NS_ERROR_FAILURE; + } + + if (!mReuseLoaderGlobal) { + Compile(cx, options, buf, fileSize32, &script); + } else { + // Note: exceptions will get handled further down; + // don't early return for them here. + AutoObjectVector envChain(cx); + if (envChain.append(obj)) { + CompileFunction(cx, envChain, + options, nullptr, 0, nullptr, + buf, fileSize32, &function); + } + } + + PR_MemUnmap(buf, fileSize32); + +#else /* HAVE_PR_MEMMAP */ + + /** + * No memmap implementation, so fall back to + * reading in the file + */ + + FILE* fileHandle; + rv = aComponentFile->OpenANSIFileDesc("r", &fileHandle); + if (NS_FAILED(rv)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + // Ensure file fclose + ANSIFileAutoCloser fileCloser(fileHandle); + + int64_t len; + rv = aComponentFile->GetFileSize(&len); + if (NS_FAILED(rv) || len < 0) { + NS_WARNING("Failed to get file size"); + return NS_ERROR_FAILURE; + } + + char* buf = (char*) malloc(len * sizeof(char)); + if (!buf) { + return NS_ERROR_FAILURE; + } + + size_t rlen = fread(buf, 1, len, fileHandle); + if (rlen != (uint64_t)len) { + free(buf); + NS_WARNING("Failed to read file"); + return NS_ERROR_FAILURE; + } + + if (!mReuseLoaderGlobal) { + script = Compile(cx, options, buf, fileSize32); + } else { + // Note: exceptions will get handled further down; + // don't early return for them here. + AutoObjectVector envChain(cx); + if (envChain.append(obj)) { + CompileFunction(cx, envChain, + options, nullptr, 0, nullptr, + buf, fileSize32, &function); + } + } + + free(buf); + +#endif /* HAVE_PR_MEMMAP */ + } else { + rv = aInfo.EnsureScriptChannel(); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIInputStream> scriptStream; + rv = NS_MaybeOpenChannelUsingOpen2(aInfo.ScriptChannel(), + getter_AddRefs(scriptStream)); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t len64; + uint32_t bytesRead; + + rv = scriptStream->Available(&len64); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(len64 < UINT32_MAX, NS_ERROR_FILE_TOO_BIG); + if (!len64) + return NS_ERROR_FAILURE; + uint32_t len = (uint32_t)len64; + + /* malloc an internal buf the size of the file */ + auto buf = MakeUniqueFallible<char[]>(len + 1); + if (!buf) + return NS_ERROR_OUT_OF_MEMORY; + + /* read the file in one swoop */ + rv = scriptStream->Read(buf.get(), len, &bytesRead); + if (bytesRead != len) + return NS_BASE_STREAM_OSERROR; + + buf[len] = '\0'; + + if (!mReuseLoaderGlobal) { + Compile(cx, options, buf.get(), bytesRead, &script); + } else { + // Note: exceptions will get handled further down; + // don't early return for them here. + AutoObjectVector envChain(cx); + if (envChain.append(obj)) { + CompileFunction(cx, envChain, + options, nullptr, 0, nullptr, + buf.get(), bytesRead, &function); + } + } + } + // Propagate the exception, if one exists. Also, don't leave the stale + // exception on this context. + if (!script && !function && aPropagateExceptions && + jsapi.HasException()) { + if (!jsapi.StealException(aException)) + return NS_ERROR_OUT_OF_MEMORY; + } + } + + if (!script && !function) { + return NS_ERROR_FAILURE; + } + + // We must have a script or a function (but not both!) here. We have a + // script when we're not reusing the loader global, and a function + // otherwise. + MOZ_ASSERT(!!script != !!function); + MOZ_ASSERT(!!script == JS_IsGlobalObject(obj)); + + if (writeToCache) { + // We successfully compiled the script, so cache it. + if (script) { + rv = WriteCachedScript(cache, cachePath, cx, mSystemPrincipal, + script); + } else { + rv = WriteCachedFunction(cache, cachePath, cx, mSystemPrincipal, + function); + } + + // Don't treat failure to write as fatal, since we might be working + // with a read-only cache. + if (NS_SUCCEEDED(rv)) { + LOG(("Successfully wrote to cache\n")); + } else { + LOG(("Failed to write to cache\n")); + } + } + + // Assign aObject here so that it's available to recursive imports. + // See bug 384168. + aObject.set(obj); + + RootedScript tableScript(cx, script); + if (!tableScript) { + tableScript = JS_GetFunctionScript(cx, function); + MOZ_ASSERT(tableScript); + } + + aTableScript.set(tableScript); + + + { // Scope for AutoEntryScript + + // We're going to run script via JS_ExecuteScript or + // JS_CallFunction, so we need an AutoEntryScript. + // This is Gecko-specific and not in any spec. + dom::AutoEntryScript aes(CurrentGlobalOrNull(cx), + "component loader load module"); + JSContext* aescx = aes.cx(); + bool ok; + if (script) { + ok = JS_ExecuteScript(aescx, script); + } else { + RootedValue rval(cx); + ok = JS_CallFunction(aescx, obj, function, + JS::HandleValueArray::empty(), &rval); + } + + if (!ok) { + if (aPropagateExceptions && aes.HasException()) { + // Ignore return value because we're returning an error code + // anyway. + Unused << aes.StealException(aException); + } + aObject.set(nullptr); + aTableScript.set(nullptr); + return NS_ERROR_FAILURE; + } + } + + /* Freed when we remove from the table. */ + *aLocation = ToNewCString(nativePath); + if (!*aLocation) { + aObject.set(nullptr); + aTableScript.set(nullptr); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void +mozJSComponentLoader::UnloadModules() +{ + mInitialized = false; + + if (mLoaderGlobal) { + MOZ_ASSERT(mReuseLoaderGlobal, "How did this happen?"); + + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedObject global(cx, mLoaderGlobal->GetJSObject()); + if (global) { + JSAutoCompartment ac(cx, global); + if (JS_HasExtensibleLexicalEnvironment(global)) { + JS_SetAllNonReservedSlotsToUndefined(cx, JS_ExtensibleLexicalEnvironment(global)); + } + JS_SetAllNonReservedSlotsToUndefined(cx, global); + } else { + NS_WARNING("Going to leak!"); + } + + mLoaderGlobal = nullptr; + } + + mInProgressImports.Clear(); + mImports.Clear(); + + for (auto iter = mModules.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->Clear(); + iter.Remove(); + } +} + +NS_IMETHODIMP +mozJSComponentLoader::Import(const nsACString& registryLocation, + HandleValue targetValArg, + JSContext* cx, + uint8_t optionalArgc, + MutableHandleValue retval) +{ + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + RootedValue targetVal(cx, targetValArg); + RootedObject targetObject(cx, nullptr); + if (optionalArgc) { + // The caller passed in the optional second argument. Get it. + if (targetVal.isObject()) { + // If we're passing in something like a content DOM window, chances + // are the caller expects the properties to end up on the object + // proper and not on the Xray holder. This is dubious, but can be used + // during testing. Given that dumb callers can already leak JSMs into + // content by passing a raw content JS object (where Xrays aren't + // possible), we aim for consistency here. Waive xray. + if (WrapperFactory::IsXrayWrapper(&targetVal.toObject()) && + !WrapperFactory::WaiveXrayAndWrap(cx, &targetVal)) + { + return NS_ERROR_FAILURE; + } + targetObject = &targetVal.toObject(); + } else if (!targetVal.isNull()) { + // If targetVal isNull(), we actually want to leave targetObject null. + // Not doing so breaks |make package|. + return ReportOnCallerUTF8(cx, ERROR_SCOPE_OBJ, + PromiseFlatCString(registryLocation).get()); + } + } else { + nsresult rv = FindTargetObject(cx, &targetObject); + NS_ENSURE_SUCCESS(rv, rv); + } + + Maybe<JSAutoCompartment> ac; + if (targetObject) { + ac.emplace(cx, targetObject); + } + + RootedObject global(cx); + nsresult rv = ImportInto(registryLocation, targetObject, cx, &global); + + if (global) { + if (!JS_WrapObject(cx, &global)) { + NS_ERROR("can't wrap return value"); + return NS_ERROR_FAILURE; + } + + retval.setObject(*global); + } + return rv; +} + +NS_IMETHODIMP +mozJSComponentLoader::ImportInto(const nsACString& aLocation, + JSObject* aTargetObj, + nsAXPCNativeCallContext* cc, + JSObject** _retval) +{ + JSContext* callercx; + nsresult rv = cc->GetJSContext(&callercx); + NS_ENSURE_SUCCESS(rv, rv); + + RootedObject targetObject(callercx, aTargetObj); + RootedObject global(callercx); + rv = ImportInto(aLocation, targetObject, callercx, &global); + NS_ENSURE_SUCCESS(rv, rv); + *_retval = global; + return NS_OK; +} + +NS_IMETHODIMP +mozJSComponentLoader::IsModuleLoaded(const nsACString& aLocation, + bool* retval) +{ + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + nsresult rv; + if (!mInitialized) { + rv = ReallyInit(); + NS_ENSURE_SUCCESS(rv, rv); + } + + ComponentLoaderInfo info(aLocation); + rv = info.EnsureKey(); + NS_ENSURE_SUCCESS(rv, rv); + + *retval = !!mImports.Get(info.Key()); + return NS_OK; +} + +static JSObject* +ResolveModuleObjectPropertyById(JSContext* aCx, HandleObject aModObj, HandleId id) +{ + if (JS_HasExtensibleLexicalEnvironment(aModObj)) { + RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj)); + bool found; + if (!JS_HasOwnPropertyById(aCx, lexical, id, &found)) { + return nullptr; + } + if (found) { + return lexical; + } + } + return aModObj; +} + +nsresult +mozJSComponentLoader::ImportInto(const nsACString& aLocation, + HandleObject targetObj, + JSContext* callercx, + MutableHandleObject vp) +{ + vp.set(nullptr); + + nsresult rv; + if (!mInitialized) { + rv = ReallyInit(); + NS_ENSURE_SUCCESS(rv, rv); + } + + ComponentLoaderInfo info(aLocation); + rv = info.EnsureResolvedURI(); + NS_ENSURE_SUCCESS(rv, rv); + + // get the JAR if there is one + nsCOMPtr<nsIJARURI> jarURI; + jarURI = do_QueryInterface(info.ResolvedURI(), &rv); + nsCOMPtr<nsIFileURL> baseFileURL; + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIURI> baseURI; + while (jarURI) { + jarURI->GetJARFile(getter_AddRefs(baseURI)); + jarURI = do_QueryInterface(baseURI, &rv); + } + baseFileURL = do_QueryInterface(baseURI, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } else { + baseFileURL = do_QueryInterface(info.ResolvedURI(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIFile> sourceFile; + rv = baseFileURL->GetFile(getter_AddRefs(sourceFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> sourceLocalFile; + sourceLocalFile = do_QueryInterface(sourceFile, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = info.EnsureKey(); + NS_ENSURE_SUCCESS(rv, rv); + + ModuleEntry* mod; + nsAutoPtr<ModuleEntry> newEntry; + if (!mImports.Get(info.Key(), &mod) && !mInProgressImports.Get(info.Key(), &mod)) { + newEntry = new ModuleEntry(RootingContext::get(callercx)); + if (!newEntry) + return NS_ERROR_OUT_OF_MEMORY; + mInProgressImports.Put(info.Key(), newEntry); + + rv = info.EnsureURI(); + NS_ENSURE_SUCCESS(rv, rv); + RootedValue exception(callercx); + rv = ObjectForLocation(info, sourceLocalFile, &newEntry->obj, + &newEntry->thisObjectKey, + &newEntry->location, true, &exception); + + mInProgressImports.Remove(info.Key()); + + if (NS_FAILED(rv)) { + if (!exception.isUndefined()) { + // An exception was thrown during compilation. Propagate it + // out to our caller so they can report it. + if (!JS_WrapValue(callercx, &exception)) + return NS_ERROR_OUT_OF_MEMORY; + JS_SetPendingException(callercx, exception); + return NS_OK; + } + + // Something failed, but we don't know what it is, guess. + return NS_ERROR_FILE_NOT_FOUND; + } + + // Set the location information for the new global, so that tools like + // about:memory may use that information + if (!mReuseLoaderGlobal) { + xpc::SetLocationForGlobal(newEntry->obj, aLocation); + } + + mod = newEntry; + } + + MOZ_ASSERT(mod->obj, "Import table contains entry with no object"); + vp.set(mod->obj); + + if (targetObj) { + // cxhelper must be created before jsapi, so that jsapi is detroyed and + // pops any context it has pushed before we report to the caller context. + JSCLContextHelper cxhelper(callercx); + + // Even though we are calling JS_SetPropertyById on targetObj, we want + // to ensure that we never run script here, so we use an AutoJSAPI and + // not an AutoEntryScript. + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JSAutoCompartment ac(cx, mod->obj); + + RootedValue symbols(cx); + RootedObject exportedSymbolsHolder(cx, ResolveModuleObjectProperty(cx, mod->obj, + "EXPORTED_SYMBOLS")); + if (!exportedSymbolsHolder || + !JS_GetProperty(cx, exportedSymbolsHolder, + "EXPORTED_SYMBOLS", &symbols)) { + nsCString location; + rv = info.GetLocation(location); + NS_ENSURE_SUCCESS(rv, rv); + return ReportOnCallerUTF8(cxhelper, ERROR_NOT_PRESENT, + location.get()); + } + + bool isArray; + if (!JS_IsArrayObject(cx, symbols, &isArray)) { + return NS_ERROR_FAILURE; + } + if (!isArray) { + nsCString location; + rv = info.GetLocation(location); + NS_ENSURE_SUCCESS(rv, rv); + return ReportOnCallerUTF8(cxhelper, ERROR_NOT_AN_ARRAY, + location.get()); + } + + RootedObject symbolsObj(cx, &symbols.toObject()); + + // Iterate over symbols array, installing symbols on targetObj: + + uint32_t symbolCount = 0; + if (!JS_GetArrayLength(cx, symbolsObj, &symbolCount)) { + nsCString location; + rv = info.GetLocation(location); + NS_ENSURE_SUCCESS(rv, rv); + return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_ARRAY_LENGTH, + location.get()); + } + +#ifdef DEBUG + nsAutoCString logBuffer; +#endif + + RootedValue value(cx); + RootedId symbolId(cx); + RootedObject symbolHolder(cx); + for (uint32_t i = 0; i < symbolCount; ++i) { + if (!JS_GetElement(cx, symbolsObj, i, &value) || + !value.isString() || + !JS_ValueToId(cx, value, &symbolId)) { + nsCString location; + rv = info.GetLocation(location); + NS_ENSURE_SUCCESS(rv, rv); + return ReportOnCallerUTF8(cxhelper, ERROR_ARRAY_ELEMENT, + location.get(), i); + } + + symbolHolder = ResolveModuleObjectPropertyById(cx, mod->obj, symbolId); + if (!symbolHolder || + !JS_GetPropertyById(cx, symbolHolder, symbolId, &value)) { + JSAutoByteString bytes; + RootedString symbolStr(cx, JSID_TO_STRING(symbolId)); + if (!bytes.encodeUtf8(cx, symbolStr)) + return NS_ERROR_FAILURE; + nsCString location; + rv = info.GetLocation(location); + NS_ENSURE_SUCCESS(rv, rv); + return ReportOnCallerUTF8(cxhelper, ERROR_GETTING_SYMBOL, + location.get(), bytes.ptr()); + } + + JSAutoCompartment target_ac(cx, targetObj); + + if (!JS_WrapValue(cx, &value) || + !JS_SetPropertyById(cx, targetObj, symbolId, value)) { + JSAutoByteString bytes; + RootedString symbolStr(cx, JSID_TO_STRING(symbolId)); + if (!bytes.encodeUtf8(cx, symbolStr)) + return NS_ERROR_FAILURE; + nsCString location; + rv = info.GetLocation(location); + NS_ENSURE_SUCCESS(rv, rv); + return ReportOnCallerUTF8(cxhelper, ERROR_SETTING_SYMBOL, + location.get(), bytes.ptr()); + } +#ifdef DEBUG + if (i == 0) { + logBuffer.AssignLiteral("Installing symbols [ "); + } + JSAutoByteString bytes(cx, JSID_TO_STRING(symbolId)); + if (!!bytes) + logBuffer.Append(bytes.ptr()); + logBuffer.Append(' '); + if (i == symbolCount - 1) { + nsCString location; + rv = info.GetLocation(location); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("%s] from %s\n", logBuffer.get(), location.get())); + } +#endif + } + } + + // Cache this module for later + if (newEntry) { + mImports.Put(info.Key(), newEntry); + newEntry.forget(); + } + + return NS_OK; +} + +NS_IMETHODIMP +mozJSComponentLoader::Unload(const nsACString & aLocation) +{ + nsresult rv; + + if (!mInitialized) { + return NS_OK; + } + + MOZ_RELEASE_ASSERT(!mReuseLoaderGlobal, "Module unloading not supported when " + "compartment sharing is enabled"); + + ComponentLoaderInfo info(aLocation); + rv = info.EnsureKey(); + NS_ENSURE_SUCCESS(rv, rv); + ModuleEntry* mod; + if (mImports.Get(info.Key(), &mod)) { + mImports.Remove(info.Key()); + } + + return NS_OK; +} + +NS_IMETHODIMP +mozJSComponentLoader::Observe(nsISupports* subject, const char* topic, + const char16_t* data) +{ + if (!strcmp(topic, "xpcom-shutdown-loaders")) { + UnloadModules(); + } else { + NS_ERROR("Unexpected observer topic."); + } + + return NS_OK; +} + +size_t +mozJSComponentLoader::ModuleEntry::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const +{ + size_t n = aMallocSizeOf(this); + n += aMallocSizeOf(location); + + return n; +} + +/* static */ already_AddRefed<nsIFactory> +mozJSComponentLoader::ModuleEntry::GetFactory(const mozilla::Module& module, + const mozilla::Module::CIDEntry& entry) +{ + const ModuleEntry& self = static_cast<const ModuleEntry&>(module); + MOZ_ASSERT(self.getfactoryobj, "Handing out an uninitialized module?"); + + nsCOMPtr<nsIFactory> f; + nsresult rv = self.getfactoryobj->Get(*entry.cid, getter_AddRefs(f)); + if (NS_FAILED(rv)) + return nullptr; + + return f.forget(); +} + +//---------------------------------------------------------------------- + +JSCLContextHelper::JSCLContextHelper(JSContext* aCx) + : mContext(aCx) + , mBuf(nullptr) +{ +} + +JSCLContextHelper::~JSCLContextHelper() +{ + if (mBuf) { + JS_ReportErrorUTF8(mContext, "%s", mBuf); + JS_smprintf_free(mBuf); + } +} + +void +JSCLContextHelper::reportErrorAfterPop(char* buf) +{ + MOZ_ASSERT(!mBuf, "Already called reportErrorAfterPop"); + mBuf = buf; +} diff --git a/js/xpconnect/loader/mozJSComponentLoader.h b/js/xpconnect/loader/mozJSComponentLoader.h new file mode 100644 index 000000000..6ebe4a371 --- /dev/null +++ b/js/xpconnect/loader/mozJSComponentLoader.h @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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/. */ + +#ifndef mozJSComponentLoader_h +#define mozJSComponentLoader_h + +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/ModuleLoader.h" +#include "nsAutoPtr.h" +#include "nsISupports.h" +#include "nsIObserver.h" +#include "nsIURI.h" +#include "xpcIJSModuleLoader.h" +#include "nsClassHashtable.h" +#include "nsDataHashtable.h" +#include "jsapi.h" + +#include "xpcIJSGetFactory.h" + +class nsIFile; +class nsIPrincipal; +class nsIXPConnectJSObjectHolder; +class ComponentLoaderInfo; + +/* 6bd13476-1dd2-11b2-bbef-f0ccb5fa64b6 (thanks, mozbot) */ + +#define MOZJSCOMPONENTLOADER_CID \ + {0x6bd13476, 0x1dd2, 0x11b2, \ + { 0xbb, 0xef, 0xf0, 0xcc, 0xb5, 0xfa, 0x64, 0xb6 }} +#define MOZJSCOMPONENTLOADER_CONTRACTID "@mozilla.org/moz/jsloader;1" + +class mozJSComponentLoader : public mozilla::ModuleLoader, + public xpcIJSModuleLoader, + public nsIObserver +{ + public: + NS_DECL_ISUPPORTS + NS_DECL_XPCIJSMODULELOADER + NS_DECL_NSIOBSERVER + + mozJSComponentLoader(); + + // ModuleLoader + const mozilla::Module* LoadModule(mozilla::FileLocation& aFile) override; + + nsresult FindTargetObject(JSContext* aCx, + JS::MutableHandleObject aTargetObject); + + static mozJSComponentLoader* Get() { return sSelf; } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + + protected: + virtual ~mozJSComponentLoader(); + + static mozJSComponentLoader* sSelf; + + nsresult ReallyInit(); + void UnloadModules(); + + JSObject* PrepareObjectForLocation(JSContext* aCx, + nsIFile* aComponentFile, + nsIURI* aComponent, + bool aReuseLoaderGlobal, + bool* aRealFile); + + nsresult ObjectForLocation(ComponentLoaderInfo& aInfo, + nsIFile* aComponentFile, + JS::MutableHandleObject aObject, + JS::MutableHandleScript aTableScript, + char** location, + bool aCatchException, + JS::MutableHandleValue aException); + + nsresult ImportInto(const nsACString& aLocation, + JS::HandleObject targetObj, + JSContext* callercx, + JS::MutableHandleObject vp); + + nsCOMPtr<nsIComponentManager> mCompMgr; + nsCOMPtr<nsIPrincipal> mSystemPrincipal; + nsCOMPtr<nsIXPConnectJSObjectHolder> mLoaderGlobal; + + class ModuleEntry : public mozilla::Module + { + public: + explicit ModuleEntry(JS::RootingContext* aRootingCx) + : mozilla::Module(), obj(aRootingCx), thisObjectKey(aRootingCx) + { + mVersion = mozilla::Module::kVersion; + mCIDs = nullptr; + mContractIDs = nullptr; + mCategoryEntries = nullptr; + getFactoryProc = GetFactory; + loadProc = nullptr; + unloadProc = nullptr; + + location = nullptr; + } + + ~ModuleEntry() { + Clear(); + } + + void Clear() { + getfactoryobj = nullptr; + + if (obj) { + mozilla::AutoJSContext cx; + JSAutoCompartment ac(cx, obj); + + if (JS_HasExtensibleLexicalEnvironment(obj)) { + JS_SetAllNonReservedSlotsToUndefined(cx, JS_ExtensibleLexicalEnvironment(obj)); + } + JS_SetAllNonReservedSlotsToUndefined(cx, obj); + obj = nullptr; + thisObjectKey = nullptr; + } + + if (location) + free(location); + + obj = nullptr; + thisObjectKey = nullptr; + location = nullptr; + } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + static already_AddRefed<nsIFactory> GetFactory(const mozilla::Module& module, + const mozilla::Module::CIDEntry& entry); + + nsCOMPtr<xpcIJSGetFactory> getfactoryobj; + JS::PersistentRootedObject obj; + JS::PersistentRootedScript thisObjectKey; + char* location; + }; + + friend class ModuleEntry; + + static size_t DataEntrySizeOfExcludingThis(const nsACString& aKey, ModuleEntry* const& aData, + mozilla::MallocSizeOf aMallocSizeOf, void* arg); + static size_t ClassEntrySizeOfExcludingThis(const nsACString& aKey, + const nsAutoPtr<ModuleEntry>& aData, + mozilla::MallocSizeOf aMallocSizeOf, void* arg); + + // Modules are intentionally leaked, but still cleared. + nsDataHashtable<nsCStringHashKey, ModuleEntry*> mModules; + + nsClassHashtable<nsCStringHashKey, ModuleEntry> mImports; + nsDataHashtable<nsCStringHashKey, ModuleEntry*> mInProgressImports; + + bool mInitialized; + bool mReuseLoaderGlobal; +}; + +#endif diff --git a/js/xpconnect/loader/mozJSLoaderUtils.cpp b/js/xpconnect/loader/mozJSLoaderUtils.cpp new file mode 100644 index 000000000..abc8aa3bb --- /dev/null +++ b/js/xpconnect/loader/mozJSLoaderUtils.cpp @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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/. */ + + +#include "mozilla/scache/StartupCache.h" + +#include "jsapi.h" +#include "jsfriendapi.h" + +#include "nsJSPrincipals.h" + +using namespace JS; +using namespace mozilla::scache; +using mozilla::UniquePtr; + +// We only serialize scripts with system principals. So we don't serialize the +// principals when writing a script. Instead, when reading it back, we set the +// principals to the system principals. +nsresult +ReadCachedScript(StartupCache* cache, nsACString& uri, JSContext* cx, + nsIPrincipal* systemPrincipal, MutableHandleScript scriptp) +{ + UniquePtr<char[]> buf; + uint32_t len; + nsresult rv = cache->GetBuffer(PromiseFlatCString(uri).get(), &buf, &len); + if (NS_FAILED(rv)) + return rv; // don't warn since NOT_AVAILABLE is an ok error + + JS::TranscodeBuffer buffer; + buffer.replaceRawBuffer(reinterpret_cast<uint8_t*>(buf.release()), len); + JS::TranscodeResult code = JS::DecodeScript(cx, buffer, scriptp); + if (code == JS::TranscodeResult_Ok) + return NS_OK; + + if ((code & JS::TranscodeResult_Failure) != 0) + return NS_ERROR_FAILURE; + + MOZ_ASSERT((code & JS::TranscodeResult_Throw) != 0); + JS_ClearPendingException(cx); + return NS_ERROR_OUT_OF_MEMORY; +} + +nsresult +ReadCachedFunction(StartupCache* cache, nsACString& uri, JSContext* cx, + nsIPrincipal* systemPrincipal, JSFunction** functionp) +{ + // This doesn't actually work ... + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult +WriteCachedScript(StartupCache* cache, nsACString& uri, JSContext* cx, + nsIPrincipal* systemPrincipal, HandleScript script) +{ + MOZ_ASSERT(JS_GetScriptPrincipals(script) == nsJSPrincipals::get(systemPrincipal)); + + JS::TranscodeBuffer buffer; + JS::TranscodeResult code = JS::EncodeScript(cx, buffer, script); + if (code != JS::TranscodeResult_Ok) { + if ((code & JS::TranscodeResult_Failure) != 0) + return NS_ERROR_FAILURE; + MOZ_ASSERT((code & JS::TranscodeResult_Throw) != 0); + JS_ClearPendingException(cx); + return NS_ERROR_OUT_OF_MEMORY; + } + + size_t size = buffer.length(); + if (size > UINT32_MAX) + return NS_ERROR_FAILURE; + nsresult rv = cache->PutBuffer(PromiseFlatCString(uri).get(), + reinterpret_cast<char*>(buffer.begin()), + size); + return rv; +} + +nsresult +WriteCachedFunction(StartupCache* cache, nsACString& uri, JSContext* cx, + nsIPrincipal* systemPrincipal, JSFunction* function) +{ + // This doesn't actually work ... + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/js/xpconnect/loader/mozJSLoaderUtils.h b/js/xpconnect/loader/mozJSLoaderUtils.h new file mode 100644 index 000000000..9adf82bf6 --- /dev/null +++ b/js/xpconnect/loader/mozJSLoaderUtils.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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/. */ + +#ifndef mozJSLoaderUtils_h +#define mozJSLoaderUtils_h + +#include "nsString.h" + +namespace mozilla { +namespace scache { +class StartupCache; +} // namespace scache +} // namespace mozilla + +nsresult +ReadCachedScript(mozilla::scache::StartupCache* cache, nsACString& uri, + JSContext* cx, nsIPrincipal* systemPrincipal, + JS::MutableHandleScript scriptp); + +nsresult +ReadCachedFunction(mozilla::scache::StartupCache* cache, nsACString& uri, + JSContext* cx, nsIPrincipal* systemPrincipal, + JSFunction** function); + +nsresult +WriteCachedScript(mozilla::scache::StartupCache* cache, nsACString& uri, + JSContext* cx, nsIPrincipal* systemPrincipal, + JS::HandleScript script); +nsresult +WriteCachedFunction(mozilla::scache::StartupCache* cache, nsACString& uri, + JSContext* cx, nsIPrincipal* systemPrincipal, + JSFunction* function); + +#endif /* mozJSLoaderUtils_h */ diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.cpp b/js/xpconnect/loader/mozJSSubScriptLoader.cpp new file mode 100644 index 000000000..824e9ab9e --- /dev/null +++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp @@ -0,0 +1,929 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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/. */ + +#include "mozJSSubScriptLoader.h" +#include "mozJSComponentLoader.h" +#include "mozJSLoaderUtils.h" + +#include "nsIURI.h" +#include "nsIIOService.h" +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIFileURL.h" +#include "nsScriptLoader.h" +#include "nsIScriptSecurityManager.h" +#include "nsThreadUtils.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "nsJSPrincipals.h" +#include "xpcprivate.h" // For xpc::OptionsBase +#include "jswrapper.h" + +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/scache/StartupCache.h" +#include "mozilla/scache/StartupCacheUtils.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" +#include "nsStringGlue.h" +#include "nsCycleCollectionParticipant.h" + +using namespace mozilla::scache; +using namespace JS; +using namespace xpc; +using namespace mozilla; +using namespace mozilla::dom; + +class MOZ_STACK_CLASS LoadSubScriptOptions : public OptionsBase { +public: + explicit LoadSubScriptOptions(JSContext* cx = xpc_GetSafeJSContext(), + JSObject* options = nullptr) + : OptionsBase(cx, options) + , target(cx) + , charset(NullString()) + , ignoreCache(false) + , async(false) + { } + + virtual bool Parse() { + return ParseObject("target", &target) && + ParseString("charset", charset) && + ParseBoolean("ignoreCache", &ignoreCache) && + ParseBoolean("async", &async); + } + + RootedObject target; + nsString charset; + bool ignoreCache; + bool async; +}; + + +/* load() error msgs, XXX localize? */ +#define LOAD_ERROR_NOSERVICE "Error creating IO Service." +#define LOAD_ERROR_NOURI "Error creating URI (invalid URL scheme?)" +#define LOAD_ERROR_NOSCHEME "Failed to get URI scheme. This is bad." +#define LOAD_ERROR_URI_NOT_LOCAL "Trying to load a non-local URI." +#define LOAD_ERROR_NOSTREAM "Error opening input stream (invalid filename?)" +#define LOAD_ERROR_NOCONTENT "ContentLength not available (not a local URL?)" +#define LOAD_ERROR_BADCHARSET "Error converting to specified charset" +#define LOAD_ERROR_BADREAD "File Read Error." +#define LOAD_ERROR_READUNDERFLOW "File Read Error (underflow.)" +#define LOAD_ERROR_NOPRINCIPALS "Failed to get principals." +#define LOAD_ERROR_NOSPEC "Failed to get URI spec. This is bad." +#define LOAD_ERROR_CONTENTTOOBIG "ContentLength is too large" + +mozJSSubScriptLoader::mozJSSubScriptLoader() : mSystemPrincipal(nullptr) +{ +} + +mozJSSubScriptLoader::~mozJSSubScriptLoader() +{ + /* empty */ +} + +NS_IMPL_ISUPPORTS(mozJSSubScriptLoader, mozIJSSubScriptLoader) + +static void +ReportError(JSContext* cx, const char* msg) +{ + RootedValue exn(cx, JS::StringValue(JS_NewStringCopyZ(cx, msg))); + JS_SetPendingException(cx, exn); +} + +static void +ReportError(JSContext* cx, const char* origMsg, nsIURI* uri) +{ + if (!uri) { + ReportError(cx, origMsg); + return; + } + + nsAutoCString spec; + nsresult rv = uri->GetSpec(spec); + if (NS_FAILED(rv)) + spec.Assign("(unknown)"); + + nsAutoCString msg(origMsg); + msg.Append(": "); + msg.Append(spec); + ReportError(cx, msg.get()); +} + +bool +PrepareScript(nsIURI* uri, + JSContext* cx, + RootedObject& targetObj, + const char* uriStr, + const nsAString& charset, + const char* buf, + int64_t len, + bool reuseGlobal, + MutableHandleScript script, + MutableHandleFunction function) +{ + JS::CompileOptions options(cx); + options.setFileAndLine(uriStr, 1) + .setVersion(JSVERSION_LATEST); + if (!charset.IsVoid()) { + char16_t* scriptBuf = nullptr; + size_t scriptLength = 0; + + nsresult rv = + nsScriptLoader::ConvertToUTF16(nullptr, reinterpret_cast<const uint8_t*>(buf), len, + charset, nullptr, scriptBuf, scriptLength); + + JS::SourceBufferHolder srcBuf(scriptBuf, scriptLength, + JS::SourceBufferHolder::GiveOwnership); + + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_BADCHARSET, uri); + return false; + } + + if (!reuseGlobal) { + if (JS_IsGlobalObject(targetObj)) { + return JS::Compile(cx, options, srcBuf, script); + } + return JS::CompileForNonSyntacticScope(cx, options, srcBuf, script); + } else { + AutoObjectVector envChain(cx); + if (!JS_IsGlobalObject(targetObj) && !envChain.append(targetObj)) { + return false; + } + return JS::CompileFunction(cx, envChain, options, nullptr, 0, nullptr, + srcBuf, function); + } + } else { + // We only use lazy source when no special encoding is specified because + // the lazy source loader doesn't know the encoding. + if (!reuseGlobal) { + options.setSourceIsLazy(true); + if (JS_IsGlobalObject(targetObj)) { + return JS::Compile(cx, options, buf, len, script); + } + return JS::CompileForNonSyntacticScope(cx, options, buf, len, script); + } else { + AutoObjectVector envChain(cx); + if (!JS_IsGlobalObject(targetObj) && !envChain.append(targetObj)) { + return false; + } + return JS::CompileFunction(cx, envChain, options, nullptr, 0, nullptr, + buf, len, function); + } + } +} + +bool +EvalScript(JSContext* cx, + RootedObject& target_obj, + MutableHandleValue retval, + nsIURI* uri, + bool cache, + RootedScript& script, + RootedFunction& function) +{ + if (function) { + script = JS_GetFunctionScript(cx, function); + } + + if (function) { + if (!JS_CallFunction(cx, target_obj, function, JS::HandleValueArray::empty(), retval)) { + return false; + } + } else { + if (JS_IsGlobalObject(target_obj)) { + if (!JS_ExecuteScript(cx, script, retval)) { + return false; + } + } else { + JS::AutoObjectVector envChain(cx); + if (!envChain.append(target_obj)) { + return false; + } + if (!JS_ExecuteScript(cx, envChain, script, retval)) { + return false; + } + } + } + + JSAutoCompartment rac(cx, target_obj); + if (!JS_WrapValue(cx, retval)) { + return false; + } + + if (cache && !!script) { + nsAutoCString cachePath; + JSVersion version = JS_GetVersion(cx); + cachePath.AppendPrintf("jssubloader/%d", version); + PathifyURI(uri, cachePath); + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID); + if (!secman) { + return false; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = secman->GetSystemPrincipal(getter_AddRefs(principal)); + if (NS_FAILED(rv) || !principal) { + ReportError(cx, LOAD_ERROR_NOPRINCIPALS, uri); + return false; + } + + WriteCachedScript(StartupCache::GetSingleton(), + cachePath, cx, principal, script); + } + + return true; +} + +class AsyncScriptLoader : public nsIIncrementalStreamLoaderObserver +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER + + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AsyncScriptLoader) + + AsyncScriptLoader(nsIChannel* aChannel, bool aReuseGlobal, + JSObject* aTargetObj, const nsAString& aCharset, + bool aCache, Promise* aPromise) + : mChannel(aChannel) + , mTargetObj(aTargetObj) + , mPromise(aPromise) + , mCharset(aCharset) + , mReuseGlobal(aReuseGlobal) + , mCache(aCache) + { + // Needed for the cycle collector to manage mTargetObj. + mozilla::HoldJSObjects(this); + } + +private: + virtual ~AsyncScriptLoader() { + mozilla::DropJSObjects(this); + } + + RefPtr<nsIChannel> mChannel; + Heap<JSObject*> mTargetObj; + RefPtr<Promise> mPromise; + nsString mCharset; + bool mReuseGlobal; + bool mCache; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(AsyncScriptLoader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AsyncScriptLoader) + NS_INTERFACE_MAP_ENTRY(nsIIncrementalStreamLoaderObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AsyncScriptLoader) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) + tmp->mTargetObj = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AsyncScriptLoader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AsyncScriptLoader) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mTargetObj) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AsyncScriptLoader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AsyncScriptLoader) + +class MOZ_STACK_CLASS AutoRejectPromise +{ +public: + AutoRejectPromise(AutoEntryScript& aAutoEntryScript, + Promise* aPromise, + nsIGlobalObject* aGlobalObject) + : mAutoEntryScript(aAutoEntryScript) + , mPromise(aPromise) + , mGlobalObject(aGlobalObject) {} + + ~AutoRejectPromise() { + if (mPromise) { + JSContext* cx = mAutoEntryScript.cx(); + RootedValue rejectionValue(cx, JS::UndefinedValue()); + if (mAutoEntryScript.HasException()) { + Unused << mAutoEntryScript.PeekException(&rejectionValue); + } + mPromise->MaybeReject(cx, rejectionValue); + } + } + + void ResolvePromise(HandleValue aResolveValue) { + mPromise->MaybeResolve(aResolveValue); + mPromise = nullptr; + } + +private: + AutoEntryScript& mAutoEntryScript; + RefPtr<Promise> mPromise; + nsCOMPtr<nsIGlobalObject> mGlobalObject; +}; + +NS_IMETHODIMP +AsyncScriptLoader::OnIncrementalData(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, + uint32_t aDataLength, + const uint8_t* aData, + uint32_t *aConsumedData) +{ + return NS_OK; +} + +NS_IMETHODIMP +AsyncScriptLoader::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, + nsresult aStatus, + uint32_t aLength, + const uint8_t* aBuf) +{ + nsCOMPtr<nsIURI> uri; + mChannel->GetURI(getter_AddRefs(uri)); + + nsCOMPtr<nsIGlobalObject> globalObject = xpc::NativeGlobal(mTargetObj); + AutoEntryScript aes(globalObject, "async loadSubScript"); + AutoRejectPromise autoPromise(aes, mPromise, globalObject); + JSContext* cx = aes.cx(); + + if (NS_FAILED(aStatus)) { + ReportError(cx, "Unable to load script.", uri); + } + // Just notify that we are done with this load. + NS_ENSURE_SUCCESS(aStatus, NS_OK); + + if (aLength == 0) { + ReportError(cx, LOAD_ERROR_NOCONTENT, uri); + return NS_OK; + } + + if (aLength > INT32_MAX) { + ReportError(cx, LOAD_ERROR_CONTENTTOOBIG, uri); + return NS_OK; + } + + RootedFunction function(cx); + RootedScript script(cx); + nsAutoCString spec; + nsresult rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + RootedObject target_obj(cx, mTargetObj); + + if (!PrepareScript(uri, cx, target_obj, spec.get(), mCharset, + reinterpret_cast<const char*>(aBuf), aLength, + mReuseGlobal, &script, &function)) + { + return NS_OK; + } + + JS::Rooted<JS::Value> retval(cx); + if (EvalScript(cx, target_obj, &retval, uri, mCache, script, function)) { + autoPromise.ResolvePromise(retval); + } + + return NS_OK; +} + +nsresult +mozJSSubScriptLoader::ReadScriptAsync(nsIURI* uri, JSObject* targetObjArg, + const nsAString& charset, + nsIIOService* serv, bool reuseGlobal, + bool cache, MutableHandleValue retval) +{ + RootedObject target_obj(RootingCx(), targetObjArg); + + nsCOMPtr<nsIGlobalObject> globalObject = xpc::NativeGlobal(target_obj); + ErrorResult result; + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(globalObject))) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<Promise> promise = Promise::Create(globalObject, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + DebugOnly<bool> asJS = ToJSValue(jsapi.cx(), promise, retval); + MOZ_ASSERT(asJS, "Should not fail to convert the promise to a JS value"); + + // We create a channel and call SetContentType, to avoid expensive MIME type + // lookups (bug 632490). + nsCOMPtr<nsIChannel> channel; + nsresult rv; + rv = NS_NewChannel(getter_AddRefs(channel), + uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, + serv); + + if (!NS_SUCCEEDED(rv)) { + return rv; + } + + channel->SetContentType(NS_LITERAL_CSTRING("application/javascript")); + + RefPtr<AsyncScriptLoader> loadObserver = + new AsyncScriptLoader(channel, + reuseGlobal, + target_obj, + charset, + cache, + promise); + + nsCOMPtr<nsIIncrementalStreamLoader> loader; + rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), loadObserver); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStreamListener> listener = loader.get(); + return channel->AsyncOpen2(listener); +} + +bool +mozJSSubScriptLoader::ReadScript(nsIURI* uri, JSContext* cx, JSObject* targetObjArg, + const nsAString& charset, const char* uriStr, + nsIIOService* serv, nsIPrincipal* principal, + bool reuseGlobal, JS::MutableHandleScript script, + JS::MutableHandleFunction function) +{ + script.set(nullptr); + function.set(nullptr); + + RootedObject target_obj(cx, targetObjArg); + + // We create a channel and call SetContentType, to avoid expensive MIME type + // lookups (bug 632490). + nsCOMPtr<nsIChannel> chan; + nsCOMPtr<nsIInputStream> instream; + nsresult rv; + rv = NS_NewChannel(getter_AddRefs(chan), + uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, + serv); + + if (NS_SUCCEEDED(rv)) { + chan->SetContentType(NS_LITERAL_CSTRING("application/javascript")); + rv = chan->Open2(getter_AddRefs(instream)); + } + + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_NOSTREAM, uri); + return false; + } + + int64_t len = -1; + + rv = chan->GetContentLength(&len); + if (NS_FAILED(rv) || len == -1) { + ReportError(cx, LOAD_ERROR_NOCONTENT, uri); + return false; + } + + if (len > INT32_MAX) { + ReportError(cx, LOAD_ERROR_CONTENTTOOBIG, uri); + return false; + } + + nsCString buf; + rv = NS_ReadInputStreamToString(instream, buf, len); + NS_ENSURE_SUCCESS(rv, false); + + return PrepareScript(uri, cx, target_obj, uriStr, charset, + buf.get(), len, + reuseGlobal, + script, function); +} + +NS_IMETHODIMP +mozJSSubScriptLoader::LoadSubScript(const nsAString& url, + HandleValue target, + const nsAString& charset, + JSContext* cx, + MutableHandleValue retval) +{ + /* + * Loads a local url and evals it into the current cx + * Synchronous (an async version would be cool too.) + * url: The url to load. Must be local so that it can be loaded + * synchronously. + * target_obj: Optional object to eval the script onto (defaults to context + * global) + * charset: Optional character set to use for reading + * returns: Whatever jsval the script pointed to by the url returns. + * Should ONLY (O N L Y !) be called from JavaScript code. + */ + LoadSubScriptOptions options(cx); + options.charset = charset; + options.target = target.isObject() ? &target.toObject() : nullptr; + return DoLoadSubScriptWithOptions(url, options, cx, retval); +} + + +NS_IMETHODIMP +mozJSSubScriptLoader::LoadSubScriptWithOptions(const nsAString& url, + HandleValue optionsVal, + JSContext* cx, + MutableHandleValue retval) +{ + if (!optionsVal.isObject()) + return NS_ERROR_INVALID_ARG; + LoadSubScriptOptions options(cx, &optionsVal.toObject()); + if (!options.Parse()) + return NS_ERROR_INVALID_ARG; + return DoLoadSubScriptWithOptions(url, options, cx, retval); +} + +nsresult +mozJSSubScriptLoader::DoLoadSubScriptWithOptions(const nsAString& url, + LoadSubScriptOptions& options, + JSContext* cx, + MutableHandleValue retval) +{ + nsresult rv = NS_OK; + + /* set the system principal if it's not here already */ + if (!mSystemPrincipal) { + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID); + if (!secman) + return NS_OK; + + rv = secman->GetSystemPrincipal(getter_AddRefs(mSystemPrincipal)); + if (NS_FAILED(rv) || !mSystemPrincipal) + return rv; + } + + RootedObject targetObj(cx); + mozJSComponentLoader* loader = mozJSComponentLoader::Get(); + rv = loader->FindTargetObject(cx, &targetObj); + NS_ENSURE_SUCCESS(rv, rv); + + // We base reusingGlobal off of what the loader told us, but we may not + // actually be using that object. + bool reusingGlobal = !JS_IsGlobalObject(targetObj); + + if (options.target) + targetObj = options.target; + + // Remember an object out of the calling compartment so that we + // can properly wrap the result later. + nsCOMPtr<nsIPrincipal> principal = mSystemPrincipal; + RootedObject result_obj(cx, targetObj); + targetObj = JS_FindCompilationScope(cx, targetObj); + if (!targetObj) + return NS_ERROR_FAILURE; + + if (targetObj != result_obj) + principal = GetObjectPrincipal(targetObj); + + /* load up the url. From here on, failures are reflected as ``custom'' + * js exceptions */ + nsCOMPtr<nsIURI> uri; + nsAutoCString uriStr; + nsAutoCString scheme; + + // Figure out who's calling us + JS::AutoFilename filename; + if (!JS::DescribeScriptedCaller(cx, &filename)) { + // No scripted frame means we don't know who's calling, bail. + return NS_ERROR_FAILURE; + } + + JSAutoCompartment ac(cx, targetObj); + + // Suppress caching if we're compiling as content. + StartupCache* cache = (principal == mSystemPrincipal) + ? StartupCache::GetSingleton() + : nullptr; + nsCOMPtr<nsIIOService> serv = do_GetService(NS_IOSERVICE_CONTRACTID); + if (!serv) { + ReportError(cx, LOAD_ERROR_NOSERVICE); + return NS_OK; + } + + // Make sure to explicitly create the URI, since we'll need the + // canonicalized spec. + rv = NS_NewURI(getter_AddRefs(uri), NS_LossyConvertUTF16toASCII(url).get(), nullptr, serv); + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_NOURI); + return NS_OK; + } + + rv = uri->GetSpec(uriStr); + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_NOSPEC); + return NS_OK; + } + + rv = uri->GetScheme(scheme); + if (NS_FAILED(rv)) { + ReportError(cx, LOAD_ERROR_NOSCHEME, uri); + return NS_OK; + } + + if (!scheme.EqualsLiteral("chrome") && !scheme.EqualsLiteral("app")) { + // This might be a URI to a local file, though! + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri); + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(innerURI); + if (!fileURL) { + ReportError(cx, LOAD_ERROR_URI_NOT_LOCAL, uri); + return NS_OK; + } + + // For file URIs prepend the filename with the filename of the + // calling script, and " -> ". See bug 418356. + nsAutoCString tmp(filename.get()); + tmp.AppendLiteral(" -> "); + tmp.Append(uriStr); + + uriStr = tmp; + } + + JSVersion version = JS_GetVersion(cx); + nsAutoCString cachePath; + cachePath.AppendPrintf("jssubloader/%d", version); + PathifyURI(uri, cachePath); + + RootedFunction function(cx); + RootedScript script(cx); + if (cache && !options.ignoreCache) { + rv = ReadCachedScript(cache, cachePath, cx, mSystemPrincipal, &script); + if (NS_FAILED(rv)) { + // ReadCachedScript may have set a pending exception. + JS_ClearPendingException(cx); + } + } + + // If we are doing an async load, trigger it and bail out. + if (!script && options.async) { + return ReadScriptAsync(uri, targetObj, options.charset, serv, + reusingGlobal, !!cache, retval); + } + + if (!script) { + if (!ReadScript(uri, cx, targetObj, options.charset, + static_cast<const char*>(uriStr.get()), serv, + principal, reusingGlobal, &script, &function)) + { + return NS_OK; + } + } else { + cache = nullptr; + } + + Unused << EvalScript(cx, targetObj, retval, uri, !!cache, script, function); + return NS_OK; +} + +/** + * Let us compile scripts from a URI off the main thread. + */ + +class ScriptPrecompiler : public nsIIncrementalStreamLoaderObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER + + ScriptPrecompiler(nsIObserver* aObserver, + nsIPrincipal* aPrincipal, + nsIChannel* aChannel) + : mObserver(aObserver) + , mPrincipal(aPrincipal) + , mChannel(aChannel) + , mScriptBuf(nullptr) + , mScriptLength(0) + {} + + static void OffThreadCallback(void* aToken, void* aData); + + /* Sends the "done" notification back. Main thread only. */ + void SendObserverNotification(); + +private: + virtual ~ScriptPrecompiler() + { + if (mScriptBuf) { + js_free(mScriptBuf); + } + } + + RefPtr<nsIObserver> mObserver; + RefPtr<nsIPrincipal> mPrincipal; + RefPtr<nsIChannel> mChannel; + char16_t* mScriptBuf; + size_t mScriptLength; +}; + +NS_IMPL_ISUPPORTS(ScriptPrecompiler, nsIIncrementalStreamLoaderObserver); + +class NotifyPrecompilationCompleteRunnable : public Runnable +{ +public: + NS_DECL_NSIRUNNABLE + + explicit NotifyPrecompilationCompleteRunnable(ScriptPrecompiler* aPrecompiler) + : mPrecompiler(aPrecompiler) + , mToken(nullptr) + {} + + void SetToken(void* aToken) { + MOZ_ASSERT(aToken && !mToken); + mToken = aToken; + } + +protected: + RefPtr<ScriptPrecompiler> mPrecompiler; + void* mToken; +}; + +/* RAII helper class to send observer notifications */ +class AutoSendObserverNotification { +public: + explicit AutoSendObserverNotification(ScriptPrecompiler* aPrecompiler) + : mPrecompiler(aPrecompiler) + {} + + ~AutoSendObserverNotification() { + if (mPrecompiler) { + mPrecompiler->SendObserverNotification(); + } + } + + void Disarm() { + mPrecompiler = nullptr; + } + +private: + ScriptPrecompiler* mPrecompiler; +}; + +NS_IMETHODIMP +NotifyPrecompilationCompleteRunnable::Run(void) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPrecompiler); + + AutoSendObserverNotification notifier(mPrecompiler); + + if (mToken) { + JSContext* cx = XPCJSContext::Get()->Context(); + NS_ENSURE_TRUE(cx, NS_ERROR_FAILURE); + JS::CancelOffThreadScript(cx, mToken); + } + + return NS_OK; +} + +NS_IMETHODIMP +ScriptPrecompiler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, + uint32_t aDataLength, + const uint8_t* aData, + uint32_t *aConsumedData) +{ + return NS_OK; +} + +NS_IMETHODIMP +ScriptPrecompiler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, + nsresult aStatus, + uint32_t aLength, + const uint8_t* aString) +{ + AutoSendObserverNotification notifier(this); + + // Just notify that we are done with this load. + NS_ENSURE_SUCCESS(aStatus, NS_OK); + + // Convert data to char16_t* and prepare to call CompileOffThread. + nsAutoString hintCharset; + nsresult rv = + nsScriptLoader::ConvertToUTF16(mChannel, aString, aLength, + hintCharset, nullptr, + mScriptBuf, mScriptLength); + + NS_ENSURE_SUCCESS(rv, NS_OK); + + // Our goal is to cache persistently the compiled script and to avoid quota + // checks. Since the caching mechanism decide the persistence type based on + // the principal, we create a new global with the app's principal. + // We then enter its compartment to compile with its principal. + AutoSafeJSContext cx; + RootedValue v(cx); + SandboxOptions sandboxOptions; + sandboxOptions.sandboxName.AssignASCII("asm.js precompilation"); + sandboxOptions.invisibleToDebugger = true; + sandboxOptions.discardSource = true; + rv = CreateSandboxObject(cx, &v, mPrincipal, sandboxOptions); + NS_ENSURE_SUCCESS(rv, NS_OK); + + JSAutoCompartment ac(cx, js::UncheckedUnwrap(&v.toObject())); + + JS::CompileOptions options(cx, JSVERSION_DEFAULT); + options.forceAsync = true; + options.installedFile = true; + + nsCOMPtr<nsIURI> uri; + mChannel->GetURI(getter_AddRefs(uri)); + nsAutoCString spec; + uri->GetSpec(spec); + options.setFile(spec.get()); + + if (!JS::CanCompileOffThread(cx, options, mScriptLength)) { + NS_WARNING("Can't compile script off thread!"); + return NS_OK; + } + + RefPtr<NotifyPrecompilationCompleteRunnable> runnable = + new NotifyPrecompilationCompleteRunnable(this); + + if (!JS::CompileOffThread(cx, options, + mScriptBuf, mScriptLength, + OffThreadCallback, + static_cast<void*>(runnable))) { + NS_WARNING("Failed to compile script off thread!"); + return NS_OK; + } + + Unused << runnable.forget(); + notifier.Disarm(); + + return NS_OK; +} + +/* static */ +void +ScriptPrecompiler::OffThreadCallback(void* aToken, void* aData) +{ + RefPtr<NotifyPrecompilationCompleteRunnable> runnable = + dont_AddRef(static_cast<NotifyPrecompilationCompleteRunnable*>(aData)); + runnable->SetToken(aToken); + + NS_DispatchToMainThread(runnable); +} + +void +ScriptPrecompiler::SendObserverNotification() +{ + MOZ_ASSERT(mChannel && mObserver); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIURI> uri; + mChannel->GetURI(getter_AddRefs(uri)); + mObserver->Observe(uri, "script-precompiled", nullptr); +} + +NS_IMETHODIMP +mozJSSubScriptLoader::PrecompileScript(nsIURI* aURI, + nsIPrincipal* aPrincipal, + nsIObserver* aObserver) +{ + nsCOMPtr<nsIChannel> channel; + nsresult rv = NS_NewChannel(getter_AddRefs(channel), + aURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<ScriptPrecompiler> loadObserver = + new ScriptPrecompiler(aObserver, aPrincipal, channel); + + nsCOMPtr<nsIIncrementalStreamLoader> loader; + rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), loadObserver); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStreamListener> listener = loader.get(); + rv = channel->AsyncOpen2(listener); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} diff --git a/js/xpconnect/loader/mozJSSubScriptLoader.h b/js/xpconnect/loader/mozJSSubScriptLoader.h new file mode 100644 index 000000000..15ed5a14c --- /dev/null +++ b/js/xpconnect/loader/mozJSSubScriptLoader.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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/. */ + +#include "nsCOMPtr.h" +#include "mozIJSSubScriptLoader.h" + +class nsIPrincipal; +class nsIURI; +class LoadSubScriptOptions; + +#define MOZ_JSSUBSCRIPTLOADER_CID \ +{ /* 829814d6-1dd2-11b2-8e08-82fa0a339b00 */ \ + 0x929814d6, \ + 0x1dd2, \ + 0x11b2, \ + {0x8e, 0x08, 0x82, 0xfa, 0x0a, 0x33, 0x9b, 0x00} \ +} + +class nsIIOService; + +class mozJSSubScriptLoader : public mozIJSSubScriptLoader +{ +public: + mozJSSubScriptLoader(); + + // all the interface method declarations... + NS_DECL_ISUPPORTS + NS_DECL_MOZIJSSUBSCRIPTLOADER + +private: + virtual ~mozJSSubScriptLoader(); + + bool ReadScript(nsIURI* uri, JSContext* cx, JSObject* target_obj, + const nsAString& charset, const char* uriStr, + nsIIOService* serv, nsIPrincipal* principal, + bool reuseGlobal, JS::MutableHandleScript script, + JS::MutableHandleFunction function); + + nsresult ReadScriptAsync(nsIURI* uri, JSObject* target_obj, + const nsAString& charset, + nsIIOService* serv, bool reuseGlobal, + bool cache, JS::MutableHandleValue retval); + + nsresult DoLoadSubScriptWithOptions(const nsAString& url, + LoadSubScriptOptions& options, + JSContext* cx, + JS::MutableHandle<JS::Value> retval); + + nsCOMPtr<nsIPrincipal> mSystemPrincipal; +}; |