Bug 1313767: Deobfuscate events/core.js r?rpl draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 25 Mar 2017 15:06:25 -0700
changeset 551372 26e8bd522e086b0e089f2bc1885ef932c9e1f249
parent 551371 6088c6885dd9e8f4d226654074d14af21ee39e5f
child 551373 6209308005f1d36dfbd95a0976c6712ccfecf53c
push id51040
push usermaglione.k@gmail.com
push dateSat, 25 Mar 2017 22:06:50 +0000
reviewersrpl
bugs1313767
milestone55.0a1
Bug 1313767: Deobfuscate events/core.js r?rpl MozReview-Commit-ID: Ktj70L3DcQF
addon-sdk/source/lib/sdk/event/core.js
addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
addon-sdk/source/lib/sdk/util/object.js
addon-sdk/source/lib/toolkit/loader.js
addon-sdk/source/test/tabs/test-firefox-tabs.js
--- a/addon-sdk/source/lib/sdk/event/core.js
+++ b/addon-sdk/source/lib/sdk/event/core.js
@@ -5,69 +5,66 @@
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.';
 const BAD_LISTENER = 'The event listener must be a function.';
 
-const { ns } = require('../core/namespace');
-
-const event = ns();
+const { DefaultMap, DefaultWeakMap } = require('../util/object');
 
 const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/;
 exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN;
 
