summaryrefslogtreecommitdiffstats
path: root/mailnews/jsaccount/modules/JSAccountUtils.jsm
blob: b5ce34682f7df1a2a49839f6e81338a03eb7d700 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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;
}