summaryrefslogtreecommitdiffstats
path: root/mailnews/jsaccount/modules/JSAccountUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/jsaccount/modules/JSAccountUtils.jsm')
-rw-r--r--mailnews/jsaccount/modules/JSAccountUtils.jsm285
1 files changed, 285 insertions, 0 deletions
diff --git a/mailnews/jsaccount/modules/JSAccountUtils.jsm b/mailnews/jsaccount/modules/JSAccountUtils.jsm
new file mode 100644
index 000000000..b5ce34682
--- /dev/null
+++ b/mailnews/jsaccount/modules/JSAccountUtils.jsm
@@ -0,0 +1,285 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 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/. */
+
+/**
+ * This file implements helper methods to make the transition of base mailnews
+ * objects from JS to C++ easier, and also to allow creating specialized
+ * versions of those accounts using only JS XPCOM implementations.
+ *
+ * In C++ land, the XPCOM component is a generic C++ class that does nothing
+ * but delegate any calls to interfaces known in C++ to either the generic
+ * C++ implementation (such as nsMsgIncomingServer.cpp) or a JavaScript
+ * implementation of those methods. Those delegations could be used for either
+ * method-by-method replacement of the generic C++ methods with JavaScript
+ * versions, or for specialization of the generic class using JavaScript to
+ * implement a particular class type. We use a C++ class as the main XPCOM
+ * version for two related reasons: First, we do not want to go through a
+ * C++->js->C++ XPCOM transition just to execute a C++ method. Second, C++
+ * inheritance is different from JS inheritance, and sometimes the C++ code
+ * will ignore the XPCOM parts of the JS, and just execute using C++
+ * inheritance.
+ *
+ * In JavaScript land, the implementation currently uses the XPCOM object for
+ * JavaScript calls, with the last object in the prototype chain defaulting
+ * to calling using the CPP object, specified in an instance-specific
+ * this.cppBase object.
+ *
+ * Examples of use can be found in the test files for jsaccount stuff.
+ */
+
+const EXPORTED_SYMBOLS = ["JSAccountUtils"];
+var JSAccountUtils = {};
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Logger definitions.
+const LOGGER_NAME = "JsAccount";
+const PREF_BRANCH_LOG = "mailnews.jsaccount.log.";
+const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
+const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
+
+// Set default logging levels.
+const LOG_LEVEL_DEFAULT = "Info"
+const LOG_DUMP_DEFAULT = true;
+
+// Logging usage: set mailnews.jsaccount.log.level to the word "Debug" to
+// increase logging level.
+
+var log = configureLogging();
+
+/**
+ *
+ * Generic factory to create XPCOM components under JsAccount.
+ *
+ * @param aProperties This a a const JS object that describes the specific
+ * details of a particular JsAccount XPCOM object:
+ * {
+ * baseContractID: string contractID used to create the base generic C++
+ * object. This object must implement the interfaces in
+ * baseInterfaces, plus msgIOverride.
+ *
+ * baseInterfaces: JS array of interfaces implemented by the base, generic
+ * C++ object.
+ *
+ * extraInterfaces: JS array of additional interfaces implemented by the
+ * component (accessed using getInterface())
+ *
+ * contractID: string contract ID for the JS object that will be
+ * created by the factory.
+ *
+ * classID: Components.ID(CID) for the JS object that will be
+ * created by the factory, where CID is a string uuid.
+ * }
+ *
+ * @param aJsDelegateConstructor: a JS contructor class, called using new,
+ * that will create the JS object to which
+ * XPCOM methods calls will be delegated.
+ */
+
+JSAccountUtils.jaFactory = function (aProperties, aJsDelegateConstructor)
+{
+ let factory = {};
+ factory.QueryInterface = XPCOMUtils.generateQI([Ci.nsIFactory]);
+ factory.lockFactory = function() {};
+
+ factory.createInstance = function(outer, iid)
+ {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+
+ // C++ delegator class.
+ let delegator = Cc[aProperties.baseContractID]
+ .createInstance(Ci.msgIOverride);
+
+ // Make sure the delegator JS wrapper knows its interfaces.
+ aProperties.baseInterfaces.forEach(iface => delegator instanceof iface);
+
+ // JavaScript overrides of base class functions.
+ let jsDelegate = new aJsDelegateConstructor(delegator, aProperties.baseInterfaces);
+ delegator.jsDelegate = jsDelegate;
+
+ // Get the delegate list for this current class. Use OwnProperty in case it
+ // inherits from another JsAccount class.
+
+ let delegateList = null;
+ if (Object.getPrototypeOf(jsDelegate).hasOwnProperty("delegateList")) {
+ delegateList = Object.getPrototypeOf(jsDelegate).delegateList;
+ }
+ if (delegateList instanceof Ci.msgIDelegateList) {
+ delegator.methodsToDelegate = delegateList;
+ } else {
+ // Lazily create and populate the list of methods to delegate.
+ log.info("creating delegate list for contractID " + aProperties.contractID);
+ let delegateList = delegator.methodsToDelegate;
+ Object.keys(delegator).forEach(name => {log.debug("delegator has key " + name);});
+
+ // jsMethods contains the methods that may be targets of the C++ delegation to JS.
+ let jsMethods = Object.getPrototypeOf(delegator.jsDelegate.wrappedJSObject);
+ for (let name in jsMethods)
+ {
+ log.debug("processing jsDelegate method: " + name);
+ if (name[0] == '_') { // don't bother with methods explicitly marked as internal.
+ log.debug("skipping " + name);
+ continue;
+ }
+ // Other methods to skip.
+ if (["QueryInterface", // nsISupports
+ "methodsToDelegate", "jsDelegate", "cppBase", // msgIOverride
+ "delegateList", "wrappedJSObject", // non-XPCOM methods to skip
+ ].includes(name)) {
+ log.debug("skipping " + name);
+ continue;
+ }
+
+ let jsDescriptor = getPropertyDescriptor(jsMethods, name);
+ if (!jsDescriptor) {
+ log.debug("no jsDescriptor for " + name);
+ continue;
+ }
+ let cppDescriptor = Object.getOwnPropertyDescriptor(delegator, name);
+ if (!cppDescriptor) {
+ log.debug("no cppDescriptor found for " + name);
+ // It is OK for jsMethods to have methods that are not used in override of C++.
+ continue;
+ }
+
+ let upperCaseName = name[0].toUpperCase() + name.substr(1);
+ if ('value' in jsDescriptor) {
+ log.info("delegating " + upperCaseName);
+ delegateList.add(upperCaseName);
+ }
+ else {
+ if (jsDescriptor.set) {
+ log.info("delegating Set" + upperCaseName);
+ delegateList.add("Set" + upperCaseName);
+ }
+ if (jsDescriptor.get) {
+ log.info("delegating Get" + upperCaseName);
+ delegateList.add("Get" + upperCaseName);
+ }
+ }
+ }
+
+ // Save the delegate list for reuse, statically for all instances.
+ Object.getPrototypeOf(jsDelegate).delegateList = delegateList;
+ }
+
+ for (let iface of aProperties.baseInterfaces)
+ if (iid.equals(iface)) {
+ log.debug("Successfully returning delegator " + delegator);
+ return delegator;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ };
+
+ return factory;
+}
+
+/**
+ * Create a JS object that contains calls to each of the methods in a CPP
+ * base class, that will reference the cpp object defined on a particular
+ * instance of the object. This is intended to be the last item in the
+ * prototype chain for a JsAccount implementation.
+ *
+ * @param aProperties see definition in jsFactory above
+ *
+ * @returns a JS object suitable as the prototype of a JsAccount implementation.
+ */
+JSAccountUtils.makeCppDelegator = function(aProperties)
+{
+ log.info("Making cppDelegator for contractID " + aProperties.contractID);
+ let cppDelegator = {};
+ let cppDummy = Cc[aProperties.baseContractID].createInstance(Ci.nsISupports);
+ // Add methods from all interfaces.
+ for (let iface of aProperties.baseInterfaces)
+ cppDummy instanceof Ci[iface];
+
+ for (let method in cppDummy) {
+ // skip nsISupports and msgIOverride methods
+ if (["QueryInterface", "methodsToDelegate", "jsDelegate", "cppBase", "getInterface"].includes(method)) {
+ log.config("Skipping " + method + "\n");
+ continue;
+ }
+ log.config("processing " + method + "\n");
+ let descriptor = Object.getOwnPropertyDescriptor(cppDummy, method);
+ let property = { enumerable: true };
+ // We must use Immediately Invoked Function Expressions to pass method, otherwise it is
+ // a closure containing just the last value it was set to.
+ if ('value' in descriptor) {
+ log.debug("Adding value for " + method);
+ property.value = function(aMethod) {
+ return function(...args) {
+ return Reflect.apply(this.cppBase[aMethod], undefined, args);
+ };
+ }(method);
+ }
+ if (descriptor.set) {
+ log.debug("Adding setter for " + method);
+ property.set = function(aMethod) {
+ return function(aVal) {
+ this.cppBase[aMethod] = aVal;
+ };
+ }(method);
+ }
+ if (descriptor.get) {
+ log.debug("Adding getter for " + method);
+ property.get = function(aMethod) {
+ return function() {
+ return this.cppBase[aMethod];
+ };
+ }(method);
+ }
+ Object.defineProperty(cppDelegator, method, property);
+ }
+ return cppDelegator;
+}
+
+// Utility functions.
+
+// Iterate over an object and its prototypes to get a property descriptor.
+function getPropertyDescriptor(obj, name)
+{
+ let descriptor = null;
+
+ // Eventually we will hit an object that will delegate JS calls to a CPP
+ // object, which are not JS overrides of CPP methods. Locate this item, and
+ // skip, because it will not have _JsPrototypeToDelegate defined.
+ while (obj && ("_JsPrototypeToDelegate" in obj)) {
+ descriptor = Object.getOwnPropertyDescriptor(obj, name);
+ if (descriptor)
+ break;
+ obj = Object.getPrototypeOf(obj);
+ }
+ return descriptor;
+}
+
+// Configure the logger based on the preferences.
+function configureLogging()
+{
+ let log = Log.repository.getLogger(LOGGER_NAME);
+
+ // Log messages need to go to the browser console.
+ let consoleAppender = new Log.ConsoleAppender(new Log.BasicFormatter());
+ log.addAppender(consoleAppender);
+
+ // Make sure the logger keeps up with the logging level preference.
+ log.level = Log.Level[Preferences.get(PREF_LOG_LEVEL, LOG_LEVEL_DEFAULT)];
+
+ // If enabled in the preferences, add a dump appender.
+ let logDumping = Preferences.get(PREF_LOG_DUMP, LOG_DUMP_DEFAULT);
+ if (logDumping) {
+ let dumpAppender = new Log.DumpAppender(new Log.BasicFormatter());
+ log.addAppender(dumpAppender);
+ }
+ return log;
+}