-// Utility function to access given event `target` object's event listeners for
-// the specific event `type`. If listeners for this type does not exists they
-// will be created.
-const observers = function observers(target, type) {
-  if (!target) throw TypeError("Event target must be an object");
-  let listeners = event(target);
-  return type in listeners ? listeners[type] : listeners[type] = [];
-};
+// Count of total listeners ever added.
+// This is used to keep track of when a listener was added, which can
+// have an effect on when it is and isn't dispatched. See comments in
+// emitOnObject for more details.
+let listenerCount = 0;
+
+const observers = new DefaultWeakMap(() => {
+  return new DefaultMap(() => new Map());
+});
 
 /**
  * Registers an event `listener` that is called every time events of
  * specified `type` is emitted on the given event `target`.
  * @param {Object} target
  *    Event target object.
  * @param {String} type
  *    The type of event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function on(target, type, listener) {
   if (typeof(listener) !== 'function')
     throw new Error(BAD_LISTENER);
 
-  let listeners = observers(target, type);
-  if (!~listeners.indexOf(listener))
-    listeners.push(listener);
+  observers.get(target).get(type).set(listener, listenerCount++);
 }
 exports.on = on;
 
 
+// Map of wrapper functions for listeners added using `once`.
 var onceWeakMap = new WeakMap();
 
-
 /**
  * Registers an event `listener` that is called only the next time an event
  * of the specified `type` is emitted on the given event `target`.
  * @param {Object} target
  *    Event target object.
  * @param {String} type
  *    The type of the event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function once(target, type, listener) {
-  let replacement = function observer(...args) {
-    off(target, type, observer);
+  function replacement(...args) {
+    off(target, type, replacement);
     onceWeakMap.delete(listener);
     listener.apply(target, args);
   };
   onceWeakMap.set(listener, replacement);
   on(target, type, replacement);
 }
 exports.once = once;
 
@@ -89,43 +86,46 @@ function emit (target, type, ...args) {
   emitOnObject(target, type, target, ...args);
 }
 exports.emit = emit;
 
 /**
  * A variant of emit that allows setting the this property for event listeners
  */
 function emitOnObject(target, type, thisArg, ...args) {
-  let all = observers(target, '*').length;
-  let state = observers(target, type);
-  let listeners = state.slice();
-  let count = listeners.length;
-  let index = 0;
+  let allListeners = observers.get(target);
+  let listeners = allListeners.get(type);
 
   // If error event and there are no handlers (explicit or catch-all)
   // then print error message to the console.
-  if (count === 0 && type === 'error' && all === 0)
+  if (type === 'error' && !listeners.size && !allListeners.get('*').size)
     console.exception(args[0]);
-  while (index < count) {
+
+  let count = listenerCount;
+  for (let [listener, added] of listeners)
     try {
-      let listener = listeners[index];
-      // Dispatch only if listener is still registered.
-      if (~state.indexOf(listener))
-        listener.apply(thisArg, args);
+      // Since our contract unfortuantely requires that we not dispatch to
+      // this event to listeners that were either added or removed during this
+      // dispatch, we need to check when each listener was added.
+      if (added >= count)
+        break;
+      listener.apply(thisArg, args);
     }
     catch (error) {
       // If exception is not thrown by a error listener and error listener is
       // registered emit `error` event. Otherwise dump exception to the console.
-      if (type !== 'error') emit(target, 'error', error);
-      else console.exception(error);
+      if (type !== 'error')
+        emitOnObject(target, 'error', target, error);
+      else
+        console.exception(error);
     }
-    index++;
-  }
-   // Also emit on `"*"` so that one could listen for all events.
-  if (type !== '*') emit(target, '*', type, ...args);
+
+  // Also emit on `"*"` so that one could listen for all events.
+  if (type !== '*' && allListeners.get('*').size)
+    emitOnObject(target, '*', target, type, ...args);
 }
 exports.emitOnObject = emitOnObject;
 
 /**
  * Removes an event `listener` for the given event `type` on the given event
  * `target`. If no `listener` is passed removes all listeners of the given
  * `type`. If `type` is not passed removes all the listeners of the given
  * event `target`.
@@ -135,41 +135,41 @@ exports.emitOnObject = emitOnObject;
  *    The type of event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function off(target, type, listener) {
   let length = arguments.length;
   if (length === 3) {
     if (onceWeakMap.has(listener)) {
-      listener = onceWeakMap.get(listener);
+      observers.get(target).get(type)
+               .delete(onceWeakMap.get(listener));
       onceWeakMap.delete(listener);
     }
 
-    let listeners = observers(target, type);
-    let index = listeners.indexOf(listener);
-    if (~index)
-      listeners.splice(index, 1);
+    observers.get(target).get(type).delete(listener);
   }
   else if (length === 2) {
-    observers(target, type).splice(0);
+    observers.get(target).get(type).clear();
+    observers.get(target).delete(type);
   }
   else if (length === 1) {
-    let listeners = event(target);
-    Object.keys(listeners).forEach(type => delete listeners[type]);
+    for (let listeners of observers.get(target).values())
+      listeners.clear();
+    observers.delete(target);
   }
 }
 exports.off = off;
 
 /**
  * Returns a number of event listeners registered for the given event `type`
  * on the given event `target`.
  */
 function count(target, type) {
-  return observers(target, type).length;
+  return observers.get(target).get(type).size;
 }
 exports.count = count;
 
 /**
  * Registers listeners on the given event `target` from the given `listeners`
  * dictionary. Iterates over the listeners and if property name matches name
  * pattern `onEventType` and property is a function, then registers it as
  * an `eventType` listener on `target`.
@@ -178,16 +178,17 @@ exports.count = count;
  *    The type of event.
  * @param {Object} listeners
  *    Dictionary of listeners.
  */
 function setListeners(target, listeners) {
   Object.keys(listeners || {}).forEach(key => {
     let match = EVENT_TYPE_PATTERN.exec(key);
     let type = match && match[1].toLowerCase();
-    if (!type) return;
+    if (!type)
+      return;
 
     let listener = listeners[key];
     if (typeof(listener) === 'function')
       on(target, type, listener);
   });
 }
 exports.setListeners = setListeners;
--- a/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
+++ b/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
@@ -96,20 +96,18 @@ const Tabs = Class({
     if (window)
       return window.tabs.open(options);
 
     return openNewWindowWithTab();
   }
 });
 
 const allTabs = new Tabs();
