1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4"use strict";
5
6/**
7 * This module contains utilities and base classes for logic which is
8 * common between the parent and child process, and in particular
9 * between ExtensionParent.jsm and ExtensionChild.jsm.
10 */
11
12const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
13
14/* exported ExtensionCommon */
15
16this.EXPORTED_SYMBOLS = ["ExtensionCommon"];
17
18Cu.import("resource://gre/modules/Services.jsm");
19Cu.import("resource://gre/modules/XPCOMUtils.jsm");
20
21XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
22                                  "resource://gre/modules/MessageChannel.jsm");
23XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
24                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
25XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
26                                  "resource://gre/modules/Schemas.jsm");
27
28Cu.import("resource://gre/modules/ExtensionUtils.jsm");
29
30var {
31  EventEmitter,
32  ExtensionError,
33  SpreadArgs,
34  getConsole,
35  getInnerWindowID,
36  getUniqueId,
37  runSafeSync,
38  runSafeSyncWithoutClone,
39  instanceOf,
40} = ExtensionUtils;
41
42XPCOMUtils.defineLazyGetter(this, "console", getConsole);
43
44class BaseContext {
45  constructor(envType, extension) {
46    this.envType = envType;
47    this.onClose = new Set();
48    this.checkedLastError = false;
49    this._lastError = null;
50    this.contextId = getUniqueId();
51    this.unloaded = false;
52    this.extension = extension;
53    this.jsonSandbox = null;
54    this.active = true;
55    this.incognito = null;
56    this.messageManager = null;
57    this.docShell = null;
58    this.contentWindow = null;
59    this.innerWindowID = 0;
60  }
61
62  setContentWindow(contentWindow) {
63    let {document} = contentWindow;
64    let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
65                                .getInterface(Ci.nsIDocShell);
66
67    this.innerWindowID = getInnerWindowID(contentWindow);
68    this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
69                                  .getInterface(Ci.nsIContentFrameMessageManager);
70
71    if (this.incognito == null) {
72      this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
73    }
74
75    MessageChannel.setupMessageManagers([this.messageManager]);
76
77    let onPageShow = event => {
78      if (!event || event.target === document) {
79        this.docShell = docShell;
80        this.contentWindow = contentWindow;
81        this.active = true;
82      }
83    };
84    let onPageHide = event => {
85      if (!event || event.target === document) {
86        // Put this off until the next tick.
87        Promise.resolve().then(() => {
88          this.docShell = null;
89          this.contentWindow = null;
90          this.active = false;
91        });
92      }
93    };
94
95    onPageShow();
96    contentWindow.addEventListener("pagehide", onPageHide, true);
97    contentWindow.addEventListener("pageshow", onPageShow, true);
98    this.callOnClose({
99      close: () => {
100        onPageHide();
101        if (this.active) {
102          contentWindow.removeEventListener("pagehide", onPageHide, true);
103          contentWindow.removeEventListener("pageshow", onPageShow, true);
104        }
105      },
106    });
107  }
108
109  get cloneScope() {
110    throw new Error("Not implemented");
111  }
112
113  get principal() {
114    throw new Error("Not implemented");
115  }
116
117  runSafe(...args) {
118    if (this.unloaded) {
119      Cu.reportError("context.runSafe called after context unloaded");
120    } else if (!this.active) {
121      Cu.reportError("context.runSafe called while context is inactive");
122    } else {
123      return runSafeSync(this, ...args);
124    }
125  }
126
127  runSafeWithoutClone(...args) {
128    if (this.unloaded) {
129      Cu.reportError("context.runSafeWithoutClone called after context unloaded");
130    } else if (!this.active) {
131      Cu.reportError("context.runSafeWithoutClone called while context is inactive");
132    } else {
133      return runSafeSyncWithoutClone(...args);
134    }
135  }
136
137  checkLoadURL(url, options = {}) {
138    let ssm = Services.scriptSecurityManager;
139
140    let flags = ssm.STANDARD;
141    if (!options.allowScript) {
142      flags |= ssm.DISALLOW_SCRIPT;
143    }
144    if (!options.allowInheritsPrincipal) {
145      flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
146    }
147    if (options.dontReportErrors) {
148      flags |= ssm.DONT_REPORT_ERRORS;
149    }
150
151    try {
152      ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags);
153    } catch (e) {
154      return false;
155    }
156    return true;
157  }
158
159  /**
160   * Safely call JSON.stringify() on an object that comes from an
161   * extension.
162   *
163   * @param {array<any>} args Arguments for JSON.stringify()
164   * @returns {string} The stringified representation of obj
165   */
166  jsonStringify(...args) {
167    if (!this.jsonSandbox) {
168      this.jsonSandbox = Cu.Sandbox(this.principal, {
169        sameZoneAs: this.cloneScope,
170        wantXrays: false,
171      });
172    }
173
174    return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
175  }
176
177  callOnClose(obj) {
178    this.onClose.add(obj);
179  }
180
181  forgetOnClose(obj) {
182    this.onClose.delete(obj);
183  }
184
185  /**
186   * A wrapper around MessageChannel.sendMessage which adds the extension ID
187   * to the recipient object, and ensures replies are not processed after the
188   * context has been unloaded.
189   *
190   * @param {nsIMessageManager} target
191   * @param {string} messageName
192   * @param {object} data
193   * @param {object} [options]
194   * @param {object} [options.sender]
195   * @param {object} [options.recipient]
196   *
197   * @returns {Promise}
198   */
199  sendMessage(target, messageName, data, options = {}) {
200    options.recipient = options.recipient || {};
201    options.sender = options.sender || {};
202
203    options.recipient.extensionId = this.extension.id;
204    options.sender.extensionId = this.extension.id;
205    options.sender.contextId = this.contextId;
206
207    return MessageChannel.sendMessage(target, messageName, data, options);
208  }
209
210  get lastError() {
211    this.checkedLastError = true;
212    return this._lastError;
213  }
214
215  set lastError(val) {
216    this.checkedLastError = false;
217    this._lastError = val;
218  }
219
220  /**
221   * Normalizes the given error object for use by the target scope. If
222   * the target is an error object which belongs to that scope, it is
223   * returned as-is. If it is an ordinary object with a `message`
224   * property, it is converted into an error belonging to the target
225   * scope. If it is an Error object which does *not* belong to the
226   * clone scope, it is reported, and converted to an unexpected
227   * exception error.
228   *
229   * @param {Error|object} error
230   * @returns {Error}
231   */
232  normalizeError(error) {
233    if (error instanceof this.cloneScope.Error) {
234      return error;
235    }
236    let message;
237    if (instanceOf(error, "Object") || error instanceof ExtensionError) {
238      message = error.message;
239    } else if (typeof error == "object" &&
240        this.principal.subsumes(Cu.getObjectPrincipal(error))) {
241      message = error.message;
242    } else {
243      Cu.reportError(error);
244    }
245    message = message || "An unexpected error occurred";
246    return new this.cloneScope.Error(message);
247  }
248
249  /**
250   * Sets the value of `.lastError` to `error`, calls the given
251   * callback, and reports an error if the value has not been checked
252   * when the callback returns.
253   *
254   * @param {object} error An object with a `message` property. May
255   *     optionally be an `Error` object belonging to the target scope.
256   * @param {function} callback The callback to call.
257   * @returns {*} The return value of callback.
258   */
259  withLastError(error, callback) {
260    this.lastError = this.normalizeError(error);
261    try {
262      return callback();
263    } finally {
264      if (!this.checkedLastError) {
265        Cu.reportError(`Unchecked lastError value: ${this.lastError}`);
266      }
267      this.lastError = null;
268    }
269  }
270
271  /**
272   * Wraps the given promise so it can be safely returned to extension
273   * code in this context.
274   *
275   * If `callback` is provided, however, it is used as a completion
276   * function for the promise, and no promise is returned. In this case,
277   * the callback is called when the promise resolves or rejects. In the
278   * latter case, `lastError` is set to the rejection value, and the
279   * callback function must check `browser.runtime.lastError` or
280   * `extension.runtime.lastError` in order to prevent it being reported
281   * to the console.
282   *
283   * @param {Promise} promise The promise with which to wrap the
284   *     callback. May resolve to a `SpreadArgs` instance, in which case
285   *     each element will be used as a separate argument.
286   *
287   *     Unless the promise object belongs to the cloneScope global, its
288   *     resolution value is cloned into cloneScope prior to calling the
289   *     `callback` function or resolving the wrapped promise.
290   *
291   * @param {function} [callback] The callback function to wrap
292   *
293   * @returns {Promise|undefined} If callback is null, a promise object
294   *     belonging to the target scope. Otherwise, undefined.
295   */
296  wrapPromise(promise, callback = null) {
297    let runSafe = this.runSafe.bind(this);
298    if (promise instanceof this.cloneScope.Promise) {
299      runSafe = this.runSafeWithoutClone.bind(this);
300    }
301
302    if (callback) {
303      promise.then(
304        args => {
305          if (this.unloaded) {
306            dump(`Promise resolved after context unloaded\n`);
307          } else if (!this.active) {
308            dump(`Promise resolved while context is inactive\n`);
309          } else if (args instanceof SpreadArgs) {
310            runSafe(callback, ...args);
311          } else {
312            runSafe(callback, args);
313          }
314        },
315        error => {
316          this.withLastError(error, () => {
317            if (this.unloaded) {
318              dump(`Promise rejected after context unloaded\n`);
319            } else if (!this.active) {
320              dump(`Promise rejected while context is inactive\n`);
321            } else {
322              this.runSafeWithoutClone(callback);
323            }
324          });
325        });
326    } else {
327      return new this.cloneScope.Promise((resolve, reject) => {
328        promise.then(
329          value => {
330            if (this.unloaded) {
331              dump(`Promise resolved after context unloaded\n`);
332            } else if (!this.active) {
333              dump(`Promise resolved while context is inactive\n`);
334            } else if (value instanceof SpreadArgs) {
335              runSafe(resolve, value.length == 1 ? value[0] : value);
336            } else {
337              runSafe(resolve, value);
338            }
339          },
340          value => {
341            if (this.unloaded) {
342              dump(`Promise rejected after context unloaded: ${value && value.message}\n`);
343            } else if (!this.active) {
344              dump(`Promise rejected while context is inactive: ${value && value.message}\n`);
345            } else {
346              this.runSafeWithoutClone(reject, this.normalizeError(value));
347            }
348          });
349      });
350    }
351  }
352
353  unload() {
354    this.unloaded = true;
355
356    MessageChannel.abortResponses({
357      extensionId: this.extension.id,
358      contextId: this.contextId,
359    });
360
361    for (let obj of this.onClose) {
362      obj.close();
363    }
364  }
365
366  /**
367   * A simple proxy for unload(), for use with callOnClose().
368   */
369  close() {
370    this.unload();
371  }
372}
373
374/**
375 * An object that runs the implementation of a schema API. Instantiations of
376 * this interfaces are used by Schemas.jsm.
377 *
378 * @interface
379 */
380class SchemaAPIInterface {
381  /**
382   * Calls this as a function that returns its return value.
383   *
384   * @abstract
385   * @param {Array} args The parameters for the function.
386   * @returns {*} The return value of the invoked function.
387   */
388  callFunction(args) {
389    throw new Error("Not implemented");
390  }
391
392  /**
393   * Calls this as a function and ignores its return value.
394   *
395   * @abstract
396   * @param {Array} args The parameters for the function.
397   */
398  callFunctionNoReturn(args) {
399    throw new Error("Not implemented");
400  }
401
402  /**
403   * Calls this as a function that completes asynchronously.
404   *
405   * @abstract
406   * @param {Array} args The parameters for the function.
407   * @param {function(*)} [callback] The callback to be called when the function
408   *     completes.
409   * @returns {Promise|undefined} Must be void if `callback` is set, and a
410   *     promise otherwise. The promise is resolved when the function completes.
411   */
412  callAsyncFunction(args, callback) {
413    throw new Error("Not implemented");
414  }
415
416  /**
417   * Retrieves the value of this as a property.
418   *
419   * @abstract
420   * @returns {*} The value of the property.
421   */
422  getProperty() {
423    throw new Error("Not implemented");
424  }
425
426  /**
427   * Assigns the value to this as property.
428   *
429   * @abstract
430   * @param {string} value The new value of the property.
431   */
432  setProperty(value) {
433    throw new Error("Not implemented");
434  }
435
436  /**
437   * Registers a `listener` to this as an event.
438   *
439   * @abstract
440   * @param {function} listener The callback to be called when the event fires.
441   * @param {Array} args Extra parameters for EventManager.addListener.
442   * @see EventManager.addListener
443   */
444  addListener(listener, args) {
445    throw new Error("Not implemented");
446  }
447
448  /**
449   * Checks whether `listener` is listening to this as an event.
450   *
451   * @abstract
452   * @param {function} listener The event listener.
453   * @returns {boolean} Whether `listener` is registered with this as an event.
454   * @see EventManager.hasListener
455   */
456  hasListener(listener) {
457    throw new Error("Not implemented");
458  }
459
460  /**
461   * Unregisters `listener` from this as an event.
462   *
463   * @abstract
464   * @param {function} listener The event listener.
465   * @see EventManager.removeListener
466   */
467  removeListener(listener) {
468    throw new Error("Not implemented");
469  }
470}
471
472/**
473 * An object that runs a locally implemented API.
474 */
475class LocalAPIImplementation extends SchemaAPIInterface {
476  /**
477   * Constructs an implementation of the `name` method or property of `pathObj`.
478   *
479   * @param {object} pathObj The object containing the member with name `name`.
480   * @param {string} name The name of the implemented member.
481   * @param {BaseContext} context The context in which the schema is injected.
482   */
483  constructor(pathObj, name, context) {
484    super();
485    this.pathObj = pathObj;
486    this.name = name;
487    this.context = context;
488  }
489
490  callFunction(args) {
491    return this.pathObj[this.name](...args);
492  }
493
494  callFunctionNoReturn(args) {
495    this.pathObj[this.name](...args);
496  }
497
498  callAsyncFunction(args, callback) {
499    let promise;
500    try {
501      promise = this.pathObj[this.name](...args) || Promise.resolve();
502    } catch (e) {
503      promise = Promise.reject(e);
504    }
505    return this.context.wrapPromise(promise, callback);
506  }
507
508  getProperty() {
509    return this.pathObj[this.name];
510  }
511
512  setProperty(value) {
513    this.pathObj[this.name] = value;
514  }
515
516  addListener(listener, args) {
517    try {
518      this.pathObj[this.name].addListener.call(null, listener, ...args);
519    } catch (e) {
520      throw this.context.normalizeError(e);
521    }
522  }
523
524  hasListener(listener) {
525    return this.pathObj[this.name].hasListener.call(null, listener);
526  }
527
528  removeListener(listener) {
529    this.pathObj[this.name].removeListener.call(null, listener);
530  }
531}
532
533/**
534 * This object loads the ext-*.js scripts that define the extension API.
535 *
536 * This class instance is shared with the scripts that it loads, so that the
537 * ext-*.js scripts and the instantiator can communicate with each other.
538 */
539class SchemaAPIManager extends EventEmitter {
540  /**
541   * @param {string} processType
542   *     "main" - The main, one and only chrome browser process.
543   *     "addon" - An addon process.
544   *     "content" - A content process.
545   */
546  constructor(processType) {
547    super();
548    this.processType = processType;
549    this.global = this._createExtGlobal();
550    this._scriptScopes = [];
551    this._schemaApis = {
552      addon_parent: [],
553      addon_child: [],
554      content_parent: [],
555      content_child: [],
556    };
557  }
558
559  /**
560   * Create a global object that is used as the shared global for all ext-*.js
561   * scripts that are loaded via `loadScript`.
562   *
563   * @returns {object} A sandbox that is used as the global by `loadScript`.
564   */
565  _createExtGlobal() {
566    let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
567      wantXrays: false,
568      sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
569    });
570
571    Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
572
573    XPCOMUtils.defineLazyGetter(global, "console", getConsole);
574
575    XPCOMUtils.defineLazyModuleGetter(global, "require",
576                                      "resource://devtools/shared/Loader.jsm");
577
578    return global;
579  }
580
581  /**
582   * Load an ext-*.js script. The script runs in its own scope, if it wishes to
583   * share state with another script it can assign to the `global` variable. If
584   * it wishes to communicate with this API manager, use `extensions`.
585   *
586   * @param {string} scriptUrl The URL of the ext-*.js script.
587   */
588  loadScript(scriptUrl) {
589    // Create the object in the context of the sandbox so that the script runs
590    // in the sandbox's context instead of here.
591    let scope = Cu.createObjectIn(this.global);
592
593    Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
594
595    // Save the scope to avoid it being garbage collected.
596    this._scriptScopes.push(scope);
597  }
598
599  /**
600   * Called by an ext-*.js script to register an API.
601   *
602   * @param {string} namespace The API namespace.
603   *     Intended to match the namespace of the generated API, but not used at
604   *     the moment - see bugzil.la/1295774.
605   * @param {string} envType Restricts the API to contexts that run in the
606   *    given environment. Must be one of the following:
607   *     - "addon_parent" - addon APIs that runs in the main process.
608   *     - "addon_child" - addon APIs that runs in an addon process.
609   *     - "content_parent" - content script APIs that runs in the main process.
610   *     - "content_child" - content script APIs that runs in a content process.
611   * @param {function(BaseContext)} getAPI A function that returns an object
612   *     that will be merged with |chrome| and |browser|. The next example adds
613   *     the create, update and remove methods to the tabs API.
614   *
615   *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
616   *       tabs: { create, update },
617   *     }));
618   *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
619   *       tabs: { remove },
620   *     }));
621   */
622  registerSchemaAPI(namespace, envType, getAPI) {
623    this._schemaApis[envType].push({namespace, getAPI});
624  }
625
626  /**
627   * Exports all registered scripts to `obj`.
628   *
629   * @param {BaseContext} context The context for which the API bindings are
630   *     generated.
631   * @param {object} obj The destination of the API.
632   */
633  generateAPIs(context, obj) {
634    let apis = this._schemaApis[context.envType];
635    if (!apis) {
636      Cu.reportError(`No APIs have been registered for ${context.envType}`);
637      return;
638    }
639    SchemaAPIManager.generateAPIs(context, apis, obj);
640  }
641
642  /**
643   * Mash together all the APIs from `apis` into `obj`.
644   *
645   * @param {BaseContext} context The context for which the API bindings are
646   *     generated.
647   * @param {Array} apis A list of objects, see `registerSchemaAPI`.
648   * @param {object} obj The destination of the API.
649   */
650  static generateAPIs(context, apis, obj) {
651    // Recursively copy properties from source to dest.
652    function copy(dest, source) {
653      for (let prop in source) {
654        let desc = Object.getOwnPropertyDescriptor(source, prop);
655        if (typeof(desc.value) == "object") {
656          if (!(prop in dest)) {
657            dest[prop] = {};
658          }
659          copy(dest[prop], source[prop]);
660        } else {
661          Object.defineProperty(dest, prop, desc);
662        }
663      }
664    }
665
666    for (let api of apis) {
667      if (Schemas.checkPermissions(api.namespace, context.extension)) {
668        api = api.getAPI(context);
669        copy(obj, api);
670      }
671    }
672  }
673}
674
675const ExtensionCommon = {
676  BaseContext,
677  LocalAPIImplementation,
678  SchemaAPIInterface,
679  SchemaAPIManager,
680};
681