1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2/* vim: set sts=2 sw=2 et tw=80: */
3/* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6"use strict";
7
8var EXPORTED_SYMBOLS = ["ExtensionUtils"];
9
10ChromeUtils.import("resource://gre/modules/Services.jsm");
11ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
12
13ChromeUtils.defineModuleGetter(this, "ConsoleAPI",
14                               "resource://gre/modules/Console.jsm");
15
16function getConsole() {
17  return new ConsoleAPI({
18    maxLogLevelPref: "extensions.webextensions.log.level",
19    prefix: "WebExtensions",
20  });
21}
22
23XPCOMUtils.defineLazyGetter(this, "console", getConsole);
24
25// xpcshell doesn't handle idle callbacks well.
26XPCOMUtils.defineLazyGetter(this, "idleTimeout",
27                            () => Services.appinfo.name === "XPCShell" ? 500 : undefined);
28
29// It would be nicer to go through `Services.appinfo`, but some tests need to be
30// able to replace that field with a custom implementation before it is first
31// called.
32// eslint-disable-next-line mozilla/use-services
33const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
34
35let nextId = 0;
36const uniqueProcessID = appinfo.uniqueProcessID;
37// Store the process ID in a 16 bit field left shifted to end of a
38// double's mantissa.
39// Note: We can't use bitwise ops here, since they truncate to a 32 bit
40// integer and we need all 53 mantissa bits.
41const processIDMask = (uniqueProcessID & 0xffff) * (2 ** 37);
42
43function getUniqueId() {
44  // Note: We can't use bitwise ops here, since they truncate to a 32 bit
45  // integer and we need all 53 mantissa bits.
46  return processIDMask + nextId++;
47}
48
49
50/**
51 * An Error subclass for which complete error messages are always passed
52 * to extensions, rather than being interpreted as an unknown error.
53 */
54class ExtensionError extends Error {}
55
56function filterStack(error) {
57  return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
58}
59
60// Run a function and report exceptions.
61function runSafeSyncWithoutClone(f, ...args) {
62  try {
63    return f(...args);
64  } catch (e) {
65    dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
66    Cu.reportError(e);
67  }
68}
69
70// Return true if the given value is an instance of the given
71// native type.
72function instanceOf(value, type) {
73  return (value && typeof value === "object" &&
74          ChromeUtils.getClassName(value) === type);
75}
76
77/**
78 * Similar to a WeakMap, but creates a new key with the given
79 * constructor if one is not present.
80 */
81class DefaultWeakMap extends WeakMap {
82  constructor(defaultConstructor = undefined, init = undefined) {
83    super(init);
84    if (defaultConstructor) {
85      this.defaultConstructor = defaultConstructor;
86    }
87  }
88
89  get(key) {
90    let value = super.get(key);
91    if (value === undefined && !this.has(key)) {
92      value = this.defaultConstructor(key);
93      this.set(key, value);
94    }
95    return value;
96  }
97}
98
99class DefaultMap extends Map {
100  constructor(defaultConstructor = undefined, init = undefined) {
101    super(init);
102    if (defaultConstructor) {
103      this.defaultConstructor = defaultConstructor;
104    }
105  }
106
107  get(key) {
108    let value = super.get(key);
109    if (value === undefined && !this.has(key)) {
110      value = this.defaultConstructor(key);
111      this.set(key, value);
112    }
113    return value;
114  }
115}
116
117const _winUtils = new DefaultWeakMap(win => {
118  return win.QueryInterface(Ci.nsIInterfaceRequestor)
119            .getInterface(Ci.nsIDOMWindowUtils);
120});
121const getWinUtils = win => _winUtils.get(win);
122
123function getInnerWindowID(window) {
124  return getWinUtils(window).currentInnerWindowID;
125}
126
127function withHandlingUserInput(window, callable) {
128  let handle = getWinUtils(window).setHandlingUserInput(true);
129  try {
130    return callable();
131  } finally {
132    handle.destruct();
133  }
134}
135
136const LISTENERS = Symbol("listeners");
137const ONCE_MAP = Symbol("onceMap");
138
139class EventEmitter {
140  constructor() {
141    this[LISTENERS] = new Map();
142    this[ONCE_MAP] = new WeakMap();
143  }
144
145  /**
146   * Adds the given function as a listener for the given event.
147   *
148   * The listener function may optionally return a Promise which
149   * resolves when it has completed all operations which event
150   * dispatchers may need to block on.
151   *
152   * @param {string} event
153   *       The name of the event to listen for.
154   * @param {function(string, ...any)} listener
155   *        The listener to call when events are emitted.
156   */
157  on(event, listener) {
158    let listeners = this[LISTENERS].get(event);
159    if (!listeners) {
160      listeners = new Set();
161      this[LISTENERS].set(event, listeners);
162    }
163
164    listeners.add(listener);
165  }
166
167  /**
168   * Removes the given function as a listener for the given event.
169   *
170   * @param {string} event
171   *       The name of the event to stop listening for.
172   * @param {function(string, ...any)} listener
173   *        The listener function to remove.
174   */
175  off(event, listener) {
176    let set = this[LISTENERS].get(event);
177    if (set) {
178      set.delete(listener);
179      set.delete(this[ONCE_MAP].get(listener));
180      if (!set.size) {
181        this[LISTENERS].delete(event);
182      }
183    }
184  }
185
186  /**
187   * Adds the given function as a listener for the given event once.
188   *
189   * @param {string} event
190   *       The name of the event to listen for.
191   * @param {function(string, ...any)} listener
192   *        The listener to call when events are emitted.
193   */
194  once(event, listener) {
195    let wrapper = (...args) => {
196      this.off(event, wrapper);
197      this[ONCE_MAP].delete(listener);
198
199      return listener(...args);
200    };
201    this[ONCE_MAP].set(listener, wrapper);
202
203    this.on(event, wrapper);
204  }
205
206
207  /**
208   * Triggers all listeners for the given event. If any listeners return
209   * a value, returns a promise which resolves when all returned
210   * promises have resolved. Otherwise, returns undefined.
211   *
212   * @param {string} event
213   *       The name of the event to emit.
214   * @param {any} args
215   *        Arbitrary arguments to pass to the listener functions, after
216   *        the event name.
217   * @returns {Promise?}
218   */
219  emit(event, ...args) {
220    let listeners = this[LISTENERS].get(event);
221
222    if (listeners) {
223      let promises = [];
224
225      for (let listener of listeners) {
226        try {
227          let result = listener(event, ...args);
228          if (result !== undefined) {
229            promises.push(result);
230          }
231        } catch (e) {
232          Cu.reportError(e);
233        }
234      }
235
236      if (promises.length) {
237        return Promise.all(promises);
238      }
239    }
240  }
241}
242
243/**
244 * A set with a limited number of slots, which flushes older entries as
245 * newer ones are added.
246 *
247 * @param {integer} limit
248 *        The maximum size to trim the set to after it grows too large.
249 * @param {integer} [slop = limit * .25]
250 *        The number of extra entries to allow in the set after it
251 *        reaches the size limit, before it is truncated to the limit.
252 * @param {iterable} [iterable]
253 *        An iterable of initial entries to add to the set.
254 */
255class LimitedSet extends Set {
256  constructor(limit, slop = Math.round(limit * .25), iterable = undefined) {
257    super(iterable);
258    this.limit = limit;
259    this.slop = slop;
260  }
261
262  truncate(limit) {
263    for (let item of this) {
264      // Live set iterators can ge relatively expensive, since they need
265      // to be updated after every modification to the set. Since
266      // breaking out of the loop early will keep the iterator alive
267      // until the next full GC, we're currently better off finishing
268      // the entire loop even after we're done truncating.
269      if (this.size > limit) {
270        this.delete(item);
271      }
272    }
273  }
274
275  add(item) {
276    if (this.size >= this.limit + this.slop && !this.has(item)) {
277      this.truncate(this.limit - 1);
278    }
279    super.add(item);
280  }
281}
282
283/**
284 * Returns a Promise which resolves when the given document's DOM has
285 * fully loaded.
286 *
287 * @param {Document} doc The document to await the load of.
288 * @returns {Promise<Document>}
289 */
290function promiseDocumentReady(doc) {
291  if (doc.readyState == "interactive" || doc.readyState == "complete") {
292    return Promise.resolve(doc);
293  }
294
295  return new Promise(resolve => {
296    doc.addEventListener("DOMContentLoaded", function onReady(event) {
297      if (event.target === event.currentTarget) {
298        doc.removeEventListener("DOMContentLoaded", onReady, true);
299        resolve(doc);
300      }
301    }, true);
302  });
303}
304
305/**
306  * Returns a Promise which resolves when the given window's document's DOM has
307  * fully loaded, the <head> stylesheets have fully loaded, and we have hit an
308  * idle time.
309  *
310  * @param {Window} window The window whose document we will await
311                           the readiness of.
312  * @returns {Promise<IdleDeadline>}
313  */
314function promiseDocumentIdle(window) {
315  return window.document.documentReadyForIdle.then(() => {
316    return new Promise(resolve =>
317      window.requestIdleCallback(resolve, {timeout: idleTimeout}));
318  });
319}
320
321/**
322 * Returns a Promise which resolves when the given document is fully
323 * loaded.
324 *
325 * @param {Document} doc The document to await the load of.
326 * @returns {Promise<Document>}
327 */
328function promiseDocumentLoaded(doc) {
329  if (doc.readyState == "complete") {
330    return Promise.resolve(doc);
331  }
332
333  return new Promise(resolve => {
334    doc.defaultView.addEventListener("load", () => resolve(doc), {once: true});
335  });
336}
337
338/**
339 * Returns a Promise which resolves when the given event is dispatched to the
340 * given element.
341 *
342 * @param {Element} element
343 *        The element on which to listen.
344 * @param {string} eventName
345 *        The event to listen for.
346 * @param {boolean} [useCapture = true]
347 *        If true, listen for the even in the capturing rather than
348 *        bubbling phase.
349 * @param {Event} [test]
350 *        An optional test function which, when called with the
351 *        observer's subject and data, should return true if this is the
352 *        expected event, false otherwise.
353 * @returns {Promise<Event>}
354 */
355function promiseEvent(element, eventName, useCapture = true, test = event => true) {
356  return new Promise(resolve => {
357    function listener(event) {
358      if (test(event)) {
359        element.removeEventListener(eventName, listener, useCapture);
360        resolve(event);
361      }
362    }
363    element.addEventListener(eventName, listener, useCapture);
364  });
365}
366
367/**
368 * Returns a Promise which resolves the given observer topic has been
369 * observed.
370 *
371 * @param {string} topic
372 *        The topic to observe.
373 * @param {function(nsISupports, string)} [test]
374 *        An optional test function which, when called with the
375 *        observer's subject and data, should return true if this is the
376 *        expected notification, false otherwise.
377 * @returns {Promise<object>}
378 */
379function promiseObserved(topic, test = () => true) {
380  return new Promise(resolve => {
381    let observer = (subject, topic, data) => {
382      if (test(subject, data)) {
383        Services.obs.removeObserver(observer, topic);
384        resolve({subject, data});
385      }
386    };
387    Services.obs.addObserver(observer, topic);
388  });
389}
390
391function getMessageManager(target) {
392  if (target.frameLoader) {
393    return target.frameLoader.messageManager;
394  }
395  return target.QueryInterface(Ci.nsIMessageSender);
396}
397
398function flushJarCache(jarPath) {
399  Services.obs.notifyObservers(null, "flush-cache-entry", jarPath);
400}
401
402/**
403 * Convert any of several different representations of a date/time to a Date object.
404 * Accepts several formats:
405 * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
406 * either a number or a string.
407 *
408 * @param {Date|string|number} date
409 *      The date to convert.
410 * @returns {Date}
411 *      A Date object
412 */
413function normalizeTime(date) {
414  // Of all the formats we accept the "number of milliseconds since the epoch as a string"
415  // is an outlier, everything else can just be passed directly to the Date constructor.
416  return new Date((typeof date == "string" && /^\d+$/.test(date))
417                        ? parseInt(date, 10) : date);
418}
419
420/**
421 * Defines a lazy getter for the given property on the given object. The
422 * first time the property is accessed, the return value of the getter
423 * is defined on the current `this` object with the given property name.
424 * Importantly, this means that a lazy getter defined on an object
425 * prototype will be invoked separately for each object instance that
426 * it's accessed on.
427 *
428 * @param {object} object
429 *        The prototype object on which to define the getter.
430 * @param {string|Symbol} prop
431 *        The property name for which to define the getter.
432 * @param {function} getter
433 *        The function to call in order to generate the final property
434 *        value.
435 */
436function defineLazyGetter(object, prop, getter) {
437  let redefine = (obj, value) => {
438    Object.defineProperty(obj, prop, {
439      enumerable: true,
440      configurable: true,
441      writable: true,
442      value,
443    });
444    return value;
445  };
446
447  Object.defineProperty(object, prop, {
448    enumerable: true,
449    configurable: true,
450
451    get() {
452      return redefine(this, getter.call(this));
453    },
454
455    set(value) {
456      redefine(this, value);
457    },
458  });
459}
460
461/**
462 * Acts as a proxy for a message manager or message manager owner, and
463 * tracks docShell swaps so that messages are always sent to the same
464 * receiver, even if it is moved to a different <browser>.
465 *
466 * @param {nsIMessageSender|Element} target
467 *        The target message manager on which to send messages, or the
468 *        <browser> element which owns it.
469 */
470class MessageManagerProxy {
471  constructor(target) {
472    this.listeners = new DefaultMap(() => new Map());
473
474    if (target instanceof Ci.nsIMessageSender) {
475      this.messageManager = target;
476    } else {
477      this.addListeners(target);
478    }
479  }
480
481  /**
482   * Disposes of the proxy object, removes event listeners, and drops
483   * all references to the underlying message manager.
484   *
485   * Must be called before the last reference to the proxy is dropped,
486   * unless the underlying message manager or <browser> is also being
487   * destroyed.
488   */
489  dispose() {
490    if (this.eventTarget) {
491      this.removeListeners(this.eventTarget);
492      this.eventTarget = null;
493    }
494    this.messageManager = null;
495  }
496
497  /**
498   * Returns true if the given target is the same as, or owns, the given
499   * message manager.
500   *
501   * @param {nsIMessageSender|MessageManagerProxy|Element} target
502   *        The message manager, MessageManagerProxy, or <browser>
503   *        element agaisnt which to match.
504   * @param {nsIMessageSender} messageManager
505   *        The message manager against which to match `target`.
506   *
507   * @returns {boolean}
508   *        True if `messageManager` is the same object as `target`, or
509   *        `target` is a MessageManagerProxy or <browser> element that
510   *        is tied to it.
511   */
512  static matches(target, messageManager) {
513    return target === messageManager || target.messageManager === messageManager;
514  }
515
516  /**
517   * @property {nsIMessageSender|null} messageManager
518   *        The message manager that is currently being proxied. This
519   *        may change during the life of the proxy object, so should
520   *        not be stored elsewhere.
521   */
522
523  /**
524   * Sends a message on the proxied message manager.
525   *
526   * @param {array} args
527   *        Arguments to be passed verbatim to the underlying
528   *        sendAsyncMessage method.
529   * @returns {undefined}
530   */
531  sendAsyncMessage(...args) {
532    if (this.messageManager) {
533      return this.messageManager.sendAsyncMessage(...args);
534    }
535
536    Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`);
537  }
538
539  get isDisconnected() {
540    return !this.messageManager;
541  }
542
543  /**
544   * Adds a message listener to the current message manager, and
545   * transfers it to the new message manager after a docShell swap.
546   *
547   * @param {string} message
548   *        The name of the message to listen for.
549   * @param {nsIMessageListener} listener
550   *        The listener to add.
551   * @param {boolean} [listenWhenClosed = false]
552   *        If true, the listener will receive messages which were sent
553   *        after the remote side of the listener began closing.
554   */
555  addMessageListener(message, listener, listenWhenClosed = false) {
556    this.messageManager.addMessageListener(message, listener, listenWhenClosed);
557    this.listeners.get(message).set(listener, listenWhenClosed);
558  }
559
560  /**
561   * Adds a message listener from the current message manager.
562   *
563   * @param {string} message
564   *        The name of the message to stop listening for.
565   * @param {nsIMessageListener} listener
566   *        The listener to remove.
567   */
568  removeMessageListener(message, listener) {
569    this.messageManager.removeMessageListener(message, listener);
570
571    let listeners = this.listeners.get(message);
572    listeners.delete(listener);
573    if (!listeners.size) {
574      this.listeners.delete(message);
575    }
576  }
577
578  /**
579   * @private
580   * Iterates over all of the currently registered message listeners.
581   */
582  * iterListeners() {
583    for (let [message, listeners] of this.listeners) {
584      for (let [listener, listenWhenClosed] of listeners) {
585        yield {message, listener, listenWhenClosed};
586      }
587    }
588  }
589
590  /**
591   * @private
592   * Adds docShell swap listeners to the message manager owner.
593   *
594   * @param {Element} target
595   *        The target element.
596   */
597  addListeners(target) {
598    target.addEventListener("SwapDocShells", this);
599
600    this.eventTarget = target;
601    this.messageManager = target.messageManager;
602
603    for (let {message, listener, listenWhenClosed} of this.iterListeners()) {
604      this.messageManager.addMessageListener(message, listener, listenWhenClosed);
605    }
606  }
607
608  /**
609   * @private
610   * Removes docShell swap listeners to the message manager owner.
611   *
612   * @param {Element} target
613   *        The target element.
614   */
615  removeListeners(target) {
616    target.removeEventListener("SwapDocShells", this);
617
618    for (let {message, listener} of this.iterListeners()) {
619      this.messageManager.removeMessageListener(message, listener);
620    }
621  }
622
623  handleEvent(event) {
624    if (event.type == "SwapDocShells") {
625      this.removeListeners(this.eventTarget);
626      this.addListeners(event.detail);
627    }
628  }
629}
630
631function checkLoadURL(url, principal, options) {
632  let ssm = Services.scriptSecurityManager;
633
634  let flags = ssm.STANDARD;
635  if (!options.allowScript) {
636    flags |= ssm.DISALLOW_SCRIPT;
637  }
638  if (!options.allowInheritsPrincipal) {
639    flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
640  }
641  if (options.dontReportErrors) {
642    flags |= ssm.DONT_REPORT_ERRORS;
643  }
644
645  try {
646    ssm.checkLoadURIWithPrincipal(principal,
647                                  Services.io.newURI(url),
648                                  flags);
649  } catch (e) {
650    return false;
651  }
652  return true;
653}
654
655var ExtensionUtils = {
656  checkLoadURL,
657  defineLazyGetter,
658  flushJarCache,
659  getConsole,
660  getInnerWindowID,
661  getMessageManager,
662  getUniqueId,
663  filterStack,
664  getWinUtils,
665  instanceOf,
666  normalizeTime,
667  promiseDocumentIdle,
668  promiseDocumentLoaded,
669  promiseDocumentReady,
670  promiseEvent,
671  promiseObserved,
672  runSafeSyncWithoutClone,
673  withHandlingUserInput,
674  DefaultMap,
675  DefaultWeakMap,
676  EventEmitter,
677  ExtensionError,
678  LimitedSet,
679  MessageManagerProxy,
680};
681