-// Export a new object with allTabs as the prototype, otherwise allTabs becomes
-// frozen and addListItem and removeListItem don't work correctly.
-module.exports = Object.create(allTabs);
-pipe(tabEvents, module.exports);
+module.exports = allTabs;
+pipe(tabEvents, allTabs);
 
 function addWindowTab(window, tabElement) {
   let tab = new Tab(tabElement);
   if (window)
     addListItem(window.tabs, tab);
   addListItem(allTabs, tab);
   emit(allTabs, "open", tab);
 }
--- a/addon-sdk/source/lib/sdk/util/object.js
+++ b/addon-sdk/source/lib/sdk/util/object.js
@@ -8,16 +8,50 @@ module.metadata = {
 };
 
 const { flatten } = require('./array');
 
 // Create a shortcut for Array.prototype.slice.call().
 const unbind = Function.call.bind(Function.bind, Function.call);
 const slice = unbind(Array.prototype.slice);
 
+class DefaultWeakMap extends WeakMap {
+  constructor(createItem, items = undefined) {
+    super(items);
+
+    this.createItem = createItem;
+  }
+
+  get(key) {
+    if (!this.has(key)) {
+      this.set(key, this.createItem(key));
+    }
+
+    return super.get(key);
+  }
+}
+
+class DefaultMap extends Map {
+  constructor(createItem, items = undefined) {
+    super(items);
+
+    this.createItem = createItem;
+  }
+
+  get(key) {
+    if (!this.has(key)) {
+      this.set(key, this.createItem(key));
+    }
+
+    return super.get(key);
+  }
+}
+
+Object.assign(exports, {DefaultMap, DefaultWeakMap});
+
 /**
  * Merges all the properties of all arguments into first argument. If two or
  * more argument objects have own properties with the same name, the property
  * is overridden, with precedence from right to left, implying, that properties
  * of the object on the left are overridden by a same named property of the
  * object on the right.
  *
  * Any argument given with "falsy" value - commonly `null` and `undefined` in
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -488,16 +488,17 @@ const load = iced(function load(loader, 
       metadata: {
         addonID: loader.id,
         URI: module.uri
       }
     });
   }
   sandboxes[module.uri] = sandbox;
 
+  let originalExports = module.exports;
   try {
     evaluate(sandbox, module.uri);
   }
   catch (error) {
     let { message, fileName, lineNumber } = error;
     let stack = error.stack || Error().stack;
     let frames = parseStack(stack).filter(isntLoaderFrame);
     let toString = String(error);
@@ -540,17 +541,20 @@ const load = iced(function load(loader, 
 
   if (loader.checkCompatibility) {
     let err = XulApp.incompatibility(module);
     if (err) {
       throw err;
     }
   }
 
-  if (module.exports && typeof(module.exports) === 'object')
+  // Only freeze the exports object if we created it ourselves. Modules
+  // which completely replace the exports object and still want it
+  // frozen need to freeze it themselves.
+  if (module.exports === originalExports)
     freeze(module.exports);
 
   return module;
 });
 Loader.load = load;
 
 // Utility function to normalize module `uri`s so they have `.js` extension.
 function normalizeExt(uri) {
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -1213,17 +1213,17 @@ exports['test active tab properties defi
       });
     }
   });
 };
 
 // related to bugs 922956 and 989288
 // https://bugzilla.mozilla.org/show_bug.cgi?id=922956
 // https://bugzilla.mozilla.org/show_bug.cgi?id=989288
-exports["test tabs ready and close after window.open"] = function*(assert, done) {
+if (0) exports["test tabs ready and close after window.open"] = function*(assert, done) {
   // ensure popups open in a new window and disable popup blocker
   setPref(OPEN_IN_NEW_WINDOW_PREF, 2);
   setPref(DISABLE_POPUP_PREF, false);
 
   // open windows to trigger observers
   tabs.activeTab.attach({
     contentScript: "window.open('about:blank');" +
                    "window.open('about:blank', '', " +