Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 17 Jan 2014 15:17:49 -0500
changeset 164099 b7717200fb18b205e69cd88e8e21ce55458e1c86
parent 164098 f95bc5df7fcfbd8801a8f043a0d9e25148f3bae9 (current diff)
parent 164075 298f262f21ff15750346ede893b49345cfa1964a (diff)
child 164100 d70ddcedc57d43aee995630e822c624c3b7a6ee0
push id26026
push userphilringnalda@gmail.com
push dateSat, 18 Jan 2014 23:17:27 +0000
treeherdermozilla-central@61fd0f987cf2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound.
addon-sdk/source/lib/sdk/content/symbiont.js
addon-sdk/source/lib/sdk/deprecated/app-strings.js
addon-sdk/source/lib/sdk/deprecated/observer-service.js
addon-sdk/source/test/test-app-strings.js
addon-sdk/source/test/test-observer-service.js
browser/components/sessionstore/src/DocumentUtils.jsm
browser/components/sessionstore/src/TextAndScrollData.jsm
browser/components/sessionstore/test/browser_346337.js
browser/components/sessionstore/test/browser_346337_sample.html
browser/components/sessionstore/test/browser_463205_helper.html
browser/components/sessionstore/test/browser_916390_form_data_loss.js
browser/components/sessionstore/test/browser_916390_sample.html
browser/components/sessionstore/test/browser_input.js
browser/components/sessionstore/test/browser_input_sample.html
dom/network/interfaces/nsIDOMMobileConnection.idl
dom/network/interfaces/nsIMobileConnectionProvider.idl
dom/network/tests/marionette/manifest.ini
dom/network/tests/marionette/test_call_barring_change_password.js
dom/network/tests/marionette/test_call_barring_get_option.js
dom/network/tests/marionette/test_call_barring_set_error.js
dom/network/tests/marionette/test_mobile_connections_array_uninitialized.js
dom/network/tests/marionette/test_mobile_data_connection.js
dom/network/tests/marionette/test_mobile_data_location.js
dom/network/tests/marionette/test_mobile_data_state.js
dom/network/tests/marionette/test_mobile_last_known_network.js
dom/network/tests/marionette/test_mobile_mmi.js
dom/network/tests/marionette/test_mobile_networks.js
dom/network/tests/marionette/test_mobile_operator_names.js
dom/network/tests/marionette/test_mobile_preferred_network_type.js
dom/network/tests/marionette/test_mobile_preferred_network_type_by_setting.js
dom/network/tests/marionette/test_mobile_roaming_preference.js
dom/network/tests/marionette/test_mobile_set_radio.js
dom/network/tests/marionette/test_mobile_voice_state.js
mobile/android/base/fxa/activities/FxAccountSetupActivity.java
mobile/android/base/fxa/sync/FxAccount.java
mobile/android/base/resources/layout/fxaccount_setup.xml
mobile/android/base/resources/values/fxaccount_styles.xml
--- a/addon-sdk/source/lib/sdk/content/content.js
+++ b/addon-sdk/source/lib/sdk/content/content.js
@@ -3,11 +3,11 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 exports.Loader = require('./loader').Loader;
-exports.Symbiont = require('./symbiont').Symbiont;
+exports.Symbiont = require('../deprecated/symbiont').Symbiont;
 exports.Worker = require('./worker').Worker;
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -0,0 +1,404 @@
+/* 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/. */
+'use strict';
+
+module.metadata = {
+  'stability': 'unstable'
+};
+
+const { Class } = require('../core/heritage');
+const { EventTarget } = require('../event/target');
+const { on, off, emit } = require('../event/core');
+const {
+  requiresAddonGlobal,
+  attach, detach, destroy
+} = require('./utils');
+const { delay: async } = require('../lang/functional');
+const { Ci, Cu, Cc } = require('chrome');
+const timer = require('../timers');
+const { URL } = require('../url');
+const { sandbox, evaluate, load } = require('../loader/sandbox');
+const { merge } = require('../util/object');
+const xulApp = require('../system/xul-app');
+const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
+                                              '17.0a2', '*');
+const { getTabForContentWindow } = require('../tabs/utils');
+
+// WeakMap of sandboxes so we can access private values
+const sandboxes = new WeakMap();
+
+/* Trick the linker in order to ensure shipping these files in the XPI.
+  require('./content-worker.js');
+  Then, retrieve URL of these files in the XPI:
+*/
+let prefix = module.uri.split('sandbox.js')[0];
+const CONTENT_WORKER_URL = prefix + 'content-worker.js';
+
+// Fetch additional list of domains to authorize access to for each content
+// script. It is stored in manifest `metadata` field which contains
+// package.json data. This list is originaly defined by authors in
+// `permissions` attribute of their package.json addon file.
+const permissions = require('@loader/options').metadata['permissions'] || {};
+const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
+
+const JS_VERSION = '1.8';
+
+const WorkerSandbox = Class({
+
+  implements: [
+    EventTarget
+  ],
+  
+  /**
+   * Emit a message to the worker content sandbox
+   */
+  emit: function emit(...args) {
+    // Ensure having an asynchronous behavior
+    let self = this;
+    async(function () {
+      emitToContent(self, JSON.stringify(args, replacer));
+    });
+  },
+
+  /**
+   * Synchronous version of `emit`.
+   * /!\ Should only be used when it is strictly mandatory /!\
+   *     Doesn't ensure passing only JSON values.
+   *     Mainly used by context-menu in order to avoid breaking it.
+   */
+  emitSync: function emitSync(...args) {
+    return emitToContent(this, args);
+  },
+
+  /**
+   * Tells if content script has at least one listener registered for one event,
+   * through `self.on('xxx', ...)`.
+   * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
+   */
+  hasListenerFor: function hasListenerFor(name) {
+    return modelFor(this).hasListenerFor(name);
+  },
+
+  /**
+   * Configures sandbox and loads content scripts into it.
+   * @param {Worker} worker
+   *    content worker
+   */
+  initialize: function WorkerSandbox(worker, window) {
+    let model = {};
+    sandboxes.set(this, model);
+    model.worker = worker;
+    // We receive a wrapped window, that may be an xraywrapper if it's content
+    let proto = window;
+
+    // TODO necessary?
+    // Ensure that `emit` has always the right `this`
+    this.emit = this.emit.bind(this);
+    this.emitSync = this.emitSync.bind(this);
+
+    // Eventually use expanded principal sandbox feature, if some are given.
+    //
+    // But prevent it when the Worker isn't used for a content script but for
+    // injecting `addon` object into a Panel, Widget, ... scope.
+    // That's because:
+    // 1/ It is useless to use multiple domains as the worker is only used
+    // to communicate with the addon,
+    // 2/ By using it it would prevent the document to have access to any JS
+    // value of the worker. As JS values coming from multiple domain principals
+    // can't be accessed by 'mono-principals' (principal with only one domain).
+    // Even if this principal is for a domain that is specified in the multiple
+    // domain principal.
+    let principals = window;
+    let wantGlobalProperties = [];
+    if (EXPANDED_PRINCIPALS.length > 0 && !requiresAddonGlobal(worker)) {
+      principals = EXPANDED_PRINCIPALS.concat(window);
+      // We have to replace XHR constructor of the content document
+      // with a custom cross origin one, automagically added by platform code:
+      delete proto.XMLHttpRequest;
+      wantGlobalProperties.push('XMLHttpRequest');
+    }
+
+    // Instantiate trusted code in another Sandbox in order to prevent content
+    // script from messing with standard classes used by proxy and API code.
+    let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
+    apiSandbox.console = console;
+
+    // Create the sandbox and bind it to window in order for content scripts to
+    // have access to all standard globals (window, document, ...)
+    let content = sandbox(principals, {
+      sandboxPrototype: proto,
+      wantXrays: true,
+      wantGlobalProperties: wantGlobalProperties,
+      sameZoneAs: window,
+      metadata: { SDKContentScript: true }
+    });
+    model.sandbox = content;
+    
+    // We have to ensure that window.top and window.parent are the exact same
+    // object than window object, i.e. the sandbox global object. But not
+    // always, in case of iframes, top and parent are another window object.
+    let top = window.top === window ? content : content.top;
+    let parent = window.parent === window ? content : content.parent;
+    merge(content, {
+      // We need 'this === window === top' to be true in toplevel scope:
+      get window() content,
+      get top() top,
+      get parent() parent,
+      // Use the Greasemonkey naming convention to provide access to the
+      // unwrapped window object so the content script can access document
+      // JavaScript values.
+      // NOTE: this functionality is experimental and may change or go away
+      // at any time!
+      get unsafeWindow() window.wrappedJSObject
+    });
+
+    // Load trusted code that will inject content script API.
+    // We need to expose JS objects defined in same principal in order to
+    // avoid having any kind of wrapper.
+    load(apiSandbox, CONTENT_WORKER_URL);
+
+    // prepare a clean `self.options`
+    let options = 'contentScriptOptions' in worker ?
+      JSON.stringify(worker.contentScriptOptions) :
+      undefined;
+
+    // Then call `inject` method and communicate with this script
+    // by trading two methods that allow to send events to the other side:
+    //   - `onEvent` called by content script
+    //   - `result.emitToContent` called by addon script
+    // Bug 758203: We have to explicitely define `__exposedProps__` in order
+    // to allow access to these chrome object attributes from this sandbox with
+    // content priviledges
+    // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
+    let onEvent = onContentEvent.bind(null, this);
+    // `ContentWorker` is defined in CONTENT_WORKER_URL file
+    let chromeAPI = createChromeAPI();
+    let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
+
+    // Merge `emitToContent` and `hasListenerFor` into our private
+    // model of the WorkerSandbox so we can communicate with content
+    // script
+    merge(model, result);
+
+    // Handle messages send by this script:
+    setListeners(this);
+
+    // Inject `addon` global into target document if document is trusted,
+    // `addon` in document is equivalent to `self` in content script.
+    if (requiresAddonGlobal(worker)) {
+      Object.defineProperty(getUnsafeWindow(window), 'addon', {
+          value: content.self
+        }
+      );
+    }
+
+    // Inject our `console` into target document if worker doesn't have a tab
+    // (e.g Panel, PageWorker, Widget).
+    // `worker.tab` can't be used because bug 804935.
+    if (!getTabForContentWindow(window)) {
+      let win = getUnsafeWindow(window);
+
+      // export our chrome console to content window, using the same approach
+      // of `ConsoleAPI`:
+      // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
+      //
+      // and described here:
+      // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
+      let con = Cu.createObjectIn(win);
+
+      let genPropDesc = function genPropDesc(fun) {
+        return { enumerable: true, configurable: true, writable: true,
+          value: console[fun] };
+      }
+
+      const properties = {
+        log: genPropDesc('log'),
+        info: genPropDesc('info'),
+        warn: genPropDesc('warn'),
+        error: genPropDesc('error'),
+        debug: genPropDesc('debug'),
+        trace: genPropDesc('trace'),
+        dir: genPropDesc('dir'),
+        group: genPropDesc('group'),
+        groupCollapsed: genPropDesc('groupCollapsed'),
+        groupEnd: genPropDesc('groupEnd'),
+        time: genPropDesc('time'),
+        timeEnd: genPropDesc('timeEnd'),
+        profile: genPropDesc('profile'),
+        profileEnd: genPropDesc('profileEnd'),
+       __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
+                            value: function() {} }
+      };
+
+      Object.defineProperties(con, properties);
+      Cu.makeObjectPropsNormal(con);
+
+      win.console = con;
+    };
+
+    // The order of `contentScriptFile` and `contentScript` evaluation is
+    // intentional, so programs can load libraries like jQuery from script URLs
+    // and use them in scripts.
+    let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
+          : null,
+        contentScript = ('contentScript' in worker) ? worker.contentScript : null;
+
+    if (contentScriptFile)
+      importScripts.apply(null, [this].concat(contentScriptFile));
+    if (contentScript) {
+      evaluateIn(
+        this,
+        Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
+      );
+    }
+  },
+  destroy: function destroy() {
+    this.emitSync('detach');
+    let model = modelFor(this);
+    model.sandbox = null
+    model.worker = null;
+  },
+
+});
+
+exports.WorkerSandbox = WorkerSandbox;
+
+/**
+ * Imports scripts to the sandbox by reading files under urls and
+ * evaluating its source. If exception occurs during evaluation
+ * `'error'` event is emitted on the worker.
+ * This is actually an analog to the `importScript` method in web
+ * workers but in our case it's not exposed even though content
+ * scripts may be able to do it synchronously since IO operation
+ * takes place in the UI process.
+ */
+function importScripts (workerSandbox, ...urls) {
+  let { worker, sandbox } = modelFor(workerSandbox);
+  for (let i in urls) {
+    let contentScriptFile = urls[i];
+    try {
+      let uri = URL(contentScriptFile);
+      if (uri.scheme === 'resource')
+        load(sandbox, String(uri));
+      else
+        throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
+    }
+    catch(e) {
+      emit(worker, 'error', e);
+    }
+  }
+}
+
+function setListeners (workerSandbox) {
+  let { worker } = modelFor(workerSandbox);
+  // console.xxx calls
+  workerSandbox.on('console', function consoleListener (kind, ...args) {
+    console[kind].apply(console, args);
+  });
+
+  // self.postMessage calls
+  workerSandbox.on('message', function postMessage(data) {
+    // destroyed?
+    if (worker)
+      emit(worker, 'message', data);
+  });
+
+  // self.port.emit calls
+  workerSandbox.on('event', function portEmit (...eventArgs) {
+    // If not destroyed, emit event information to worker
+    // `eventArgs` has the event name as first element,
+    // and remaining elements are additional arguments to pass
+    if (worker)
+      emit.apply(null, [worker.port].concat(eventArgs));
+  });
+
+  // unwrap, recreate and propagate async Errors thrown from content-script
+  workerSandbox.on('error', function onError({instanceOfError, value}) {
+    if (worker) {
+      let error = value;
+      if (instanceOfError) {
+        error = new Error(value.message, value.fileName, value.lineNumber);
+        error.stack = value.stack;
+        error.name = value.name;
+      }
+      emit(worker, 'error', error);
+    }
+  });
+}
+
+/**
+ * Evaluates code in the sandbox.
+ * @param {String} code
+ *    JavaScript source to evaluate.
+ * @param {String} [filename='javascript:' + code]
+ *    Name of the file
+ */
+function evaluateIn (workerSandbox, code, filename) {
+  let { worker, sandbox } = modelFor(workerSandbox);
+  try {
+    evaluate(sandbox, code, filename || 'javascript:' + code);
+  }
+  catch(e) {
+    emit(worker, 'error', e);
+  }
+}
+
+/**
+ * Method called by the worker sandbox when it needs to send a message
+ */
+function onContentEvent (workerSandbox, args) {
+  // As `emit`, we ensure having an asynchronous behavior
+  async(function () {
+    // We emit event to chrome/addon listeners
+    emit.apply(null, [workerSandbox].concat(JSON.parse(args)));
+  });
+}
+
+
+function modelFor (workerSandbox) {
+  return sandboxes.get(workerSandbox);
+}
+
+/**
+ * JSON.stringify is buggy with cross-sandbox values,
+ * it may return '{}' on functions. Use a replacer to match them correctly.
+ */
+function replacer (k, v) {
+  return typeof v === 'function' ? undefined : v;
+}
+
+function getUnsafeWindow (win) {
+  return win.wrappedJSObject || win;
+}
+
+function emitToContent (workerSandbox, args) {
+  return modelFor(workerSandbox).emitToContent(args);
+}
+
+function createChromeAPI () {
+  return {
+    timers: {
+      setTimeout: timer.setTimeout,
+      setInterval: timer.setInterval,
+      clearTimeout: timer.clearTimeout,
+      clearInterval: timer.clearInterval,
+      __exposedProps__: {
+        setTimeout: 'r',
+        setInterval: 'r',
+        clearTimeout: 'r',
+        clearInterval: 'r'
+      },
+    },
+    sandbox: {
+      evaluate: evaluate,
+      __exposedProps__: {
+        evaluate: 'r'
+      }
+    },
+    __exposedProps__: {
+      timers: 'r',
+      sandbox: 'r'
+    }
+  };
+}
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/content/symbiont.js
+++ /dev/null
@@ -1,229 +0,0 @@
-/* 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/. */
-"use strict";
-
-module.metadata = {
-  "stability": "unstable"
-};
-
-const { Worker } = require('./worker');
-const { Loader } = require('./loader');
-const hiddenFrames = require('../frame/hidden-frame');
-const observers = require('../deprecated/observer-service');
-const unload = require('../system/unload');
-const { getDocShell } = require("../frame/utils");
-const { ignoreWindow } = require('../private-browsing/utils');
-
-// Everything coming from add-on's xpi considered an asset.
-const assetsURI = require('../self').data.url().replace(/data\/$/, "");
-
-/**
- * This trait is layered on top of `Worker` and in contrast to symbiont
- * Worker constructor requires `content` option that represents content
- * that will be loaded in the provided frame, if frame is not provided
- * Worker will create hidden one.
- */
-const Symbiont = Worker.resolve({
-    constructor: '_initWorker',
-    destroy: '_workerDestroy'
-  }).compose(Loader, {
-  
-  /**
-   * The constructor requires all the options that are required by
-   * `require('content').Worker` with the difference that the `frame` option
-   * is optional. If `frame` is not provided, `contentURL` is expected.
-   * @param {Object} options
-   * @param {String} options.contentURL
-   *    URL of a content to load into `this._frame` and create worker for.
-   * @param {Element} [options.frame]
-   *    iframe element that is used to load `options.contentURL` into.
-   *    if frame is not provided hidden iframe will be created.
-   */
-  constructor: function Symbiont(options) {
-    options = options || {};
-
-    if ('contentURL' in options)
-        this.contentURL = options.contentURL;
-    if ('contentScriptWhen' in options)
-      this.contentScriptWhen = options.contentScriptWhen;
-    if ('contentScriptOptions' in options)
-      this.contentScriptOptions = options.contentScriptOptions;
-    if ('contentScriptFile' in options)
-      this.contentScriptFile = options.contentScriptFile;
-    if ('contentScript' in options)
-      this.contentScript = options.contentScript;
-    if ('allow' in options)
-      this.allow = options.allow;
-    if ('onError' in options)
-        this.on('error', options.onError);
-    if ('onMessage' in options)
-        this.on('message', options.onMessage);
-    if ('frame' in options) {
-      this._initFrame(options.frame);
-    }
-    else {
-      let self = this;
-      this._hiddenFrame = hiddenFrames.HiddenFrame({
-        onReady: function onFrame() {
-          self._initFrame(this.element);
-        },
-        onUnload: function onUnload() {
-          // Bug 751211: Remove reference to _frame when hidden frame is
-          // automatically removed on unload, otherwise we are going to face
-          // "dead object" exception
-          self.destroy();
-        }
-      });
-      hiddenFrames.add(this._hiddenFrame);
-    }
-
-    unload.ensure(this._public, "destroy");
-  },
-  
-  destroy: function destroy() {
-    this._workerDestroy();
-    this._unregisterListener();
-    this._frame = null;
-    if (this._hiddenFrame) {
-      hiddenFrames.remove(this._hiddenFrame);
-      this._hiddenFrame = null;
-    }
-  },
-  
-  /**
-   * XUL iframe or browser elements with attribute `type` being `content`.
-   * Used to create `ContentSymbiont` from.
-   * @type {nsIFrame|nsIBrowser}
-   */
-  _frame: null,
-  
-  /**
-   * Listener to the `'frameReady"` event (emitted when `iframe` is ready).
-   * Removes listener, sets right permissions to the frame and loads content.
-   */
-  _initFrame: function _initFrame(frame) {
-    if (this._loadListener)
-      this._unregisterListener();
-    
-    this._frame = frame;
-
-    if (getDocShell(frame)) {
-      this._reallyInitFrame(frame);
-    }
-    else {
-      if (this._waitForFrame) {
-        observers.remove('content-document-global-created', this._waitForFrame);
-      }
-      this._waitForFrame = this.__waitForFrame.bind(this, frame);
-      observers.add('content-document-global-created', this._waitForFrame);
-    }
-  },
-
-  __waitForFrame: function _waitForFrame(frame, win, topic) {
-    if (frame.contentWindow == win) {
-      observers.remove('content-document-global-created', this._waitForFrame);
-      delete this._waitForFrame;
-      this._reallyInitFrame(frame);
-    }
-  },
-
-  _reallyInitFrame: function _reallyInitFrame(frame) {
-    getDocShell(frame).allowJavascript = this.allow.script;
-    frame.setAttribute("src", this._contentURL);
-
-    // Inject `addon` object in document if we load a document from
-    // one of our addon folder and if no content script are defined. bug 612726
-    let isDataResource =
-      typeof this._contentURL == "string" &&
-      this._contentURL.indexOf(assetsURI) == 0;
-    let hasContentScript =
-      (Array.isArray(this.contentScript) ? this.contentScript.length > 0
-                                             : !!this.contentScript) ||
-      (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0
-                                             : !!this.contentScriptFile);
-    // If we have to inject `addon` we have to do it before document
-    // script execution, so during `start`:
-    this._injectInDocument = isDataResource && !hasContentScript;
-    if (this._injectInDocument)
-      this.contentScriptWhen = "start";
-
-    if ((frame.contentDocument.readyState == "complete" ||
-        (frame.contentDocument.readyState == "interactive" &&
-         this.contentScriptWhen != 'end' )) &&
-        frame.contentDocument.location == this._contentURL) {
-      // In some cases src doesn't change and document is already ready
-      // (for ex: when the user moves a widget while customizing toolbars.)
-      this._onInit();
-      return;
-    }
-    
-    let self = this;
-    
-    if ('start' == this.contentScriptWhen) {
-      this._loadEvent = 'start';
-      observers.add('document-element-inserted', 
-        this._loadListener = function onStart(doc) {
-          let window = doc.defaultView;
-
-          if (ignoreWindow(window)) {
-            return;
-          }
-
-          if (window && window == frame.contentWindow) {
-            self._unregisterListener();
-            self._onInit();
-          }
-          
-        });
-      return;
-    }
-    
-    let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
-    let self = this;
-    this._loadEvent = eventName;
-    frame.addEventListener(eventName, 
-      this._loadListener = function _onReady(event) {
-      
-        if (event.target != frame.contentDocument)
-          return;
-        self._unregisterListener();
-        
-        self._onInit();
-        
-      }, true);
-    
-  },
-  
-  /**
-   * Unregister listener that watchs for document being ready to be injected.
-   * This listener is registered in `Symbiont._initFrame`.
-   */
-  _unregisterListener: function _unregisterListener() {
-    if (this._waitForFrame) {
-      observers.remove('content-document-global-created', this._waitForFrame);
-      delete this._waitForFrame;
-    }
-
-    if (!this._loadListener)
-      return;
-    if (this._loadEvent == "start") {
-      observers.remove('document-element-inserted', this._loadListener);
-    }
-    else {
-      this._frame.removeEventListener(this._loadEvent, this._loadListener,
-                                      true);
-    }
-    this._loadListener = null;
-  },
-  
-  /**
-   * Called by Symbiont itself when the frame is ready to load  
-   * content scripts according to contentScriptWhen. Overloaded by Panel. 
-   */
-  _onInit: function () {
-    this._initWorker({ window: this._frame.contentWindow });
-  }
-  
-});
-exports.Symbiont = Symbiont;
--- a/addon-sdk/source/lib/sdk/content/utils.js
+++ b/addon-sdk/source/lib/sdk/content/utils.js
@@ -1,41 +1,82 @@
 /* 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/. */
-"use strict";
+'use strict';
 
 module.metadata = {
-  "stability": "unstable"
+  'stability': 'unstable'
 };
 
-let assetsURI = require("../self").data.url();
+let { merge } = require('../util/object');
+let assetsURI = require('../self').data.url();
 let isArray = Array.isArray;
+let method = require('method/core');
 
 function isAddonContent({ contentURL }) {
-  return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
+  return typeof(contentURL) === 'string' && contentURL.indexOf(assetsURI) === 0;
 }
 exports.isAddonContent = isAddonContent;
 
 function hasContentScript({ contentScript, contentScriptFile }) {
   return (isArray(contentScript) ? contentScript.length > 0 :
          !!contentScript) ||
          (isArray(contentScriptFile) ? contentScriptFile.length > 0 :
          !!contentScriptFile);
 }
 exports.hasContentScript = hasContentScript;
 
 function requiresAddonGlobal(model) {
-  return isAddonContent(model) && !hasContentScript(model);
+  return model.injectInDocument || (isAddonContent(model) && !hasContentScript(model));
 }
 exports.requiresAddonGlobal = requiresAddonGlobal;
 
 function getAttachEventType(model) {
   if (!model) return null;
   let when = model.contentScriptWhen;
-  return requiresAddonGlobal(model) ? "document-element-inserted" :
-         when === "start" ? "document-element-inserted" :
-         when === "end" ? "load" :
-         when === "ready" ? "DOMContentLoaded" :
+  return requiresAddonGlobal(model) ? 'document-element-inserted' :
+         when === 'start' ? 'document-element-inserted' :
+         when === 'end' ? 'load' :
+         when === 'ready' ? 'DOMContentLoaded' :
          null;
 }
 exports.getAttachEventType = getAttachEventType;
 
+let attach = method('worker-attach');
+exports.attach = attach;
+
+let detach = method('worker-detach');
+exports.detach = detach;
+
+let destroy = method('worker-destroy');
+exports.destroy = destroy;
+
+function WorkerHost (workerFor) {
+  // Define worker properties that just proxy to underlying worker
+  return ['postMessage', 'port', 'url', 'tab'].reduce(function(proto, name) {
+    // Use descriptor properties instead so we can call
+    // the worker function in the context of the worker so we
+    // don't have to create new functions with `fn.bind(worker)`
+    let descriptorProp = {
+      value: function (...args) {
+        let worker = workerFor(this);
+        return worker[name].apply(worker, args);
+      }
+    };
+    
+    let accessorProp = {
+      get: function () { return workerFor(this)[name]; },
+      set: function (value) { workerFor(this)[name] = value; }
+    };
+
+    Object.defineProperty(proto, name, merge({
+      enumerable: true,
+      configurable: false,
+    }, isDescriptor(name) ? descriptorProp : accessorProp));
+    return proto;
+  }, {});
+  
+  function isDescriptor (prop) {
+    return ~['postMessage'].indexOf(prop);
+  }
+}
+exports.WorkerHost = WorkerHost;
--- a/addon-sdk/source/lib/sdk/content/worker.js
+++ b/addon-sdk/source/lib/sdk/content/worker.js
@@ -2,650 +2,281 @@
  * 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/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-const { Trait } = require('../deprecated/traits');
-const { EventEmitter, EventEmitterTrait } = require('../deprecated/events');
+const { Class } = require('../core/heritage');
+const { EventTarget } = require('../event/target');
+const { on, off, emit, setListeners } = require('../event/core');
+const {
+  attach, detach, destroy
+} = require('./utils');
+const { method } = require('../lang/functional');
 const { Ci, Cu, Cc } = require('chrome');
-const timer = require('../timers');
-const { URL } = require('../url');
 const unload = require('../system/unload');
-const observers = require('../deprecated/observer-service');
-const { Cortex } = require('../deprecated/cortex');
-const { sandbox, evaluate, load } = require("../loader/sandbox");
-const { merge } = require('../util/object');
-const xulApp = require("../system/xul-app");
-const { getInnerId } = require("../window/utils")
-const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
-                                              "17.0a2", "*");
+const events = require('../system/events');
+const { getInnerId } = require("../window/utils");
+const { WorkerSandbox } = require('./sandbox');
 const { getTabForWindow } = require('../tabs/helpers');
-const { getTabForContentWindow } = require('../tabs/utils');
-
-/* Trick the linker in order to ensure shipping these files in the XPI.
-  require('./content-worker.js');
-  Then, retrieve URL of these files in the XPI:
-*/
-let prefix = module.uri.split('worker.js')[0];
-const CONTENT_WORKER_URL = prefix + 'content-worker.js';
 
-// Fetch additional list of domains to authorize access to for each content
-// script. It is stored in manifest `metadata` field which contains
-// package.json data. This list is originaly defined by authors in
-// `permissions` attribute of their package.json addon file.
-const permissions = require('@loader/options').metadata['permissions'] || {};
-const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
+// A weak map of workers to hold private attributes that
+// should not be exposed
+const workers = new WeakMap();
 
-const JS_VERSION = '1.8';
+let modelFor = (worker) => workers.get(worker);
 
 const ERR_DESTROYED =
   "Couldn't find the worker to receive this message. " +
   "The script may not be initialized yet, or may already have been unloaded.";
 
 const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
                    "until it is visible again.";
 
 
-const WorkerSandbox = EventEmitter.compose({
-
-  /**
-   * Emit a message to the worker content sandbox
-   */
-  emit: function emit() {
-    // First ensure having a regular array
-    // (otherwise, `arguments` would be mapped to an object by `stringify`)
-    let array = Array.slice(arguments);
-    // JSON.stringify is buggy with cross-sandbox values,
-    // it may return "{}" on functions. Use a replacer to match them correctly.
-    function replacer(k, v) {
-      return typeof v === "function" ? undefined : v;
-    }
-    // Ensure having an asynchronous behavior
-    let self = this;
-    timer.setTimeout(function () {
-      self._emitToContent(JSON.stringify(array, replacer));
-    }, 0);
-  },
-
-  /**
-   * Synchronous version of `emit`.
-   * /!\ Should only be used when it is strictly mandatory /!\
-   *     Doesn't ensure passing only JSON values.
-   *     Mainly used by context-menu in order to avoid breaking it.
-   */
-  emitSync: function emitSync() {
-    let args = Array.slice(arguments);
-    return this._emitToContent(args);
-  },
-
-  /**
-   * Tells if content script has at least one listener registered for one event,
-   * through `self.on('xxx', ...)`.
-   * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
-   */
-  hasListenerFor: function hasListenerFor(name) {
-    return this._hasListenerFor(name);
-  },
-
-  /**
-   * Method called by the worker sandbox when it needs to send a message
-   */
-  _onContentEvent: function onContentEvent(args) {
-    // As `emit`, we ensure having an asynchronous behavior
-    let self = this;
-    timer.setTimeout(function () {
-      // We emit event to chrome/addon listeners
-      self._emit.apply(self, JSON.parse(args));
-    }, 0);
-  },
-
-  /**
-   * Configures sandbox and loads content scripts into it.
-   * @param {Worker} worker
-   *    content worker
-   */
-  constructor: function WorkerSandbox(worker) {
-    this._addonWorker = worker;
-
-    // Ensure that `emit` has always the right `this`
-    this.emit = this.emit.bind(this);
-    this.emitSync = this.emitSync.bind(this);
-
-    // We receive a wrapped window, that may be an xraywrapper if it's content
-    let window = worker._window;
-    let proto = window;
-
-    // Eventually use expanded principal sandbox feature, if some are given.
-    //
-    // But prevent it when the Worker isn't used for a content script but for
-    // injecting `addon` object into a Panel, Widget, ... scope.
-    // That's because:
-    // 1/ It is useless to use multiple domains as the worker is only used
-    // to communicate with the addon,
-    // 2/ By using it it would prevent the document to have access to any JS
-    // value of the worker. As JS values coming from multiple domain principals
-    // can't be accessed by "mono-principals" (principal with only one domain).
-    // Even if this principal is for a domain that is specified in the multiple
-    // domain principal.
-    let principals  = window;
-    let wantGlobalProperties = []
-    if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
-      principals = EXPANDED_PRINCIPALS.concat(window);
-      // We have to replace XHR constructor of the content document
-      // with a custom cross origin one, automagically added by platform code:
-      delete proto.XMLHttpRequest;
-      wantGlobalProperties.push("XMLHttpRequest");
-    }
-
-    // Instantiate trusted code in another Sandbox in order to prevent content
-    // script from messing with standard classes used by proxy and API code.
-    let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
-    apiSandbox.console = console;
-
-    // Create the sandbox and bind it to window in order for content scripts to
-    // have access to all standard globals (window, document, ...)
-    let content = this._sandbox = sandbox(principals, {
-      sandboxPrototype: proto,
-      wantXrays: true,
-      wantGlobalProperties: wantGlobalProperties,
-      sameZoneAs: window,
-      metadata: { SDKContentScript: true }
-    });
-    // We have to ensure that window.top and window.parent are the exact same
-    // object than window object, i.e. the sandbox global object. But not
-    // always, in case of iframes, top and parent are another window object.
-    let top = window.top === window ? content : content.top;
-    let parent = window.parent === window ? content : content.parent;
-    merge(content, {
-      // We need "this === window === top" to be true in toplevel scope:
-      get window() content,
-      get top() top,
-      get parent() parent,
-      // Use the Greasemonkey naming convention to provide access to the
-      // unwrapped window object so the content script can access document
-      // JavaScript values.
-      // NOTE: this functionality is experimental and may change or go away
-      // at any time!
-      get unsafeWindow() window.wrappedJSObject
-    });
-
-    // Load trusted code that will inject content script API.
-    // We need to expose JS objects defined in same principal in order to
-    // avoid having any kind of wrapper.
-    load(apiSandbox, CONTENT_WORKER_URL);
-
-    // prepare a clean `self.options`
-    let options = 'contentScriptOptions' in worker ?
-      JSON.stringify( worker.contentScriptOptions ) :
-      undefined;
-
-    // Then call `inject` method and communicate with this script
-    // by trading two methods that allow to send events to the other side:
-    //   - `onEvent` called by content script
-    //   - `result.emitToContent` called by addon script
-    // Bug 758203: We have to explicitely define `__exposedProps__` in order
-    // to allow access to these chrome object attributes from this sandbox with
-    // content priviledges
-    // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
-    let chromeAPI = {
-      timers: {
-        setTimeout: timer.setTimeout,
-        setInterval: timer.setInterval,
-        clearTimeout: timer.clearTimeout,
-        clearInterval: timer.clearInterval,
-        __exposedProps__: {
-          setTimeout: 'r',
-          setInterval: 'r',
-          clearTimeout: 'r',
-          clearInterval: 'r'
-        }
-      },
-      sandbox: {
-        evaluate: evaluate,
-        __exposedProps__: {
-          evaluate: 'r',
-        }
-      },
-      __exposedProps__: {
-        timers: 'r',
-        sandbox: 'r',
-      }
-    };
-    let onEvent = this._onContentEvent.bind(this);
-    // `ContentWorker` is defined in CONTENT_WORKER_URL file
-    let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
-    this._emitToContent = result.emitToContent;
-    this._hasListenerFor = result.hasListenerFor;
-
-    // Handle messages send by this script:
-    let self = this;
-    // console.xxx calls
-    this.on("console", function consoleListener(kind) {
-      console[kind].apply(console, Array.slice(arguments, 1));
-    });
-
-    // self.postMessage calls
-    this.on("message", function postMessage(data) {
-      // destroyed?
-      if (self._addonWorker)
-        self._addonWorker._emit('message', data);
-    });
-
-    // self.port.emit calls
-    this.on("event", function portEmit(name, args) {
-      // destroyed?
-      if (self._addonWorker)
-        self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
-    });
-
-    // unwrap, recreate and propagate async Errors thrown from content-script
-    this.on("error", function onError({instanceOfError, value}) {
-      if (self._addonWorker) {
-        let error = value;
-        if (instanceOfError) {
-          error = new Error(value.message, value.fileName, value.lineNumber);
-          error.stack = value.stack;
-          error.name = value.name;
-        }
-        self._addonWorker._emit('error', error);
-      }
-    });
-
-    // Inject `addon` global into target document if document is trusted,
-    // `addon` in document is equivalent to `self` in content script.
-    if (worker._injectInDocument) {
-      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
-      Object.defineProperty(win, "addon", {
-          value: content.self
-        }
-      );
-    }
-
-    // Inject our `console` into target document if worker doesn't have a tab
-    // (e.g Panel, PageWorker, Widget).
-    // `worker.tab` can't be used because bug 804935.
-    if (!getTabForContentWindow(window)) {
-      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
-
-      // export our chrome console to content window, using the same approach
-      // of `ConsoleAPI`:
-      // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
-      //
-      // and described here:
-      // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
-      let con = Cu.createObjectIn(win);
-
-      let genPropDesc = function genPropDesc(fun) {
-        return { enumerable: true, configurable: true, writable: true,
-          value: console[fun] };
-      }
-
-      const properties = {
-        log: genPropDesc('log'),
-        info: genPropDesc('info'),
-        warn: genPropDesc('warn'),
-        error: genPropDesc('error'),
-        debug: genPropDesc('debug'),
-        trace: genPropDesc('trace'),
-        dir: genPropDesc('dir'),
-        group: genPropDesc('group'),
-        groupCollapsed: genPropDesc('groupCollapsed'),
-        groupEnd: genPropDesc('groupEnd'),
-        time: genPropDesc('time'),
-        timeEnd: genPropDesc('timeEnd'),
-        profile: genPropDesc('profile'),
-        profileEnd: genPropDesc('profileEnd'),
-       __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
-                            value: function() {} }
-      };
-
-      Object.defineProperties(con, properties);
-      Cu.makeObjectPropsNormal(con);
-
-      win.console = con;
-    };
-
-    // The order of `contentScriptFile` and `contentScript` evaluation is
-    // intentional, so programs can load libraries like jQuery from script URLs
-    // and use them in scripts.
-    let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
-          : null,
-        contentScript = ('contentScript' in worker) ? worker.contentScript : null;
-
-    if (contentScriptFile) {
-      if (Array.isArray(contentScriptFile))
-        this._importScripts.apply(this, contentScriptFile);
-      else
-        this._importScripts(contentScriptFile);
-    }
-    if (contentScript) {
-      this._evaluate(
-        Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
-      );
-    }
-  },
-  destroy: function destroy() {
-    this.emitSync("detach");
-    this._sandbox = null;
-    this._addonWorker = null;
-  },
-
-  /**
-   * JavaScript sandbox where all the content scripts are evaluated.
-   * {Sandbox}
-   */
-  _sandbox: null,
-
-  /**
-   * Reference to the addon side of the worker.
-   * @type {Worker}
-   */
-  _addonWorker: null,
-
-  /**
-   * Evaluates code in the sandbox.
-   * @param {String} code
-   *    JavaScript source to evaluate.
-   * @param {String} [filename='javascript:' + code]
-   *    Name of the file
-   */
-  _evaluate: function(code, filename) {
-    try {
-      evaluate(this._sandbox, code, filename || 'javascript:' + code);
-    }
-    catch(e) {
-      this._addonWorker._emit('error', e);
-    }
-  },
-  /**
-   * Imports scripts to the sandbox by reading files under urls and
-   * evaluating its source. If exception occurs during evaluation
-   * `"error"` event is emitted on the worker.
-   * This is actually an analog to the `importScript` method in web
-   * workers but in our case it's not exposed even though content
-   * scripts may be able to do it synchronously since IO operation
-   * takes place in the UI process.
-   */
-  _importScripts: function _importScripts(url) {
-    let urls = Array.slice(arguments, 0);
-    for each (let contentScriptFile in urls) {
-      try {
-        let uri = URL(contentScriptFile);
-        if (uri.scheme === 'resource')
-          load(this._sandbox, String(uri));
-        else
-          throw Error("Unsupported `contentScriptFile` url: " + String(uri));
-      }
-      catch(e) {
-        this._addonWorker._emit('error', e);
-      }
-    }
-  }
-});
-
 /**
  * Message-passing facility for communication between code running
  * in the content and add-on process.
  * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
  */
-const Worker = EventEmitter.compose({
-  on: Trait.required,
-  _removeAllListeners: Trait.required,
+const Worker = Class({
+  implements: [EventTarget],
+  initialize: function WorkerConstructor (options) {
+    // Save model in weak map to not expose properties
+    let model = createModel();
+    workers.set(this, model);
+
+    options = options || {};
 
-  // List of messages fired before worker is initialized
-  get _earlyEvents() {
-    delete this._earlyEvents;
-    this._earlyEvents = [];
-    return this._earlyEvents;
+    if ('contentScriptFile' in options)
+      this.contentScriptFile = options.contentScriptFile;
+    if ('contentScriptOptions' in options)
+      this.contentScriptOptions = options.contentScriptOptions;
+    if ('contentScript' in options)
+      this.contentScript = options.contentScript;
+    if ('injectInDocument' in options)
+      this.injectInDocument = !!options.injectInDocument;
+
+    setListeners(this, options);
+
+    unload.ensure(this, "destroy");
+
+    // Ensure that worker.port is initialized for contentWorker to be able
+    // to send events during worker initialization.
+    this.port = createPort(this);
+
+    model.documentUnload = documentUnload.bind(this);
+    model.pageShow = pageShow.bind(this);
+    model.pageHide = pageHide.bind(this);
+
+    if ('window' in options)
+      attach(this, options.window);
   },
 
   /**
    * Sends a message to the worker's global scope. Method takes single
    * argument, which represents data to be sent to the worker. The data may
    * be any primitive type value or `JSON`. Call of this method asynchronously
    * emits `message` event with data value in the global scope of this
    * symbiont.
    *
    * `message` event listeners can be set either by calling
    * `self.on` with a first argument string `"message"` or by
    * implementing `onMessage` function in the global scope of this worker.
    * @param {Number|String|JSON} data
    */
-  postMessage: function (data) {
-    let args = ['message'].concat(Array.slice(arguments));
-    if (!this._inited) {
-      this._earlyEvents.push(args);
+  postMessage: function (...data) {
+    let model = modelFor(this);
+    let args = ['message'].concat(data);
+    if (!model.inited) {
+      model.earlyEvents.push(args);
       return;
     }
-    processMessage.apply(this, args);
-  },
-
-  /**
-   * EventEmitter, that behaves (calls listeners) asynchronously.
-   * A way to send customized messages to / from the worker.
-   * Events from in the worker can be observed / emitted via
-   * worker.on / worker.emit.
-   */
-  get port() {
-    // We generate dynamically this attribute as it needs to be accessible
-    // before Worker.constructor gets called. (For ex: Panel)
-
-    // create an event emitter that receive and send events from/to the worker
-    this._port = EventEmitterTrait.create({
-      emit: this._emitEventToContent.bind(this)
-    });
-
-    // expose wrapped port, that exposes only public properties:
-    // We need to destroy this getter in order to be able to set the
-    // final value. We need to update only public port attribute as we never
-    // try to access port attribute from private API.
-    delete this._public.port;
-    this._public.port = Cortex(this._port);
-    // Replicate public port to the private object
-    delete this.port;
-    this.port = this._public.port;
-
-    return this._port;
-  },
-
-  /**
-   * Same object than this.port but private API.
-   * Allow access to _emit, in order to send event to port.
-   */
-  _port: null,
-
-  /**
-   * Emit a custom event to the content script,
-   * i.e. emit this event on `self.port`
-   */
-  _emitEventToContent: function () {
-    let args = ['event'].concat(Array.slice(arguments));
-    if (!this._inited) {
-      this._earlyEvents.push(args);
-      return;
-    }
-    processMessage.apply(this, args);
+    processMessage.apply(null, [this].concat(args));
   },
 
-  // Is worker connected to the content worker sandbox ?
-  _inited: false,
-
-  // Is worker being frozen? i.e related document is frozen in bfcache.
-  // Content script should not be reachable if frozen.
-  _frozen: true,
-
-  constructor: function Worker(options) {
-    options = options || {};
-
-    if ('contentScriptFile' in options)
-      this.contentScriptFile = options.contentScriptFile;
-    if ('contentScriptOptions' in options)
-      this.contentScriptOptions = options.contentScriptOptions;
-    if ('contentScript' in options)
-      this.contentScript = options.contentScript;
-
-    this._setListeners(options);
-
-    unload.ensure(this._public, "destroy");
-
-    // Ensure that worker._port is initialized for contentWorker to be able
-    // to send events during worker initialization.
-    this.port;
-
-    this._documentUnload = this._documentUnload.bind(this);
-    this._pageShow = this._pageShow.bind(this);
-    this._pageHide = this._pageHide.bind(this);
-
-    if ("window" in options) this._attach(options.window);
-  },
-
-  _setListeners: function(options) {
-    if ('onError' in options)
-      this.on('error', options.onError);
-    if ('onMessage' in options)
-      this.on('message', options.onMessage);
-    if ('onDetach' in options)
-      this.on('detach', options.onDetach);
+  get url () {
+    let model = modelFor(this);
+    // model.window will be null after detach
+    return model.window ? model.window.document.location.href : null;
   },
 
-  _attach: function(window) {
-    this._window = window;
-    // Track document unload to destroy this worker.
-    // We can't watch for unload event on page's window object as it
-    // prevents bfcache from working:
-    // https://developer.mozilla.org/En/Working_with_BFCache
-    this._windowID = getInnerId(this._window);
-    observers.add("inner-window-destroyed", this._documentUnload);
-
-    // Listen to pagehide event in order to freeze the content script
-    // while the document is frozen in bfcache:
-    this._window.addEventListener("pageshow", this._pageShow, true);
-    this._window.addEventListener("pagehide", this._pageHide, true);
-
-    // will set this._contentWorker pointing to the private API:
-    this._contentWorker = WorkerSandbox(this);
-
-    // Mainly enable worker.port.emit to send event to the content worker
-    this._inited = true;
-    this._frozen = false;
-
-    // Process all events and messages that were fired before the
-    // worker was initialized.
-    this._earlyEvents.forEach((function (args) {
-      processMessage.apply(this, args);
-    }).bind(this));
+  get contentURL () {
+    let model = modelFor(this);
+    return model.window ? model.window.document.URL : null;
   },
 
-  _documentUnload: function _documentUnload(subject, topic, data) {
-    let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-    if (innerWinID != this._windowID) return false;
-    this._workerCleanup();
-    return true;
-  },
-
-  _pageShow: function _pageShow() {
-    this._contentWorker.emitSync("pageshow");
-    this._emit("pageshow");
-    this._frozen = false;
-  },
-
-  _pageHide: function _pageHide() {
-    this._contentWorker.emitSync("pagehide");
-    this._emit("pagehide");
-    this._frozen = true;
-  },
-
-  get url() {
-    // this._window will be null after detach
-    return this._window ? this._window.document.location.href : null;
-  },
-
-  get tab() {
-    // this._window will be null after detach
-    if (this._window)
-      return getTabForWindow(this._window);
+  get tab () {
+    let model = modelFor(this);
+    // model.window will be null after detach
+    if (model.window)
+      return getTabForWindow(model.window);
     return null;
   },
 
-  /**
-   * Tells content worker to unload itself and
-   * removes all the references from itself.
-   */
-  destroy: function destroy() {
-    this._workerCleanup();
-    this._inited = true;
-    this._removeAllListeners();
+  // Implemented to provide some of the previous features of exposing sandbox
+  // so that Worker can be extended
+  getSandbox: function () {
+    return modelFor(this).contentWorker;
   },
 
-  /**
-   * Remove all internal references to the attached document
-   * Tells _port to unload itself and removes all the references from itself.
-   */
-  _workerCleanup: function _workerCleanup() {
-    // maybe unloaded before content side is created
-    // As Symbiont call worker.constructor on document load
-    if (this._contentWorker)
-      this._contentWorker.destroy();
-    this._contentWorker = null;
-    if (this._window) {
-      this._window.removeEventListener("pageshow", this._pageShow, true);
-      this._window.removeEventListener("pagehide", this._pageHide, true);
-    }
-    this._window = null;
-    // This method may be called multiple times,
-    // avoid dispatching `detach` event more than once
-    if (this._windowID) {
-      this._windowID = null;
-      observers.remove("inner-window-destroyed", this._documentUnload);
-      this._earlyEvents.length = 0;
-      this._emit("detach");
-    }
-    this._inited = false;
-  },
+  toString: function () { return '[object Worker]'; },
+  attach: method(attach),
+  detach: method(detach),
+  destroy: method(destroy)
+});
+exports.Worker = Worker;
+
+attach.define(Worker, function (worker, window) {
+  let model = modelFor(worker);
+  model.window = window;
+  // Track document unload to destroy this worker.
+  // We can't watch for unload event on page's window object as it
+  // prevents bfcache from working:
+  // https://developer.mozilla.org/En/Working_with_BFCache
+  model.windowID = getInnerId(model.window);
+  events.on("inner-window-destroyed", model.documentUnload);
 
-  /**
-   * Receive an event from the content script that need to be sent to
-   * worker.port. Provide a way for composed object to catch all events.
-   */
-  _onContentScriptEvent: function _onContentScriptEvent() {
-    this._port._emit.apply(this._port, arguments);
-  },
+  // Listen to pagehide event in order to freeze the content script
+  // while the document is frozen in bfcache:
+  model.window.addEventListener("pageshow", model.pageShow, true);
+  model.window.addEventListener("pagehide", model.pageHide, true);
+
+  // will set model.contentWorker pointing to the private API:
+  model.contentWorker = WorkerSandbox(worker, model.window);
 
-  /**
-   * Reference to the content side of the worker.
-   * @type {WorkerGlobalScope}
-   */
-  _contentWorker: null,
+  // Mainly enable worker.port.emit to send event to the content worker
+  model.inited = true;
+  model.frozen = false;
 
-  /**
-   * Reference to the window that is accessible from
-   * the content scripts.
-   * @type {Object}
-   */
-  _window: null,
+  // Fire off `attach` event
+  emit(worker, 'attach', window);
 
-  /**
-   * Flag to enable `addon` object injection in document. (bug 612726)
-   * @type {Boolean}
-   */
-  _injectInDocument: false
+  // Process all events and messages that were fired before the
+  // worker was initialized.
+  model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
 });
 
 /**
- * Fired from postMessage and _emitEventToContent, or from the _earlyMessage
+ * Remove all internal references to the attached document
+ * Tells _port to unload itself and removes all the references from itself.
+ */
+detach.define(Worker, function (worker) {
+  let model = modelFor(worker);
+  // maybe unloaded before content side is created
+  if (model.contentWorker)
+    model.contentWorker.destroy();
+  model.contentWorker = null;
+  if (model.window) {
+    model.window.removeEventListener("pageshow", model.pageShow, true);
+    model.window.removeEventListener("pagehide", model.pageHide, true);
+  }
+  model.window = null;
+  // This method may be called multiple times,
+  // avoid dispatching `detach` event more than once
+  if (model.windowID) {
+    model.windowID = null;
+    events.off("inner-window-destroyed", model.documentUnload);
+    model.earlyEvents.length = 0;
+    emit(worker, 'detach');
+  }
+  model.inited = false;
+});
+
+/**
+ * Tells content worker to unload itself and
+ * removes all the references from itself.
+ */
+destroy.define(Worker, function (worker) {
+  detach(worker);
+  modelFor(worker).inited = true;
+  // Specifying no type or listener removes all listeners
+  // from target
+  off(worker);
+});
+
+/**
+ * Events fired by workers
+ */
+function documentUnload ({ subject, data }) {
+  let model = modelFor(this);
+  let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+  if (innerWinID != model.windowID) return false;
+  detach(this);
+  return true;
+}
+
+function pageShow () {
+  let model = modelFor(this);
+  model.contentWorker.emitSync('pageshow');
+  emit(this, 'pageshow');
+  model.frozen = false;
+}
+
+function pageHide () {
+  let model = modelFor(this);
+  model.contentWorker.emitSync('pagehide');
+  emit(this, 'pagehide');
+  model.frozen = true;
+}
+
+/**
+ * Fired from postMessage and emitEventToContent, or from the earlyMessage
  * queue when fired before the content is loaded. Sends arguments to
  * contentWorker if able
  */
 
-function processMessage () {
-  if (!this._contentWorker)
+function processMessage (worker, ...args) {
+  let model = modelFor(worker) || {};
+  if (!model.contentWorker)
     throw new Error(ERR_DESTROYED);
-  if (this._frozen)
+  if (model.frozen)
     throw new Error(ERR_FROZEN);
 
-  this._contentWorker.emit.apply(null, Array.slice(arguments));
+  model.contentWorker.emit.apply(null, args);
 }
 
-exports.Worker = Worker;
+function createModel () {
+  return {
+    // List of messages fired before worker is initialized
+    earlyEvents: [],
+    // Is worker connected to the content worker sandbox ?
+    inited: false,
+    // Is worker being frozen? i.e related document is frozen in bfcache.
+    // Content script should not be reachable if frozen.
+    frozen: true,
+    /**
+     * Reference to the content side of the worker.
+     * @type {WorkerGlobalScope}
+     */
+    contentWorker: null,
+    /**
+     * Reference to the window that is accessible from
+     * the content scripts.
+     * @type {Object}
+     */
+    window: null
+  };
+}
+
+function createPort (worker) {
+  let port = EventTarget();
+  port.emit = emitEventToContent.bind(null, worker);
+  return port;
+}
+
+/**
+ * Emit a custom event to the content script,
+ * i.e. emit this event on `self.port`
+ */
+function emitEventToContent (worker, ...eventArgs) {
+  let model = modelFor(worker);
+  let args = ['event'].concat(eventArgs);
+  if (!model.inited) {
+    model.earlyEvents.push(args);
+    return;
+  }
+  processMessage.apply(null, [worker].concat(args));
+}
+
--- a/addon-sdk/source/lib/sdk/context-menu.js
+++ b/addon-sdk/source/lib/sdk/context-menu.js
@@ -358,36 +358,38 @@ let menuRules = mix(labelledItemRules, {
         return item instanceof BaseItem;
       });
     },
     msg: "items must be an array, and each element in the array must be an " +
          "Item, Menu, or Separator."
   }
 });
 
-let ContextWorker = Worker.compose({
+let ContextWorker = Class({
+  implements: [ Worker ],
+
   //Returns true if any context listeners are defined in the worker's port.
   anyContextListeners: function anyContextListeners() {
-    return this._contentWorker.hasListenerFor("context");
+    return this.getSandbox().hasListenerFor("context");
   },
 
   // Calls the context workers context listeners and returns the first result
   // that is either a string or a value that evaluates to true. If all of the
   // listeners returned false then returns false. If there are no listeners
   // then returns null.
   getMatchedContext: function getCurrentContexts(popupNode) {
-    let results = this._contentWorker.emitSync("context", popupNode);
+    let results = this.getSandbox().emitSync("context", popupNode);
     return results.reduce(function(val, result) val || result, null);
   },
 
   // Emits a click event in the worker's port. popupNode is the node that was
   // context-clicked, and clickedItemData is the data of the item that was
   // clicked.
   fireClick: function fireClick(popupNode, clickedItemData) {
-    this._contentWorker.emitSync("click", popupNode, clickedItemData);
+    this.getSandbox().emitSync("click", popupNode, clickedItemData);
   }
 });
 
 // Returns true if any contexts match. If there are no contexts then a
 // PageContext is tested instead
 function hasMatchingContext(contexts, popupNode) {
   for (let context in contexts) {
     if (!context.isCurrent(popupNode))
--- a/addon-sdk/source/lib/sdk/deprecated/api-utils.js
+++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js
@@ -23,39 +23,16 @@ const VALID_TYPES = [
   "object",
   "string",
   "undefined",
 ];
 
 const { isArray } = Array;
 
 /**
- * Returns a function C that creates instances of privateCtor.  C may be called
- * with or without the new keyword.  The prototype of each instance returned
- * from C is C.prototype, and C.prototype is an object whose prototype is
- * privateCtor.prototype.  Instances returned from C will therefore be instances
- * of both C and privateCtor.  Additionally, the constructor of each instance
- * returned from C is C.
- *
- * @param  privateCtor
- *         A constructor.
- * @return A function that makes new instances of privateCtor.
- */
-exports.publicConstructor = function publicConstructor(privateCtor) {
-  function PublicCtor() {
-    let obj = { constructor: PublicCtor, __proto__: PublicCtor.prototype };
-    memory.track(obj, privateCtor.name);
-    privateCtor.apply(obj, arguments);
-    return obj;
-  }
-  PublicCtor.prototype = { __proto__: privateCtor.prototype };
-  return PublicCtor;
-};
-
-/**
  * Returns a validated options dictionary given some requirements.  If any of
  * the requirements are not met, an exception is thrown.
  *
  * @param  options
  *         An object, the options dictionary to validate.  It's not modified.
  *         If it's null or otherwise falsey, an empty object is assumed.
  * @param  requirements
  *         An object whose keys are the expected keys in options.  Any key in
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/deprecated/app-strings.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-module.metadata = {
-  "stability": "deprecated"
-};
-
-const {Cc,Ci} = require("chrome");
-const apiUtils = require("./api-utils");
-
-/**
- * A bundle of strings.
- *
- * @param url {String}
- *        the URL of the string bundle
- */
-exports.StringBundle = apiUtils.publicConstructor(function StringBundle(url) {
-
-  let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
-                     getService(Ci.nsIStringBundleService).
-                     createBundle(url);
-
-  this.__defineGetter__("url", function () url);
-
-  /**
-   * Get a string from the bundle.
-   *
-   * @param name {String}
-   *        the name of the string to get
-   * @param args {array} [optional]
-   *        an array of arguments that replace occurrences of %S in the string
-   *
-   * @returns {String} the value of the string
-   */
-  this.get = function strings_get(name, args) {
-    try {
-      if (args)
-        return stringBundle.formatStringFromName(name, args, args.length);
-      else
-        return stringBundle.GetStringFromName(name);
-    }
-    catch(ex) {
-      // f.e. "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE)
-      // [nsIStringBundle.GetStringFromName]"
-      throw new Error("String '" + name + "' could not be retrieved from the " +
-                      "bundle due to an unknown error (it doesn't exist?).");
-    }
-  },
-
-  /**
-   * Iterate the strings in the bundle.
-   *
-   */
-  apiUtils.addIterator(
-    this,
-    function keysValsGen() {
-      let enumerator = stringBundle.getSimpleEnumeration();
-      while (enumerator.hasMoreElements()) {
-        let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
-        yield [elem.key, elem.value];
-      }
-    }
-  );
-});
--- a/addon-sdk/source/lib/sdk/deprecated/memory.js
+++ b/addon-sdk/source/lib/sdk/deprecated/memory.js
@@ -1,53 +1,57 @@
 /* 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/. */
-
 "use strict";
 
 module.metadata = {
   "stability": "deprecated"
 };
 
-const {Cc,Ci,Cu,components} = require("chrome");
-var trackedObjects = {};
+const { Cc, Ci, Cu, components } = require("chrome");
+const { when: unload } = require("../system/unload")
 
-var Compacter = {
-  INTERVAL: 5000,
-  notify: function(timer) {
+var trackedObjects = {};
+const Compacter = {
+  notify: function() {
     var newTrackedObjects = {};
+
     for (let name in trackedObjects) {
-      var oldBin = trackedObjects[name];
-      var newBin = [];
-      var strongRefs = [];
-      for (var i = 0; i < oldBin.length; i++) {
-        var strongRef = oldBin[i].weakref.get();
+      let oldBin = trackedObjects[name];
+      let newBin = [];
+      let strongRefs = [];
+
+      for (let i = 0, l = oldBin.length; i < l; i++) {
+        let strongRef = oldBin[i].weakref.get();
+
         if (strongRef && strongRefs.indexOf(strongRef) == -1) {
           strongRefs.push(strongRef);
           newBin.push(oldBin[i]);
         }
       }
+
       if (newBin.length)
         newTrackedObjects[name] = newBin;
     }
+
     trackedObjects = newTrackedObjects;
   }
 };
 
 var timer = Cc["@mozilla.org/timer;1"]
             .createInstance(Ci.nsITimer);
-
 timer.initWithCallback(Compacter,
-                       Compacter.INTERVAL,
+                       5000,
                        Ci.nsITimer.TYPE_REPEATING_SLACK);
 
-var track = exports.track = function track(object, bin, stackFrameNumber) {
+function track(object, bin, stackFrameNumber) {
   var frame = components.stack.caller;
   var weakref = Cu.getWeakReference(object);
+
   if (!bin && 'constructor' in object)
     bin = object.constructor.name;
   if (bin == "Object")
     bin = frame.name;
   if (!bin)
     bin = "generic";
   if (!(bin in trackedObjects))
     trackedObjects[bin] = [];
@@ -56,63 +60,70 @@ var track = exports.track = function tra
     for (var i = 0; i < stackFrameNumber; i++)
       frame = frame.caller;
 
   trackedObjects[bin].push({weakref: weakref,
                             created: new Date(),
                             filename: frame.filename,
                             lineNo: frame.lineNumber,
                             bin: bin});
-};
+}
+exports.track = track;
 
 var getBins = exports.getBins = function getBins() {
   var names = [];
   for (let name in trackedObjects)
     names.push(name);
   return names;
 };
 
-var getObjects = exports.getObjects = function getObjects(bin) {
-  function getLiveObjectsInBin(bin, array) {
-    for (var i = 0; i < bin.length; i++) {
-      var object = bin[i].weakref.get();
-      if (object)
-        array.push(bin[i]);
+function getObjects(bin) {
+  var results = [];
+
+  function getLiveObjectsInBin(bin) {
+    for (let i = 0, l = bin.length; i < l; i++) {
+      let object = bin[i].weakref.get();
+
+      if (object) {
+        results.push(bin[i]);
+      }
     }
   }
 
-  var results = [];
   if (bin) {
     if (bin in trackedObjects)
-      getLiveObjectsInBin(trackedObjects[bin], results);
-  } else
+      getLiveObjectsInBin(trackedObjects[bin]);
+  }
+  else {
     for (let name in trackedObjects)
-      getLiveObjectsInBin(trackedObjects[name], results);
+      getLiveObjectsInBin(trackedObjects[name]);
+  }
+
   return results;
-};
+}
+exports.getObjects = getObjects;
 
-var gc = exports.gc = function gc() {
+function gc() {
   // Components.utils.forceGC() doesn't currently perform
   // cycle collection, which means that e.g. DOM elements
   // won't be collected by it. Fortunately, there are
   // other ways...
-
-  var window = Cc["@mozilla.org/appshell/appShellService;1"]
+  var test_utils = Cc["@mozilla.org/appshell/appShellService;1"]
                .getService(Ci.nsIAppShellService)
-               .hiddenDOMWindow;
-  var test_utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                   .getInterface(Ci.nsIDOMWindowUtils);
+               .hiddenDOMWindow
+               .QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils);
   test_utils.garbageCollect();
+  // Clean metadata for dead objects
   Compacter.notify();
-
   // Not sure why, but sometimes it appears that we don't get
   // them all with just one CC, so let's do it again.
   test_utils.garbageCollect();
 };
+exports.gc = gc;
 
-require("../system/unload").when(
-  function() {
-    trackedObjects = {};
-    if (timer) {
-      timer.cancel();
-      timer = null;
-    }
-  });
+unload(_ => {
+  trackedObjects = {};
+  if (timer) {
+    timer.cancel();
+    timer = null;
+  }
+});
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/deprecated/observer-service.js
+++ /dev/null
@@ -1,134 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-module.metadata = {
-  "stability": "deprecated"
-};
-
-const { Cc, Ci } = require("chrome");
-const { when: unload } = require("../system/unload");
-const { ns } = require("../core/namespace");
-const { on, off, emit, once } = require("../system/events");
-const { id } = require("../self");
-
-const subscribers = ns();
-const cache = [];
-
-/**
- * Topics specifically available to Jetpack-generated extensions.
- *
- * Using these predefined consts instead of the platform strings is good:
- *   - allows us to scope topics specifically for Jetpacks
- *   - addons aren't dependent on strings nor behavior of core platform topics
- *   - the core platform topics are not clearly named
- *
- */
-exports.topics = {
-  /**
-   * A topic indicating that the application is in a state usable
-   * by add-ons.
-   */
-  APPLICATION_READY: id + "_APPLICATION_READY"
-};
-
-function Listener(callback, target) {
-  return function listener({ subject, data }) {
-    callback.call(target || callback, subject, data);
-  }
-}
-
-/**
- * Register the given callback as an observer of the given topic.
- *
- * @param   topic       {String}
- *          the topic to observe
- *
- * @param   callback    {Object}
- *          the callback; an Object that implements nsIObserver or a Function
- *          that gets called when the notification occurs
- *
- * @param   target  {Object}  [optional]
- *          the object to use as |this| when calling a Function callback
- *
- * @returns the observer
- */
-function add(topic, callback, target) {
-  let listeners = subscribers(callback);
-  if (!(topic in listeners)) {
-    let listener = Listener(callback, target);
-    listeners[topic] = listener;
-
-    // Cache callback unless it's already cached.
-    if (!~cache.indexOf(callback))
-      cache.push(callback);
-
-    on(topic, listener);
-  }
-};
-exports.add = add;
-
-/**
- * Unregister the given callback as an observer of the given topic.
- *
- * @param   topic       {String}
- *          the topic being observed
- *
- * @param   callback    {Object}
- *          the callback doing the observing
- *
- * @param   target  {Object}  [optional]
- *          the object being used as |this| when calling a Function callback
- */
-function remove(topic, callback, target) {
-  let listeners = subscribers(callback);
-  if (topic in listeners) {
-    let listener = listeners[topic];
-    delete listeners[topic];
-
-    // If no more observers are registered and callback is still in cache
-    // then remove it.
-    let index = cache.indexOf(callback);
-    if (~index && !Object.keys(listeners).length)
-      cache.splice(index, 1)
-
-    off(topic, listener);
-  }
-};
-exports.remove = remove;
-
-/**
- * Notify observers about something.
- *
- * @param topic   {String}
- *        the topic to notify observers about
- *
- * @param subject {Object}  [optional]
- *        some information about the topic; can be any JS object or primitive
- *
- * @param data    {String}  [optional] [deprecated]
- *        some more information about the topic; deprecated as the subject
- *        is sufficient to pass all needed information to the JS observers
- *        that this module targets; if you have multiple values to pass to
- *        the observer, wrap them in an object and pass them via the subject
- *        parameter (i.e.: { foo: 1, bar: "some string", baz: myObject })
- */
-function notify(topic, subject, data) {
-  emit(topic, {
-    subject: subject === undefined ? null : subject,
-    data: data === undefined ? null : data
-  });
-}
-exports.notify = notify;
-
-unload(function() {
-  // Make a copy of cache first, since cache will be changing as we
-  // iterate through it.
-  cache.slice().forEach(function(callback) {
-    Object.keys(subscribers(callback)).forEach(function(topic) {
-      remove(topic, callback);
-    });
-  });
-})
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/symbiont.js
@@ -0,0 +1,229 @@
+/* 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/. */
+"use strict";
+
+module.metadata = {
+  "stability": "deprecated"
+};
+
+const { Worker } = require('./traits-worker');
+const { Loader } = require('../content/loader');
+const hiddenFrames = require('../frame/hidden-frame');
+const { on, off } = require('../system/events');
+const unload = require('../system/unload');
+const { getDocShell } = require("../frame/utils");
+const { ignoreWindow } = require('../private-browsing/utils');
+
+// Everything coming from add-on's xpi considered an asset.
+const assetsURI = require('../self').data.url().replace(/data\/$/, "");
+
+/**
+ * This trait is layered on top of `Worker` and in contrast to symbiont
+ * Worker constructor requires `content` option that represents content
+ * that will be loaded in the provided frame, if frame is not provided
+ * Worker will create hidden one.
+ */
+const Symbiont = Worker.resolve({
+    constructor: '_initWorker',
+    destroy: '_workerDestroy'
+  }).compose(Loader, {
+
+  /**
+   * The constructor requires all the options that are required by
+   * `require('content').Worker` with the difference that the `frame` option
+   * is optional. If `frame` is not provided, `contentURL` is expected.
+   * @param {Object} options
+   * @param {String} options.contentURL
+   *    URL of a content to load into `this._frame` and create worker for.
+   * @param {Element} [options.frame]
+   *    iframe element that is used to load `options.contentURL` into.
+   *    if frame is not provided hidden iframe will be created.
+   */
+  constructor: function Symbiont(options) {
+    options = options || {};
+
+    if ('contentURL' in options)
+        this.contentURL = options.contentURL;
+    if ('contentScriptWhen' in options)
+      this.contentScriptWhen = options.contentScriptWhen;
+    if ('contentScriptOptions' in options)
+      this.contentScriptOptions = options.contentScriptOptions;
+    if ('contentScriptFile' in options)
+      this.contentScriptFile = options.contentScriptFile;
+    if ('contentScript' in options)
+      this.contentScript = options.contentScript;
+    if ('allow' in options)
+      this.allow = options.allow;
+    if ('onError' in options)
+        this.on('error', options.onError);
+    if ('onMessage' in options)
+        this.on('message', options.onMessage);
+    if ('frame' in options) {
+      this._initFrame(options.frame);
+    }
+    else {
+      let self = this;
+      this._hiddenFrame = hiddenFrames.HiddenFrame({
+        onReady: function onFrame() {
+          self._initFrame(this.element);
+        },
+        onUnload: function onUnload() {
+          // Bug 751211: Remove reference to _frame when hidden frame is
+          // automatically removed on unload, otherwise we are going to face
+          // "dead object" exception
+          self.destroy();
+        }
+      });
+      hiddenFrames.add(this._hiddenFrame);
+    }
+
+    unload.ensure(this._public, "destroy");
+  },
+
+  destroy: function destroy() {
+    this._workerDestroy();
+    this._unregisterListener();
+    this._frame = null;
+    if (this._hiddenFrame) {
+      hiddenFrames.remove(this._hiddenFrame);
+      this._hiddenFrame = null;
+    }
+  },
+
+  /**
+   * XUL iframe or browser elements with attribute `type` being `content`.
+   * Used to create `ContentSymbiont` from.
+   * @type {nsIFrame|nsIBrowser}
+   */
+  _frame: null,
+
+  /**
+   * Listener to the `'frameReady"` event (emitted when `iframe` is ready).
+   * Removes listener, sets right permissions to the frame and loads content.
+   */
+  _initFrame: function _initFrame(frame) {
+    if (this._loadListener)
+      this._unregisterListener();
+
+    this._frame = frame;
+
+    if (getDocShell(frame)) {
+      this._reallyInitFrame(frame);
+    }
+    else {
+      if (this._waitForFrame) {
+        off('content-document-global-created', this._waitForFrame);
+      }
+      this._waitForFrame = this.__waitForFrame.bind(this, frame);
+      on('content-document-global-created', this._waitForFrame);
+    }
+  },
+
+  __waitForFrame: function _waitForFrame(frame, { subject: win }) {
+    if (frame.contentWindow == win) {
+      off('content-document-global-created', this._waitForFrame);
+      delete this._waitForFrame;
+      this._reallyInitFrame(frame);
+    }
+  },
+
+  _reallyInitFrame: function _reallyInitFrame(frame) {
+    getDocShell(frame).allowJavascript = this.allow.script;
+    frame.setAttribute("src", this._contentURL);
+
+    // Inject `addon` object in document if we load a document from
+    // one of our addon folder and if no content script are defined. bug 612726
+    let isDataResource =
+      typeof this._contentURL == "string" &&
+      this._contentURL.indexOf(assetsURI) == 0;
+    let hasContentScript =
+      (Array.isArray(this.contentScript) ? this.contentScript.length > 0
+                                             : !!this.contentScript) ||
+      (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0
+                                             : !!this.contentScriptFile);
+    // If we have to inject `addon` we have to do it before document
+    // script execution, so during `start`:
+    this._injectInDocument = isDataResource && !hasContentScript;
+    if (this._injectInDocument)
+      this.contentScriptWhen = "start";
+
+    if ((frame.contentDocument.readyState == "complete" ||
+        (frame.contentDocument.readyState == "interactive" &&
+         this.contentScriptWhen != 'end' )) &&
+        frame.contentDocument.location == this._contentURL) {
+      // In some cases src doesn't change and document is already ready
+      // (for ex: when the user moves a widget while customizing toolbars.)
+      this._onInit();
+      return;
+    }
+
+    let self = this;
+
+    if ('start' == this.contentScriptWhen) {
+      this._loadEvent = 'start';
+      on('document-element-inserted',
+        this._loadListener = function onStart({ subject: doc }) {
+          let window = doc.defaultView;
+
+          if (ignoreWindow(window)) {
+            return;
+          }
+
+          if (window && window == frame.contentWindow) {
+            self._unregisterListener();
+            self._onInit();
+          }
+
+        });
+      return;
+    }
+
+    let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
+    let self = this;
+    this._loadEvent = eventName;
+    frame.addEventListener(eventName,
+      this._loadListener = function _onReady(event) {
+
+        if (event.target != frame.contentDocument)
+          return;
+        self._unregisterListener();
+
+        self._onInit();
+
+      }, true);
+
+  },
+
+  /**
+   * Unregister listener that watchs for document being ready to be injected.
+   * This listener is registered in `Symbiont._initFrame`.
+   */
+  _unregisterListener: function _unregisterListener() {
+    if (this._waitForFrame) {
+      off('content-document-global-created', this._waitForFrame);
+      delete this._waitForFrame;
+    }
+
+    if (!this._loadListener)
+      return;
+    if (this._loadEvent == "start") {
+      off('document-element-inserted', this._loadListener);
+    }
+    else {
+      this._frame.removeEventListener(this._loadEvent, this._loadListener,
+                                      true);
+    }
+    this._loadListener = null;
+  },
+
+  /**
+   * Called by Symbiont itself when the frame is ready to load
+   * content scripts according to contentScriptWhen. Overloaded by Panel.
+   */
+  _onInit: function () {
+    this._initWorker({ window: this._frame.contentWindow });
+  }
+
+});
+exports.Symbiont = Symbiont;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
@@ -0,0 +1,660 @@
+/* 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/. */
+
+/**
+ *
+ * `deprecated/traits-worker` was previously `content/worker` and kept
+ * only due to `deprecated/symbiont` using it, which is necessary for
+ * `widget`, until that reaches deprecation EOL.
+ *
+ */
+
+"use strict";
+
+module.metadata = {
+  "stability": "deprecated"
+};
+
+const { Trait } = require('./traits');
+const { EventEmitter, EventEmitterTrait } = require('./events');
+const { Ci, Cu, Cc } = require('chrome');
+const timer = require('../timers');
+const { URL } = require('../url');
+const unload = require('../system/unload');
+const observers = require('../system/events');
+const { Cortex } = require('./cortex');
+const { sandbox, evaluate, load } = require("../loader/sandbox");
+const { merge } = require('../util/object');
+const xulApp = require("../system/xul-app");
+const { getInnerId } = require("../window/utils")
+const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
+                                              "17.0a2", "*");
+const { getTabForWindow } = require('../tabs/helpers');
+const { getTabForContentWindow } = require('../tabs/utils');
+
+/* Trick the linker in order to ensure shipping these files in the XPI.
+  require('../content/content-worker.js');
+  Then, retrieve URL of these files in the XPI:
+*/
+let prefix = module.uri.split('deprecated/traits-worker.js')[0];
+const CONTENT_WORKER_URL = prefix + 'content/content-worker.js';
+
+// Fetch additional list of domains to authorize access to for each content
+// script. It is stored in manifest `metadata` field which contains
+// package.json data. This list is originaly defined by authors in
+// `permissions` attribute of their package.json addon file.
+const permissions = require('@loader/options').metadata['permissions'] || {};
+const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
+
+const JS_VERSION = '1.8';
+
+const ERR_DESTROYED =
+  "Couldn't find the worker to receive this message. " +
+  "The script may not be initialized yet, or may already have been unloaded.";
+
+const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
+                   "until it is visible again.";
+
+
+const WorkerSandbox = EventEmitter.compose({
+
+  /**
+   * Emit a message to the worker content sandbox
+   */
+  emit: function emit() {
+    // First ensure having a regular array
+    // (otherwise, `arguments` would be mapped to an object by `stringify`)
+    let array = Array.slice(arguments);
+    // JSON.stringify is buggy with cross-sandbox values,
+    // it may return "{}" on functions. Use a replacer to match them correctly.
+    function replacer(k, v) {
+      return typeof v === "function" ? undefined : v;
+    }
+    // Ensure having an asynchronous behavior
+    let self = this;
+    timer.setTimeout(function () {
+      self._emitToContent(JSON.stringify(array, replacer));
+    }, 0);
+  },
+
+  /**
+   * Synchronous version of `emit`.
+   * /!\ Should only be used when it is strictly mandatory /!\
+   *     Doesn't ensure passing only JSON values.
+   *     Mainly used by context-menu in order to avoid breaking it.
+   */
+  emitSync: function emitSync() {
+    let args = Array.slice(arguments);
+    return this._emitToContent(args);
+  },
+
+  /**
+   * Tells if content script has at least one listener registered for one event,
+   * through `self.on('xxx', ...)`.
+   * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
+   */
+  hasListenerFor: function hasListenerFor(name) {
+    return this._hasListenerFor(name);
+  },
+
+  /**
+   * Method called by the worker sandbox when it needs to send a message
+   */
+  _onContentEvent: function onContentEvent(args) {
+    // As `emit`, we ensure having an asynchronous behavior
+    let self = this;
+    timer.setTimeout(function () {
+      // We emit event to chrome/addon listeners
+      self._emit.apply(self, JSON.parse(args));
+    }, 0);
+  },
+
+  /**
+   * Configures sandbox and loads content scripts into it.
+   * @param {Worker} worker
+   *    content worker
+   */
+  constructor: function WorkerSandbox(worker) {
+    this._addonWorker = worker;
+
+    // Ensure that `emit` has always the right `this`
+    this.emit = this.emit.bind(this);
+    this.emitSync = this.emitSync.bind(this);
+
+    // We receive a wrapped window, that may be an xraywrapper if it's content
+    let window = worker._window;
+    let proto = window;
+
+    // Eventually use expanded principal sandbox feature, if some are given.
+    //
+    // But prevent it when the Worker isn't used for a content script but for
+    // injecting `addon` object into a Panel, Widget, ... scope.
+    // That's because:
+    // 1/ It is useless to use multiple domains as the worker is only used
+    // to communicate with the addon,
+    // 2/ By using it it would prevent the document to have access to any JS
+    // value of the worker. As JS values coming from multiple domain principals
+    // can't be accessed by "mono-principals" (principal with only one domain).
+    // Even if this principal is for a domain that is specified in the multiple
+    // domain principal.
+    let principals  = window;
+    let wantGlobalProperties = []
+    if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
+      principals = EXPANDED_PRINCIPALS.concat(window);
+      // We have to replace XHR constructor of the content document
+      // with a custom cross origin one, automagically added by platform code:
+      delete proto.XMLHttpRequest;
+      wantGlobalProperties.push("XMLHttpRequest");
+    }
+
+    // Instantiate trusted code in another Sandbox in order to prevent content
+    // script from messing with standard classes used by proxy and API code.
+    let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
+    apiSandbox.console = console;
+
+    // Create the sandbox and bind it to window in order for content scripts to
+    // have access to all standard globals (window, document, ...)
+    let content = this._sandbox = sandbox(principals, {
+      sandboxPrototype: proto,
+      wantXrays: true,
+      wantGlobalProperties: wantGlobalProperties,
+      sameZoneAs: window,
+      metadata: { SDKContentScript: true }
+    });
+    // We have to ensure that window.top and window.parent are the exact same
+    // object than window object, i.e. the sandbox global object. But not
+    // always, in case of iframes, top and parent are another window object.
+    let top = window.top === window ? content : content.top;
+    let parent = window.parent === window ? content : content.parent;
+    merge(content, {
+      // We need "this === window === top" to be true in toplevel scope:
+      get window() content,
+      get top() top,
+      get parent() parent,
+      // Use the Greasemonkey naming convention to provide access to the
+      // unwrapped window object so the content script can access document
+      // JavaScript values.
+      // NOTE: this functionality is experimental and may change or go away
+      // at any time!
+      get unsafeWindow() window.wrappedJSObject
+    });
+
+    // Load trusted code that will inject content script API.
+    // We need to expose JS objects defined in same principal in order to
+    // avoid having any kind of wrapper.
+    load(apiSandbox, CONTENT_WORKER_URL);
+
+    // prepare a clean `self.options`
+    let options = 'contentScriptOptions' in worker ?
+      JSON.stringify( worker.contentScriptOptions ) :
+      undefined;
+
+    // Then call `inject` method and communicate with this script
+    // by trading two methods that allow to send events to the other side:
+    //   - `onEvent` called by content script
+    //   - `result.emitToContent` called by addon script
+    // Bug 758203: We have to explicitely define `__exposedProps__` in order
+    // to allow access to these chrome object attributes from this sandbox with
+    // content priviledges
+    // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
+    let chromeAPI = {
+      timers: {
+        setTimeout: timer.setTimeout,
+        setInterval: timer.setInterval,
+        clearTimeout: timer.clearTimeout,
+        clearInterval: timer.clearInterval,
+        __exposedProps__: {
+          setTimeout: 'r',
+          setInterval: 'r',
+          clearTimeout: 'r',
+          clearInterval: 'r'
+        }
+      },
+      sandbox: {
+        evaluate: evaluate,
+        __exposedProps__: {
+          evaluate: 'r',
+        }
+      },
+      __exposedProps__: {
+        timers: 'r',
+        sandbox: 'r',
+      }
+    };
+    let onEvent = this._onContentEvent.bind(this);
+    // `ContentWorker` is defined in CONTENT_WORKER_URL file
+    let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
+    this._emitToContent = result.emitToContent;
+    this._hasListenerFor = result.hasListenerFor;
+
+    // Handle messages send by this script:
+    let self = this;
+    // console.xxx calls
+    this.on("console", function consoleListener(kind) {
+      console[kind].apply(console, Array.slice(arguments, 1));
+    });
+
+    // self.postMessage calls
+    this.on("message", function postMessage(data) {
+      // destroyed?
+      if (self._addonWorker)
+        self._addonWorker._emit('message', data);
+    });
+
+    // self.port.emit calls
+    this.on("event", function portEmit(name, args) {
+      // destroyed?
+      if (self._addonWorker)
+        self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
+    });
+
+    // unwrap, recreate and propagate async Errors thrown from content-script
+    this.on("error", function onError({instanceOfError, value}) {
+      if (self._addonWorker) {
+        let error = value;
+        if (instanceOfError) {
+          error = new Error(value.message, value.fileName, value.lineNumber);
+          error.stack = value.stack;
+          error.name = value.name;
+        }
+        self._addonWorker._emit('error', error);
+      }
+    });
+
+    // Inject `addon` global into target document if document is trusted,
+    // `addon` in document is equivalent to `self` in content script.
+    if (worker._injectInDocument) {
+      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
+      Object.defineProperty(win, "addon", {
+          value: content.self
+        }
+      );
+    }
+
+    // Inject our `console` into target document if worker doesn't have a tab
+    // (e.g Panel, PageWorker, Widget).
+    // `worker.tab` can't be used because bug 804935.
+    if (!getTabForContentWindow(window)) {
+      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
+
+      // export our chrome console to content window, using the same approach
+      // of `ConsoleAPI`:
+      // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
+      //
+      // and described here:
+      // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
+      let con = Cu.createObjectIn(win);
+
+      let genPropDesc = function genPropDesc(fun) {
+        return { enumerable: true, configurable: true, writable: true,
+          value: console[fun] };
+      }
+
+      const properties = {
+        log: genPropDesc('log'),
+        info: genPropDesc('info'),
+        warn: genPropDesc('warn'),
+        error: genPropDesc('error'),
+        debug: genPropDesc('debug'),
+        trace: genPropDesc('trace'),
+        dir: genPropDesc('dir'),
+        group: genPropDesc('group'),
+        groupCollapsed: genPropDesc('groupCollapsed'),
+        groupEnd: genPropDesc('groupEnd'),
+        time: genPropDesc('time'),
+        timeEnd: genPropDesc('timeEnd'),
+        profile: genPropDesc('profile'),
+        profileEnd: genPropDesc('profileEnd'),
+       __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
+                            value: function() {} }
+      };
+
+      Object.defineProperties(con, properties);
+      Cu.makeObjectPropsNormal(con);
+
+      win.console = con;
+    };
+
+    // The order of `contentScriptFile` and `contentScript` evaluation is
+    // intentional, so programs can load libraries like jQuery from script URLs
+    // and use them in scripts.
+    let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
+          : null,
+        contentScript = ('contentScript' in worker) ? worker.contentScript : null;
+
+    if (contentScriptFile) {
+      if (Array.isArray(contentScriptFile))
+        this._importScripts.apply(this, contentScriptFile);
+      else
+        this._importScripts(contentScriptFile);
+    }
+    if (contentScript) {
+      this._evaluate(
+        Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
+      );
+    }
+  },
+  destroy: function destroy() {
+    this.emitSync("detach");
+    this._sandbox = null;
+    this._addonWorker = null;
+  },
+
+  /**
+   * JavaScript sandbox where all the content scripts are evaluated.
+   * {Sandbox}
+   */
+  _sandbox: null,
+
+  /**
+   * Reference to the addon side of the worker.
+   * @type {Worker}
+   */
+  _addonWorker: null,
+
+  /**
+   * Evaluates code in the sandbox.
+   * @param {String} code
+   *    JavaScript source to evaluate.
+   * @param {String} [filename='javascript:' + code]
+   *    Name of the file
+   */
+  _evaluate: function(code, filename) {
+    try {
+      evaluate(this._sandbox, code, filename || 'javascript:' + code);
+    }
+    catch(e) {
+      this._addonWorker._emit('error', e);
+    }
+  },
+  /**
+   * Imports scripts to the sandbox by reading files under urls and
+   * evaluating its source. If exception occurs during evaluation
+   * `"error"` event is emitted on the worker.
+   * This is actually an analog to the `importScript` method in web
+   * workers but in our case it's not exposed even though content
+   * scripts may be able to do it synchronously since IO operation
+   * takes place in the UI process.
+   */
+  _importScripts: function _importScripts(url) {
+    let urls = Array.slice(arguments, 0);
+    for each (let contentScriptFile in urls) {
+      try {
+        let uri = URL(contentScriptFile);
+        if (uri.scheme === 'resource')
+          load(this._sandbox, String(uri));
+        else
+          throw Error("Unsupported `contentScriptFile` url: " + String(uri));
+      }
+      catch(e) {
+        this._addonWorker._emit('error', e);
+      }
+    }
+  }
+});
+
+/**
+ * Message-passing facility for communication between code running
+ * in the content and add-on process.
+ * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
+ */
+const Worker = EventEmitter.compose({
+  on: Trait.required,
+  _removeAllListeners: Trait.required,
+
+  // List of messages fired before worker is initialized
+  get _earlyEvents() {
+    delete this._earlyEvents;
+    this._earlyEvents = [];
+    return this._earlyEvents;
+  },
+
+  /**
+   * Sends a message to the worker's global scope. Method takes single
+   * argument, which represents data to be sent to the worker. The data may
+   * be any primitive type value or `JSON`. Call of this method asynchronously
+   * emits `message` event with data value in the global scope of this
+   * symbiont.
+   *
+   * `message` event listeners can be set either by calling
+   * `self.on` with a first argument string `"message"` or by
+   * implementing `onMessage` function in the global scope of this worker.
+   * @param {Number|String|JSON} data
+   */
+  postMessage: function (data) {
+    let args = ['message'].concat(Array.slice(arguments));
+    if (!this._inited) {
+      this._earlyEvents.push(args);
+      return;
+    }
+    processMessage.apply(this, args);
+  },
+
+  /**
+   * EventEmitter, that behaves (calls listeners) asynchronously.
+   * A way to send customized messages to / from the worker.
+   * Events from in the worker can be observed / emitted via
+   * worker.on / worker.emit.
+   */
+  get port() {
+    // We generate dynamically this attribute as it needs to be accessible
+    // before Worker.constructor gets called. (For ex: Panel)
+
+    // create an event emitter that receive and send events from/to the worker
+    this._port = EventEmitterTrait.create({
+      emit: this._emitEventToContent.bind(this)
+    });
+
+    // expose wrapped port, that exposes only public properties:
+    // We need to destroy this getter in order to be able to set the
+    // final value. We need to update only public port attribute as we never
+    // try to access port attribute from private API.
+    delete this._public.port;
+    this._public.port = Cortex(this._port);
+    // Replicate public port to the private object
+    delete this.port;
+    this.port = this._public.port;
+
+    return this._port;
+  },
+
+  /**
+   * Same object than this.port but private API.
+   * Allow access to _emit, in order to send event to port.
+   */
+  _port: null,
+
+  /**
+   * Emit a custom event to the content script,
+   * i.e. emit this event on `self.port`
+   */
+  _emitEventToContent: function () {
+    let args = ['event'].concat(Array.slice(arguments));
+    if (!this._inited) {
+      this._earlyEvents.push(args);
+      return;
+    }
+    processMessage.apply(this, args);
+  },
+
+  // Is worker connected to the content worker sandbox ?
+  _inited: false,
+
+  // Is worker being frozen? i.e related document is frozen in bfcache.
+  // Content script should not be reachable if frozen.
+  _frozen: true,
+
+  constructor: function Worker(options) {
+    options = options || {};
+
+    if ('contentScriptFile' in options)
+      this.contentScriptFile = options.contentScriptFile;
+    if ('contentScriptOptions' in options)
+      this.contentScriptOptions = options.contentScriptOptions;
+    if ('contentScript' in options)
+      this.contentScript = options.contentScript;
+
+    this._setListeners(options);
+
+    unload.ensure(this._public, "destroy");
+
+    // Ensure that worker._port is initialized for contentWorker to be able
+    // to send events during worker initialization.
+    this.port;
+
+    this._documentUnload = this._documentUnload.bind(this);
+    this._pageShow = this._pageShow.bind(this);
+    this._pageHide = this._pageHide.bind(this);
+
+    if ("window" in options) this._attach(options.window);
+  },
+
+  _setListeners: function(options) {
+    if ('onError' in options)
+      this.on('error', options.onError);
+    if ('onMessage' in options)
+      this.on('message', options.onMessage);
+    if ('onDetach' in options)
+      this.on('detach', options.onDetach);
+  },
+
+  _attach: function(window) {
+    this._window = window;
+    // Track document unload to destroy this worker.
+    // We can't watch for unload event on page's window object as it
+    // prevents bfcache from working:
+    // https://developer.mozilla.org/En/Working_with_BFCache
+    this._windowID = getInnerId(this._window);
+    observers.on("inner-window-destroyed", this._documentUnload);
+
+    // Listen to pagehide event in order to freeze the content script
+    // while the document is frozen in bfcache:
+    this._window.addEventListener("pageshow", this._pageShow, true);
+    this._window.addEventListener("pagehide", this._pageHide, true);
+
+    // will set this._contentWorker pointing to the private API:
+    this._contentWorker = WorkerSandbox(this);
+
+    // Mainly enable worker.port.emit to send event to the content worker
+    this._inited = true;
+    this._frozen = false;
+
+    // Process all events and messages that were fired before the
+    // worker was initialized.
+    this._earlyEvents.forEach((function (args) {
+      processMessage.apply(this, args);
+    }).bind(this));
+  },
+
+  _documentUnload: function _documentUnload({ subject, data }) {
+    let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+    if (innerWinID != this._windowID) return false;
+    this._workerCleanup();
+    return true;
+  },
+
+  _pageShow: function _pageShow() {
+    this._contentWorker.emitSync("pageshow");
+    this._emit("pageshow");
+    this._frozen = false;
+  },
+
+  _pageHide: function _pageHide() {
+    this._contentWorker.emitSync("pagehide");
+    this._emit("pagehide");
+    this._frozen = true;
+  },
+
+  get url() {
+    // this._window will be null after detach
+    return this._window ? this._window.document.location.href : null;
+  },
+
+  get tab() {
+    // this._window will be null after detach
+    if (this._window)
+      return getTabForWindow(this._window);
+    return null;
+  },
+
+  /**
+   * Tells content worker to unload itself and
+   * removes all the references from itself.
+   */
+  destroy: function destroy() {
+    this._workerCleanup();
+    this._inited = true;
+    this._removeAllListeners();
+  },
+
+  /**
+   * Remove all internal references to the attached document
+   * Tells _port to unload itself and removes all the references from itself.
+   */
+  _workerCleanup: function _workerCleanup() {
+    // maybe unloaded before content side is created
+    // As Symbiont call worker.constructor on document load
+    if (this._contentWorker)
+      this._contentWorker.destroy();
+    this._contentWorker = null;
+    if (this._window) {
+      this._window.removeEventListener("pageshow", this._pageShow, true);
+      this._window.removeEventListener("pagehide", this._pageHide, true);
+    }
+    this._window = null;
+    // This method may be called multiple times,
+    // avoid dispatching `detach` event more than once
+    if (this._windowID) {
+      this._windowID = null;
+      observers.off("inner-window-destroyed", this._documentUnload);
+      this._earlyEvents.length = 0;
+      this._emit("detach");
+    }
+    this._inited = false;
+  },
+
+  /**
+   * Receive an event from the content script that need to be sent to
+   * worker.port. Provide a way for composed object to catch all events.
+   */
+  _onContentScriptEvent: function _onContentScriptEvent() {
+    this._port._emit.apply(this._port, arguments);
+  },
+
+  /**
+   * Reference to the content side of the worker.
+   * @type {WorkerGlobalScope}
+   */
+  _contentWorker: null,
+
+  /**
+   * Reference to the window that is accessible from
+   * the content scripts.
+   * @type {Object}
+   */
+  _window: null,
+
+  /**
+   * Flag to enable `addon` object injection in document. (bug 612726)
+   * @type {Boolean}
+   */
+  _injectInDocument: false
+});
+
+/**
+ * Fired from postMessage and _emitEventToContent, or from the _earlyMessage
+ * queue when fired before the content is loaded. Sends arguments to
+ * contentWorker if able
+ */
+
+function processMessage () {
+  if (!this._contentWorker)
+    throw new Error(ERR_DESTROYED);
+  if (this._frozen)
+    throw new Error(ERR_FROZEN);
+
+  this._contentWorker.emit.apply(null, Array.slice(arguments));
+}
+
+exports.Worker = Worker;
--- a/addon-sdk/source/lib/sdk/event/core.js
+++ b/addon-sdk/source/lib/sdk/event/core.js
@@ -11,16 +11,17 @@ module.metadata = {
 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 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] = [];
@@ -156,15 +157,16 @@ 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();
-    let listener = listeners[key];
+    if (!type) return;
 
-    if (type && typeof(listener) === 'function')
+    let listener = listeners[key];
+    if (typeof(listener) === 'function')
       on(target, type, listener);
   });
 }
 exports.setListeners = setListeners;
--- a/addon-sdk/source/lib/sdk/event/utils.js
+++ b/addon-sdk/source/lib/sdk/event/utils.js
@@ -2,17 +2,17 @@
  * 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/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-let { emit, on, once, off } = require("./core");
+let { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core");
 
 // This module provides set of high order function for working with event
 // streams (streams in a NodeJS style that dispatch data, end and error
 // events).
 
 // Function takes a `target` object and returns set of implicit references
 // (non property references) it keeps. This basically allows defining
 // references between objects without storing the explicitly. See transform for
@@ -250,8 +250,26 @@ Reactor.prototype.onNext = function(pres
 Reactor.prototype.run = function(input) {
   on(input, "data", message => this.onNext(message, input.value));
   on(input, "end", () => this.onEnd(input.value));
   start(input);
   this.value = input.value;
   this.onStart(input.value);
 };
 exports.Reactor = Reactor;
+
+/**
+ * Takes an object used as options with potential keys like 'onMessage',
+ * used to be called `require('sdk/event/core').setListeners` on.
+ * This strips all keys that would trigger a listener to be set.
+ * 
+ * @params {Object} object
+ * @return {Object}
+ */
+
+function stripListeners (object) {
+  return Object.keys(object).reduce((agg, key) => {
+    if (!EVENT_TYPE_PATTERN.test(key))
+      agg[key] = object[key];
+    return agg;
+  }, {});
+}
+exports.stripListeners = stripListeners;
--- a/addon-sdk/source/lib/sdk/l10n/prefs.js
+++ b/addon-sdk/source/lib/sdk/l10n/prefs.js
@@ -1,21 +1,20 @@
 /* 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/. */
-
 "use strict";
 
-const observers = require("../deprecated/observer-service");
+const { on } = require("../system/events");
 const core = require("./core");
 const { id: jetpackId} = require('../self');
 
 const OPTIONS_DISPLAYED = "addon-options-displayed";
 
-function onOptionsDisplayed(document, addonId) {
+function onOptionsDisplayed({ subjec: document, data: addonId }) {
   if (addonId !== jetpackId)
     return;
   let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' +
               'button[data-jetpack-id="' + jetpackId + '"][pref-name]';
   let nodes = document.querySelectorAll(query);
   for (let node of nodes) {
     let name = node.getAttribute("pref-name");
     if (node.tagName == "setting") {
@@ -35,10 +34,9 @@ function onOptionsDisplayed(document, ad
     }
     else if (node.tagName == "button") {
       let label = core.get(name + "_label");
       if (label)
         node.setAttribute("label", label);
     }
   }
 }
-
-observers.add(OPTIONS_DISPLAYED, onOptionsDisplayed);
+on(OPTIONS_DISPLAYED, onOptionsDisplayed);
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -2,17 +2,17 @@
  * 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/. */
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
-const observers = require('./deprecated/observer-service');
+const observers = require('./system/events');
 const { Loader, validationAttributes } = require('./content/loader');
 const { Worker } = require('./content/worker');
 const { Registry } = require('./util/registry');
 const { EventEmitter } = require('./deprecated/events');
 const { on, emit } = require('./event/core');
 const { validateOptions : validate } = require('./deprecated/api-utils');
 const { Cc, Ci } = require('chrome');
 const { merge } = require('./util/object');
@@ -95,17 +95,17 @@ const PageMod = Loader.compose(EventEmit
                         ' `top` or `frame` value');
     }
     else {
       this.attachTo = ["top", "frame"];
     }
 
     let include = options.include;
     let rules = this.include = Rules();
-    
+
     if (!include)
       throw new Error('The `include` option must always contain atleast one rule');
 
     rules.add.apply(rules, [].concat(include));
 
     if (contentStyle || contentStyleFile) {
       this._style = Style({
         uri: contentStyleFile,
@@ -212,34 +212,34 @@ exports.PageMod = function(options) Page
 exports.PageMod.prototype = PageMod.prototype;
 
 const PageModManager = Registry.resolve({
   constructor: '_init',
   _destructor: '_registryDestructor'
 }).compose({
   constructor: function PageModRegistry(constructor) {
     this._init(PageMod);
-    observers.add(
+    observers.on(
       'document-element-inserted',
       this._onContentWindow = this._onContentWindow.bind(this)
     );
   },
   _destructor: function _destructor() {
-    observers.remove('document-element-inserted', this._onContentWindow);
+    observers.off('document-element-inserted', this._onContentWindow);
     this._removeAllListeners();
 
     // We need to do some cleaning er PageMods, like unregistering any
     // `contentStyle*`
     this._registry.forEach(function(pageMod) {
       pageMod.destroy();
     });
 
     this._registryDestructor();
   },
-  _onContentWindow: function _onContentWindow(document) {
+  _onContentWindow: function _onContentWindow({ subject: document }) {
     let window = document.defaultView;
     // XML documents don't have windows, and we don't yet support them.
     if (!window)
       return;
     // We apply only on documents in tabs of Firefox
     if (!getTabForContentWindow(window))
       return;
 
--- a/addon-sdk/source/lib/sdk/page-worker.js
+++ b/addon-sdk/source/lib/sdk/page-worker.js
@@ -4,18 +4,19 @@
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
 const { Class } = require('./core/heritage');
 const { on, emit, off, setListeners } = require('./event/core');
-const { filter, pipe, map, merge: streamMerge } = require('./event/utils');
-const { WorkerHost, Worker, detach, attach, destroy } = require('./worker/utils');
+const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./event/utils');
+const { detach, attach, destroy, WorkerHost } = require('./content/utils');
+const { Worker } = require('./content/worker');
 const { Disposable } = require('./core/disposable');
 const { EventTarget } = require('./event/target');
 const { unload } = require('./system/unload');
 const { events, streamEventsFrom } = require('./content/events');
 const { getAttachEventType } = require('./content/utils');
 const { window } = require('./addon/window');
 const { getParentWindow } = require('./window/utils');
 const { create: makeFrame, getDocShell } = require('./frame/utils');
@@ -61,18 +62,18 @@ function enableScript (page) {
 }
 
 function disableScript (page) {
   getDocShell(viewFor(page)).allowJavascript = false;
 }
 
 function Allow (page) {
   return {
-    get script() getDocShell(viewFor(page)).allowJavascript,
-    set script(value) value ? enableScript(page) : disableScript(page)
+    get script() { return getDocShell(viewFor(page)).allowJavascript; },
+    set script(value) { return value ? enableScript(page) : disableScript(page); }
   };
 }
 
 function injectWorker ({page}) {
   let worker = workerFor(page);
   let view = viewFor(page);
   if (isValidURL(page, view.contentDocument.URL))
     attach(worker, view.contentWindow);
@@ -84,42 +85,44 @@ const Page = Class({
   implements: [
     EventTarget,
     Disposable
   ],
   extends: WorkerHost(workerFor),
   setup: function Page(options) {
     let page = this;
     options = pageContract(options);
-    setListeners(this, options);
     let view = makeFrame(window.document, {
       nodeName: 'iframe',
       type: 'content',
       uri: options.contentURL,
       allowJavascript: options.allow.script,
       allowPlugins: true,
       allowAuth: true
     });
 
     ['contentScriptFile', 'contentScript', 'contentScriptWhen']
-      .forEach(function (prop) page[prop] = options[prop]);
+      .forEach(prop => page[prop] = options[prop]);
 
     views.set(this, view);
     pages.set(view, this);
 
-    let worker = new Worker(options);
+    // Set listeners on the {Page} object itself, not the underlying worker,
+    // like `onMessage`, as it gets piped
+    setListeners(this, options);
+    let worker = new Worker(stripListeners(options));
     workers.set(this, worker);
     pipe(worker, this);
 
     if (this.include || options.include) {
       this.rules = Rules();
       this.rules.add.apply(this.rules, [].concat(this.include || options.include));
     }
   },
-  get allow() Allow(this),
+  get allow() { return Allow(this); },
   set allow(value) {
     let allowJavascript = pageContract({ allow: value }).allow.script;
     return allowJavascript ? enableScript(this) : disableScript(this);
   },
   get contentURL() { return viewFor(this).getAttribute('src'); },
   set contentURL(value) {
     if (!isValidURL(this, value)) return;
     let view = viewFor(this);
@@ -128,17 +131,17 @@ const Page = Class({
   },
   dispose: function () {
     if (isDisposed(this)) return;
     let view = viewFor(this);
     if (view.parentNode) view.parentNode.removeChild(view);
     views.delete(this);
     destroy(workers.get(this));
   },
-  toString: function () '[object Page]'
+  toString: function () { return '[object Page]' }
 });
 
 exports.Page = Page;
 
 let pageEvents = streamMerge([events, streamEventsFrom(window)]);
 let readyEvents = filter(pageEvents, isReadyEvent);
 let formattedEvents = map(readyEvents, function({target, type}) {
   return { type: type, page: pageFromDoc(target) };
--- a/addon-sdk/source/lib/sdk/panel.js
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -14,27 +14,27 @@ module.metadata = {
 
 const { Ci } = require("chrome");
 const { validateOptions: valid } = require('./deprecated/api-utils');
 const { setTimeout } = require('./timers');
 const { isPrivateBrowsingSupported } = require('./self');
 const { isWindowPBSupported } = require('./private-browsing/utils');
 const { Class } = require("./core/heritage");
 const { merge } = require("./util/object");
-const { WorkerHost, Worker, detach, attach, destroy,
-        requiresAddonGlobal } = require("./worker/utils");
+const { WorkerHost, detach, attach, destroy } = require("./content/utils");
+const { Worker } = require("./content/worker");
 const { Disposable } = require("./core/disposable");
 const { contract: loaderContract } = require("./content/loader");
 const { contract } = require("./util/contract");
 const { on, off, emit, setListeners } = require("./event/core");
 const { EventTarget } = require("./event/target");
 const domPanel = require("./panel/utils");
 const { events } = require("./panel/events");
 const systemEvents = require("./system/events");
-const { filter, pipe } = require("./event/utils");
+const { filter, pipe, stripListeners } = require("./event/utils");
 const { getNodeView, getActiveView } = require("./view/core");
 const { isNil, isObject } = require("./lang/type");
 const { getAttachEventType } = require("./content/utils");
 
 let number = { is: ['number', 'undefined', 'null'] };
 let boolean = { is: ['boolean', 'undefined', 'null'] };
 
 let rectContract = contract({
@@ -112,30 +112,30 @@ const Panel = Class({
     let model = merge({
       defaultWidth: 320,
       defaultHeight: 240,
       focus: true,
       position: Object.freeze({}),
     }, panelContract(options));
     models.set(this, model);
 
-    // Setup listeners.
-    setListeners(this, options);
 
     // Setup view
     let view = domPanel.make();
     panels.set(view, this);
     views.set(this, view);
 
     // Load panel content.
     domPanel.setURL(view, model.contentURL);
 
     setupAutoHide(this);
 
-    let worker = new Worker(options);
+    // Setup listeners.
+    setListeners(this, options);
+    let worker = new Worker(stripListeners(options));
     workers.set(this, worker);
 
     // pipe events from worker to a panel.
     pipe(worker, this);
   },
   dispose: function dispose() {
     this.hide();
     off(this);
--- a/addon-sdk/source/lib/sdk/simple-prefs.js
+++ b/addon-sdk/source/lib/sdk/simple-prefs.js
@@ -3,30 +3,24 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 module.metadata = {
   "stability": "experimental"
 };
 
 const { emit, off } = require("./event/core");
-const { when: unload } = require("./system/unload");
 const { PrefsTarget } = require("./preferences/event-target");
 const { id } = require("./self");
-const observers = require("./deprecated/observer-service");
+const { on } = require("./system/events");
 
 const ADDON_BRANCH = "extensions." + id + ".";
 const BUTTON_PRESSED = id + "-cmdPressed";
 
 const target = PrefsTarget({ branchName: ADDON_BRANCH });
 
 // Listen to clicks on buttons
-function buttonClick(subject, data) {
+function buttonClick({ data }) {
   emit(target, data);
 }
-observers.add(BUTTON_PRESSED, buttonClick);
-
-// Make sure we cleanup listeners on unload.
-unload(function() {
-  observers.remove(BUTTON_PRESSED, buttonClick);
-});
+on(BUTTON_PRESSED, buttonClick);
 
 module.exports = target;
--- a/addon-sdk/source/lib/sdk/test/harness.js
+++ b/addon-sdk/source/lib/sdk/test/harness.js
@@ -1,33 +1,40 @@
 /* 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/. */
-
 "use strict";
 
 module.metadata = {
   "stability": "experimental"
 };
 
 const { Cc, Ci, Cu } = require("chrome");
 const { Loader } = require('./loader');
 const { serializeStack, parseStack  } = require("toolkit/loader");
 const { setTimeout } = require('../timers');
-const memory = require('../deprecated/memory');
 const { PlainTextConsole } = require("../console/plain-text");
 const { when: unload } = require("../system/unload");
 const { format, fromException }  = require("../console/traceback");
 const system = require("../system");
+const memory = require('../deprecated/memory');
+const { gc: gcPromise } = require('./memory');
+const { defer } = require('../core/promise');
 
 // Trick manifest builder to make it think we need these modules ?
 const unit = require("../deprecated/unit-test");
 const test = require("../../test");
 const url = require("../url");
 
+function emptyPromise() {
+  let { promise, resolve } = defer();
+  resolve();
+  return promise;
+}
+
 var cService = Cc['@mozilla.org/consoleservice;1'].getService()
                .QueryInterface(Ci.nsIConsoleService);
 
 // The console used to log messages
 var testConsole;
 
 // Cuddlefish loader in which we load and execute tests.
 var loader;
@@ -138,67 +145,61 @@ function dictDiff(last, curr) {
     var result = curr[name] - (last[name] || 0);
     if (result)
       diff[name] = (result > 0 ? "+" : "") + result;
   }
   return diff;
 }
 
 function reportMemoryUsage() {
-  memory.gc();
-
-  var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
-            .getService(Ci.nsIMemoryReporterManager);
-
-  // Bug 916501: this code is *so* bogus -- nsIMemoryReporter changed its |memoryUsed|
-  // field to |amount| *years* ago, and even bigger changes have happened
-  // since -- that it must just never be run.
-  var reporters = mgr.enumerateReporters();
-  if (reporters.hasMoreElements())
-    print("\n");
-
-  while (reporters.hasMoreElements()) {
-    var reporter = reporters.getNext();
-    reporter.QueryInterface(Ci.nsIMemoryReporter);
-    print(reporter.description + ": " + reporter.memoryUsed + "\n");
+  if (!profileMemory) {
+    return emptyPromise();
   }
 
-  var weakrefs = [info.weakref.get()
-                  for each (info in memory.getObjects())];
-  weakrefs = [weakref for each (weakref in weakrefs) if (weakref)];
-  print("Tracked memory objects in testing sandbox: " +
-        weakrefs.length + "\n");
+  return gcPromise().then((function () {
+    var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+              .getService(Ci.nsIMemoryReporterManager);
+    let count = 0;
+    function logReporter(process, path, kind, units, amount, description) {
+      print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n");
+    }
+    mgr.getReportsForThisProcess(logReporter, null);
+
+    var weakrefs = [info.weakref.get()
+                    for each (info in memory.getObjects())];
+    weakrefs = [weakref for each (weakref in weakrefs) if (weakref)];
+    print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n");
+  }));
 }
 
 var gWeakrefInfo;
 
 function checkMemory() {
-  memory.gc();
-  Cu.schedulePreciseGC(function () {
+  return gcPromise().then(_ => {
     let leaks = getPotentialLeaks();
 
     let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) {
       return !(url in startLeaks.compartments);
     });
 
     let windowURLs = Object.keys(leaks.windows).filter(function(url) {
       return !(url in startLeaks.windows);
     });
 
     for (let url of compartmentURLs)
       console.warn("LEAKED", leaks.compartments[url]);
 
     for (let url of windowURLs)
       console.warn("LEAKED", leaks.windows[url]);
-
-    showResults();
-  });
+  }).then(showResults);
 }
 
 function showResults() {
+  let { promise, resolve } = defer();
+
   if (gWeakrefInfo) {
     gWeakrefInfo.forEach(
       function(info) {
         var ref = info.weakref.get();
         if (ref !== null) {
           var data = ref.__url__ ? ref.__url__ : ref;
           var warning = data == "[object Object]"
             ? "[object " + data.constructor.name + "(" +
@@ -206,16 +207,19 @@ function showResults() {
             : data;
           console.warn("LEAK", warning, info.bin);
         }
       }
     );
   }
 
   onDone(results);
+
+  resolve();
+  return promise;
 }
 
 function cleanup() {
   let coverObject = {};
   try {
     for (let name in loader.modules)
       memory.track(loader.modules[name],
                            "module global scope: " + name);
@@ -245,17 +249,18 @@ function cleanup() {
     if (typeof loader.globals.global == "object") {
       coverObject = loader.globals.global['__$coverObject'] || {};
     }
 
     consoleListener.errorsLogged = 0;
     loader = null;
 
     memory.gc();
-  } catch (e) {
+  }
+  catch (e) {
     results.failed++;
     console.error("unload.send() threw an exception.");
     console.exception(e);
   };
 
   setTimeout(require('@test/options').checkMemory ? checkMemory : showResults, 1);
 
   // dump the coverobject
@@ -328,34 +333,33 @@ function getPotentialLeaks() {
       if (matches[1] in compartments)
         return;
 
       let details = compartmentDetails.exec(matches[1]);
       if (!details) {
         console.error("Unable to parse compartment detail " + matches[1]);
         return;
       }
- 
+
       let item = {
         path: matches[1],
         principal: details[1],
         location: details[2] ? details[2].replace("\\", "/", "g") : undefined,
         source: details[3] ? details[3].split(" -> ").reverse() : undefined,
         toString: function() this.location
       };
 
       if (!isPossibleLeak(item))
         return;
 
       compartments[matches[1]] = item;
       return;
     }
 
-    matches = windowRegexp.exec(path);
-    if (matches) {
+    if (matches = windowRegexp.exec(path)) {
       if (matches[1] in windows)
         return;
 
       let details = windowDetails.exec(matches[1]);
       if (!details) {
         console.error("Unable to parse window detail " + matches[1]);
         return;
       }
@@ -369,45 +373,50 @@ function getPotentialLeaks() {
 
       if (!isPossibleLeak(item))
         return;
 
       windows[matches[1]] = item;
     }
   }
 
-  let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
-            getService(Ci.nsIMemoryReporterManager);
-
-  mgr.getReportsForThisProcess(logReporter, null);
+  Cc["@mozilla.org/memory-reporter-manager;1"]
+    .getService(Ci.nsIMemoryReporterManager)
+    .getReportsForThisProcess(logReporter, null);
 
   return { compartments: compartments, windows: windows };
 }
 
 function nextIteration(tests) {
   if (tests) {
     results.passed += tests.passed;
     results.failed += tests.failed;
 
-    if (profileMemory)
-      reportMemoryUsage();
+    reportMemoryUsage().then(_ => {
+      let testRun = [];
+      for each (let test in tests.testRunSummary) {
+        let testCopy = {};
+        for (let info in test) {
+          testCopy[info] = test[info];
+        }
+        testRun.push(testCopy);
+      }
 
-    let testRun = [];
-    for each (let test in tests.testRunSummary) {
-      let testCopy = {};
-      for (let info in test) {
-        testCopy[info] = test[info];
-      }
-      testRun.push(testCopy);
-    }
+      results.testRuns.push(testRun);
+      iterationsLeft--;
 
-    results.testRuns.push(testRun);
-    iterationsLeft--;
+      checkForEnd();
+    })
   }
+  else {
+    checkForEnd();
+  }
+}
 
+function checkForEnd() {
   if (iterationsLeft && (!stopOnError || results.failed == 0)) {
     // Pass the loader which has a hooked console that doesn't dispatch
     // errors to the JS console and avoid firing false alarm in our
     // console listener
     findAndRunTests(loader, nextIteration);
   }
   else {
     setTimeout(cleanup, 0);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/memory.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const { Cu } = require("chrome");
+const memory = require('../deprecated/memory');
+const { defer } = require('../core/promise');
+
+function gc() {
+  let { promise, resolve } = defer();
+
+  Cu.forceGC();
+  memory.gc();
+
+  Cu.schedulePreciseGC(_ => resolve());
+
+  return promise;
+}
+exports.gc = gc;
--- a/addon-sdk/source/lib/sdk/ui/button/action.js
+++ b/addon-sdk/source/lib/sdk/ui/button/action.js
@@ -5,16 +5,27 @@
 
 module.metadata = {
   'stability': 'experimental',
   'engines': {
     'Firefox': '> 28'
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
+}
+catch (e) {
+  throw Error('Unsupported Application: The module ' + module.id +
+              ' does not support this application.');
+}
+
 const { Class } = require('../../core/heritage');
 const { merge } = require('../../util/object');
 const { Disposable } = require('../../core/disposable');
 const { on, off, emit, setListeners } = require('../../event/core');
 const { EventTarget } = require('../../event/target');
 
 const view = require('./view');
 const { buttonContract, stateContract } = require('./contract');
--- a/addon-sdk/source/lib/sdk/ui/button/toggle.js
+++ b/addon-sdk/source/lib/sdk/ui/button/toggle.js
@@ -5,16 +5,27 @@
 
 module.metadata = {
   'stability': 'experimental',
   'engines': {
     'Firefox': '> 28'
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
+}
+catch (e) {
+  throw Error('Unsupported Application: The module ' + module.id +
+              ' does not support this application.');
+}
+
 const { Class } = require('../../core/heritage');
 const { merge } = require('../../util/object');
 const { Disposable } = require('../../core/disposable');
 const { on, off, emit, setListeners } = require('../../event/core');
 const { EventTarget } = require('../../event/target');
 
 const view = require('./view');
 const { toggleButtonContract, toggleStateContract } = require('./contract');
--- a/addon-sdk/source/lib/sdk/ui/button/view.js
+++ b/addon-sdk/source/lib/sdk/ui/button/view.js
@@ -9,17 +9,18 @@ module.metadata = {
     'Firefox': '> 28'
   }
 };
 
 const { Cu } = require('chrome');
 const { on, off, emit } = require('../../event/core');
 
 const { id: addonID, data } = require('sdk/self');
-const buttonPrefix = 'button--' + addonID.replace(/@/g, '-at-');
+const buttonPrefix =
+  'button--' + addonID.toLowerCase().replace(/[^a-z0-9-_]/g, '');
 
 const { isObject } = require('../../lang/type');
 
 const { getMostRecentBrowserWindow } = require('../../window/utils');
 const { ignoreWindow } = require('../../private-browsing/utils');
 const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
 const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
 
--- a/addon-sdk/source/lib/sdk/ui/frame.js
+++ b/addon-sdk/source/lib/sdk/ui/frame.js
@@ -5,12 +5,23 @@
 
 module.metadata = {
   "stability": "experimental",
   "engines": {
     "Firefox": "> 28"
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {});
+}
+catch (e) {
+  throw Error("Unsupported Application: The module"  + module.id +
+              " does not support this application.");
+}
+
 require("./frame/view");
 const { Frame } = require("./frame/model");
 
 exports.Frame = Frame;
--- a/addon-sdk/source/lib/sdk/ui/sidebar.js
+++ b/addon-sdk/source/lib/sdk/ui/sidebar.js
@@ -19,32 +19,26 @@ const { URL } = require('../url');
 const { add, remove, has, clear, iterator } = require('../lang/weak-set');
 const { id: addonID } = require('../self');
 const { WindowTracker } = require('../deprecated/window-utils');
 const { isShowing } = require('./sidebar/utils');
 const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils');
 const { ns } = require('../core/namespace');
 const { remove: removeFromArray } = require('../util/array');
 const { show, hide, toggle } = require('./sidebar/actions');
-const { Worker: WorkerTrait } = require('../content/worker');
+const { Worker } = require('../content/worker');
 const { contract: sidebarContract } = require('./sidebar/contract');
 const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view');
 const { defer } = require('../core/promise');
 const { models, views, viewsFor, modelFor } = require('./sidebar/namespace');
 const { isLocalURL } = require('../url');
 const { ensure } = require('../system/unload');
 const { identify } = require('./id');
 const { uuid } = require('../util/uuid');
 
-const Worker = WorkerTrait.resolve({
-  _injectInDocument: '__injectInDocument'
-}).compose({
-  get _injectInDocument() true
-});
-
 const sidebarNS = ns();
 
 const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
 
 let sidebars = {};
 
 const Sidebar = Class({
   implements: [ Disposable ],
@@ -113,17 +107,18 @@ const Sidebar = Class({
           let sbTitle = window.document.getElementById('sidebar-title');
           function onWebPanelSidebarCreated() {
             if (panelBrowser.contentWindow.location != model.url ||
                 sbTitle.value != model.title) {
               return;
             }
 
             let worker = windowNS(window).worker = Worker({
-              window: panelBrowser.contentWindow
+              window: panelBrowser.contentWindow,
+              injectInDocument: true
             });
 
             function onWebPanelSidebarUnload() {
               windowNS(window).onWebPanelSidebarUnload = null;
 
               // uncheck the associated menuitem
               bar.setAttribute('checked', 'false');
 
--- a/addon-sdk/source/lib/sdk/ui/toolbar.js
+++ b/addon-sdk/source/lib/sdk/ui/toolbar.js
@@ -5,12 +5,23 @@
 
 module.metadata = {
   "stability": "experimental",
   "engines": {
     "Firefox": "> 28"
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {});
+}
+catch (e) {
+  throw Error("Unsupported Application: The module"  + module.id +
+              " does not support this application.");
+}
+
 const { Toolbar } = require("./toolbar/model");
 require("./toolbar/view");
 
 exports.Toolbar = Toolbar;
--- a/addon-sdk/source/lib/sdk/window/utils.js
+++ b/addon-sdk/source/lib/sdk/window/utils.js
@@ -4,17 +4,16 @@
 'use strict';
 
 module.metadata = {
   'stability': 'unstable'
 };
 
 const { Cc, Ci } = require('chrome');
 const array = require('../util/array');
-const observers = require('../deprecated/observer-service');
 const { defer } = require('sdk/core/promise');
 
 const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1'].
                        getService(Ci.nsIWindowWatcher);
 const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
                         getService(Ci.nsIAppShellService);
 const WM = Cc['@mozilla.org/appshell/window-mediator;1'].
            getService(Ci.nsIWindowMediator);
@@ -147,35 +146,16 @@ function getWindowLoadingContext(window)
          QueryInterface(Ci.nsILoadContext);
 }
 exports.getWindowLoadingContext = getWindowLoadingContext;
 
 const isTopLevel = window => window && getToplevelWindow(window) === window;
 exports.isTopLevel = isTopLevel;
 
 /**
- * Removes given window from the application's window registry. Unless
- * `options.close` is `false` window is automatically closed on application
- * quit.
- * @params {nsIDOMWindow} window
- * @params {Boolean} options.close
- */
-function backgroundify(window, options) {
-  let base = getBaseWindow(window);
-  base.visibility = false;
-  base.enabled = false;
-  appShellService.unregisterTopLevelWindow(getXULWindow(window));
-  if (!options || options.close !== false)
-    observers.add('quit-application-granted', window.close.bind(window));
-
-  return window;
-}
-exports.backgroundify = backgroundify;
-
-/**
  * Takes hash of options and serializes it to a features string that
  * can be used passed to `window.open`. For more details on features string see:
  * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features
  */
 function serializeFeatures(options) {
   return Object.keys(options).reduce(function(result, name) {
     let value = options[name];
 
--- a/addon-sdk/source/lib/sdk/worker/utils.js
+++ b/addon-sdk/source/lib/sdk/worker/utils.js
@@ -1,103 +1,19 @@
 /* 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/. */
 
-"use strict";
+'use strict';
 
 module.metadata = {
-  "stability": "unstable"
+  'stability': 'deprecated'
 };
 
-// This module attempts to hide trait based nature of the worker so that
-// code depending on workers could be de-trait-ified without changing worker
-// implementation.
-
-const { Worker: WorkerTrait } = require("../content/worker");
-const { Loader } = require("../content/loader");
-const { merge } = require("../util/object");
-const { emit } = require("../event/core");
-
-let assetsURI = require("../self").data.url();
-let isArray = Array.isArray;
-
-function isAddonContent({ contentURL }) {
-  return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
-}
-
-function hasContentScript({ contentScript, contentScriptFile }) {
-  return (isArray(contentScript) ? contentScript.length > 0 :
-         !!contentScript) ||
-         (isArray(contentScriptFile) ? contentScriptFile.length > 0 :
-         !!contentScriptFile);
-}
-
-function requiresAddonGlobal(model) {
-  return isAddonContent(model) && !hasContentScript(model);
-}
-exports.requiresAddonGlobal = requiresAddonGlobal;
-
-
-const LegacyWorker = WorkerTrait.compose(Loader).resolve({
-  _setListeners: "__setListeners",
-  _injectInDocument: "__injectInDocument",
-  contentURL: "__contentURL"
-}).compose({
-  _setListeners: function() {},
-  get contentURL() this._window.document.URL,
-  get _injectInDocument() requiresAddonGlobal(this),
-  attach: function(window) this._attach(window),
-  detach: function() this._workerCleanup()
-});
-
-// Weak map that stores mapping between regular worker instances and
-// legacy trait based worker instances.
-let traits = new WeakMap();
-
-function traitFor(worker) traits.get(worker, null);
+const {
+  requiresAddonGlobal, attach, detach, destroy, WorkerHost
+} = require('../content/utils');
 
-function WorkerHost(workerFor) {
-  // Define worker properties that just proxy to a wrapped trait.
-  return ["postMessage", "port", "url", "tab"].reduce(function(proto, name) {
-    Object.defineProperty(proto, name, {
-      enumerable: true,
-      configurable: false,
-      get: function() traitFor(workerFor(this))[name],
-      set: function(value) traitFor(workerFor(this))[name] = value
-    });
-    return proto;
-  }, {});
-}
 exports.WorkerHost = WorkerHost;
-
-// Type representing worker instance.
-function Worker(options) {
-  let worker = Object.create(Worker.prototype);
-  let trait = new LegacyWorker(options);
-  ["pageshow", "pagehide", "detach", "message", "error"].forEach(function(key) {
-    trait.on(key, function() {
-      emit.apply(emit, [worker, key].concat(Array.slice(arguments)));
-    });
-  });
-  traits.set(worker, trait);
-  return worker;
-}
-exports.Worker = Worker;
-
-function detach(worker) {
-  let trait = traitFor(worker);
-  if (trait) trait.detach();
-}
 exports.detach = detach;
-
-function attach(worker, window) {
-  let trait = traitFor(worker);
-  // Cleanup the worker before injecting the content script into a new document.
-  trait.attach(window);
-}
 exports.attach = attach;
-
-function destroy(worker) {
-  let trait = traitFor(worker);
-  if (trait) trait.destroy();
-}
 exports.destroy = destroy;
+exports.requiresAddonGlobal = requiresAddonGlobal;
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -608,18 +608,30 @@ const Require = iced(function Require(lo
           throw err;
         uri = uri + '.js';
       }
     }
     // If not yet cached, load and cache it.
     // We also freeze module to prevent it from further changes
     // at runtime.
     if (!(uri in modules)) {
+      // Many of the loader's functionalities are dependent
+      // on modules[uri] being set before loading, so we set it and 
+      // remove it if we have any errors.
       module = modules[uri] = Module(requirement, uri);
-      freeze(load(loader, module));
+      try {
+        freeze(load(loader, module));
+      }
+      catch (e) {
+        // Clear out modules cache so we can throw on a second invalid require
+        delete modules[uri];
+        // Also clear out the Sandbox that was created
+        delete loader.sandboxes[uri];
+        throw e;
+      }
     }
 
     return module.exports;
   }
   // Make `require.main === module` evaluate to true in main module scope.
   require.main = loader.main === requirer ? requirer : undefined;
   return iced(require);
 });
--- a/addon-sdk/source/mapping.json
+++ b/addon-sdk/source/mapping.json
@@ -10,17 +10,16 @@
   "l10n/html": "sdk/l10n/html",
   "l10n/loader": "sdk/l10n/loader",
   "l10n/locale": "sdk/l10n/locale",
   "l10n/prefs": "sdk/l10n/prefs",
   "list": "sdk/util/list",
   "loader": "sdk/loader/loader",
   "memory": "sdk/deprecated/memory",
   "namespace": "sdk/core/namespace",
-  "observer-service": "sdk/deprecated/observer-service",
   "preferences-service": "sdk/preferences/service",
   "promise": "sdk/core/promise",
   "system": "sdk/system",
   "system/events": "sdk/system/events",
   "tabs/tab": "sdk/tabs/tab",
   "tabs/utils": "sdk/tabs/utils",
   "timer": "sdk/timers",
   "traits": "sdk/deprecated/traits",
--- a/addon-sdk/source/test/addons/layout-change/main.js
+++ b/addon-sdk/source/test/addons/layout-change/main.js
@@ -91,19 +91,16 @@ exports["test compatibility"] = function
                require("sdk/page-worker"), "sdk/page-worker -> page-worker");
 
   assert.equal(require("timer"),
                require("sdk/timers"), "sdk/timers -> timer");
 
   assert.equal(require("xhr"),
                require("sdk/net/xhr"), "sdk/io/xhr -> xhr");
 
-  assert.equal(require("observer-service"),
-               require("sdk/deprecated/observer-service"), "sdk/deprecated/observer-service -> observer-service");
-
   assert.equal(require("private-browsing"),
                require("sdk/private-browsing"), "sdk/private-browsing -> private-browsing");
 
   assert.equal(require("passwords"),
                require("sdk/passwords"), "sdk/passwords -> passwords");
 
   assert.equal(require("events"),
                require("sdk/deprecated/events"), "sdk/deprecated/events -> events");
@@ -142,19 +139,16 @@ exports["test compatibility"] = function
                require("sdk/querystring"), "sdk/querystring -> querystring");
 
   assert.equal(loader.require("addon-page"),
                loader.require("sdk/addon-page"), "sdk/addon-page -> addon-page");
 
   assert.equal(require("tabs/utils"),
                require("sdk/tabs/utils"), "sdk/tabs/utils -> tabs/utils");
 
-  assert.equal(require("app-strings"),
-               require("sdk/deprecated/app-strings"), "sdk/deprecated/app-strings -> app-strings");
-
   assert.equal(require("dom/events"),
                require("sdk/dom/events"), "sdk/dom/events -> dom/events");
 
   assert.equal(require("tabs/tab.js"),
                require("sdk/tabs/tab"), "sdk/tabs/tab -> tabs/tab.js");
 
   assert.equal(require("memory"),
                require("sdk/deprecated/memory"), "sdk/deprecated/memory -> memory");
--- a/addon-sdk/source/test/addons/symbiont/main.js
+++ b/addon-sdk/source/test/addons/symbiont/main.js
@@ -1,16 +1,16 @@
 /* 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/. */
 
 "use strict";
 
 const { data } = require("sdk/self");
-const { Symbiont } = require("sdk/content/symbiont");
+const { Symbiont } = require("sdk/deprecated/symbiont");
 
 exports["test:direct communication with trusted document"] = function(assert, done) {
   let worker = Symbiont({
     contentURL: data.url("test-trusted-document.html")
   });
 
   worker.port.on('document-to-addon', function (arg) {
     assert.equal(arg, "ok", "Received an event from the document");
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/missing-twice/file.json
@@ -0,0 +1,1 @@
+an invalid json file
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/missing-twice/main.js
@@ -0,0 +1,32 @@
+/* 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/. */
+
+'use strict';
+
+try {
+  require('./not-found');
+}
+catch (e1) {
+  exports.firstError = e1;
+  // It should throw again and not be cached
+  try {
+    require('./not-found');
+  }
+  catch (e2) {
+    exports.secondError = e2;
+  }
+}
+
+try {
+  require('./file.json');
+}
+catch (e) {
+  exports.invalidJSON1 = e;
+  try {
+    require('./file.json');
+  }
+  catch (e) {
+    exports.invalidJSON2 = e;
+  }
+}
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -4,33 +4,36 @@
 'use strict';
 
 const { Cc, Ci } = require('chrome');
 const { Loader } = require('sdk/test/loader');
 const timer = require('sdk/timers');
 const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
 const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { open, focus, close } = require('sdk/window/helpers');
-const { StringBundle } = require('sdk/deprecated/app-strings');
 const tabs = require('sdk/tabs');
 const { browserWindows } = require('sdk/windows');
 const { set: setPref } = require("sdk/preferences/service");
 const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";
 
 const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAA";
 
 // Bug 682681 - tab.title should never be empty
 exports.testBug682681_aboutURI = function(assert, done) {
-  let tabStrings = StringBundle('chrome://browser/locale/tabbrowser.properties');
+  let url = 'chrome://browser/locale/tabbrowser.properties';
+  let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
+                        getService(Ci.nsIStringBundleService).
+                        createBundle(url);
+  let emptyTabTitle = stringBundle.GetStringFromName('tabs.emptyTabTitle');
 
   tabs.on('ready', function onReady(tab) {
     tabs.removeListener('ready', onReady);
 
     assert.equal(tab.title,
-                     tabStrings.get('tabs.emptyTabTitle'),
+                     emptyTabTitle,
                      "title of about: tab is not blank");
 
     tab.close(done);
   });
 
   // open a about: url
   tabs.open({
     url: "about:blank",
--- a/addon-sdk/source/test/test-addon-installer.js
+++ b/addon-sdk/source/test/test-addon-installer.js
@@ -1,56 +1,55 @@
 /* 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/. */
-
 "use strict";
 
 const { Cc, Ci, Cu } = require("chrome");
 const AddonInstaller = require("sdk/addon/installer");
-const observers = require("sdk/deprecated/observer-service");
+const { on, off } = require("sdk/system/events");
 const { setTimeout } = require("sdk/timers");
 const tmp = require("sdk/test/tmp-file");
 const system = require("sdk/system");
 const fixtures = require("./fixtures");
 
 const testFolderURL = module.uri.split('test-addon-installer.js')[0];
 const ADDON_URL = testFolderURL + "fixtures/addon-install-unit-test@mozilla.com.xpi";
 const ADDON_PATH = tmp.createFromURL(ADDON_URL);
 
 exports["test Install"] = function (assert, done) {
 
   // Save all events distpatched by bootstrap.js of the installed addon
   let events = [];
-  function eventsObserver(subject, data) {
+  function eventsObserver({ data }) {
     events.push(data);
   }
-  observers.add("addon-install-unit-test", eventsObserver, false);
+  on("addon-install-unit-test", eventsObserver);
 
   // Install the test addon
   AddonInstaller.install(ADDON_PATH).then(
     function onInstalled(id) {
       assert.equal(id, "addon-install-unit-test@mozilla.com", "`id` is valid");
 
       // Now uninstall it
       AddonInstaller.uninstall(id).then(function () {
         // Ensure that bootstrap.js methods of the addon have been called
         // successfully and in the right order
         let expectedEvents = ["install", "startup", "shutdown", "uninstall"];
         assert.equal(JSON.stringify(events),
                          JSON.stringify(expectedEvents),
                          "addon's bootstrap.js functions have been called");
 
-        observers.remove("addon-install-unit-test", eventsObserver);
+        off("addon-install-unit-test", eventsObserver);
         done();
       });
     },
     function onFailure(code) {
       assert.fail("Install failed: "+code);
-      observers.remove("addon-install-unit-test", eventsObserver);
+      off("addon-install-unit-test", eventsObserver);
       done();
     }
   );
 };
 
 exports["test Failing Install With Invalid Path"] = function (assert, done) {
   AddonInstaller.install("invalid-path").then(
     function onInstalled(id) {
@@ -79,20 +78,18 @@ exports["test Failing Install With Inval
     }
   );
 }
 
 exports["test Update"] = function (assert, done) {
   // Save all events distpatched by bootstrap.js of the installed addon
   let events = [];
   let iteration = 1;
-  function eventsObserver(subject, data) {
-    events.push(data);
-  }
-  observers.add("addon-install-unit-test", eventsObserver);
+  let eventsObserver = ({data}) => events.push(data);
+  on("addon-install-unit-test", eventsObserver);
 
   function onInstalled(id) {
     let prefix = "[" + iteration + "] ";
     assert.equal(id, "addon-install-unit-test@mozilla.com",
                      prefix + "`id` is valid");
 
     // On 2nd and 3rd iteration, we receive uninstall events from the last
     // previously installed addon
@@ -110,24 +107,24 @@ exports["test Update"] = function (asser
     else {
       events = [];
       AddonInstaller.uninstall(id).then(function() {
         let expectedEvents = ["shutdown", "uninstall"];
         assert.equal(JSON.stringify(events),
                      JSON.stringify(expectedEvents),
                      prefix + "addon's bootstrap.js functions have been called");
 
-        observers.remove("addon-install-unit-test", eventsObserver);
+        off("addon-install-unit-test", eventsObserver);
         done();
       });
     }
   }
   function onFailure(code) {
     assert.fail("Install failed: "+code);
-    observers.remove("addon-install-unit-test", eventsObserver);
+    off("addon-install-unit-test", eventsObserver);
     done();
   }
 
   function next() {
     events = [];
     AddonInstaller.install(ADDON_PATH).then(onInstalled, onFailure);
   }
 
--- a/addon-sdk/source/test/test-api-utils.js
+++ b/addon-sdk/source/test/test-api-utils.js
@@ -1,39 +1,14 @@
 /* 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/. */
 
 const apiUtils = require("sdk/deprecated/api-utils");
 
-exports.testPublicConstructor = function (assert) {
-  function PrivateCtor() {}
-  PrivateCtor.prototype = {};
-
-  let PublicCtor = apiUtils.publicConstructor(PrivateCtor);
-  assert.ok(
-    PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype),
-    "PrivateCtor.prototype should be prototype of PublicCtor.prototype"
-  );
-
-  function testObj(useNew) {
-    let obj = useNew ? new PublicCtor() : PublicCtor();
-    assert.ok(obj instanceof PublicCtor,
-                "Object should be instance of PublicCtor");
-    assert.ok(obj instanceof PrivateCtor,
-                "Object should be instance of PrivateCtor");
-    assert.ok(PublicCtor.prototype.isPrototypeOf(obj),
-                "PublicCtor's prototype should be prototype of object");
-    assert.equal(obj.constructor, PublicCtor,
-                     "Object constructor should be PublicCtor");
-  }
-  testObj(true);
-  testObj(false);
-};
-
 exports.testValidateOptionsEmpty = function (assert) {
   let val = apiUtils.validateOptions(null, {});
 
   assert.deepEqual(val, {});
 
   val = apiUtils.validateOptions(null, { foo: {} });
   assert.deepEqual(val, {});
 
deleted file mode 100644
--- a/addon-sdk/source/test/test-app-strings.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/* 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/. */
-"use strict";
-
-const { Cc, Ci } = require("chrome");
-const { StringBundle } = require("sdk/deprecated/app-strings");
-
-exports.testStringBundle = function(assert) {
-  let url = "chrome://global/locale/security/caps.properties";
-
-  let strings = StringBundle(url);
-
-  assert.equal(strings.url, url,
-                   "'url' property contains correct URL of string bundle");
-
-  let appLocale = Cc["@mozilla.org/intl/nslocaleservice;1"].
-                  getService(Ci.nsILocaleService).
-                  getApplicationLocale();
-
-  let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
-                     getService(Ci.nsIStringBundleService).
-                     createBundle(url, appLocale);
-
-  let (name = "CheckMessage") {
-    assert.equal(strings.get(name), stringBundle.GetStringFromName(name),
-                 "getting a string returns the string");
-  }
-
-  let (name = "CreateWrapperDenied", args = ["foo"]) {
-    assert.equal(strings.get(name, args),
-                 stringBundle.formatStringFromName(name, args, args.length),
-                 "getting a formatted string returns the formatted string");
-  }
-
-  assert.throws(function () strings.get("nonexistentString"),
-                RegExp("String 'nonexistentString' could not be retrieved from " +
-                       "the bundle due to an unknown error \\(it doesn't exist\\?\\)\\."),
-                "retrieving a nonexistent string throws an exception");
-
-  let a = [], b = [];
-  let enumerator = stringBundle.getSimpleEnumeration();
-  while (enumerator.hasMoreElements()) {
-    let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
-    a.push([elem.key, elem.value]);
-  }
-
-  for (let key in strings) {
-    b.push([ key, strings.get(key) ]);
-  }
-
-  // Sort the arrays, because we don't assume enumeration has a set order.
-  // Sort compares [key, val] as string "key,val", which sorts the way we want
-  // it to, so there is no need to provide a custom compare function.
-  a.sort();
-  b.sort();
-
-  assert.equal(a.length, b.length,
-               "the iterator returns the correct number of items");
-
-  for (let i = 0; i < a.length; i++) {
-    assert.equal(a[i][0], b[i][0], "the iterated string's name is correct");
-    assert.equal(a[i][1], b[i][1],
-                     "the iterated string's value is correct");
-  }
-};
-
-require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-content-symbiont.js
+++ b/addon-sdk/source/test/test-content-symbiont.js
@@ -1,15 +1,15 @@
 /* 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/. */
 "use strict";
 
 const { Cc, Ci } = require('chrome');
-const { Symbiont } = require('sdk/content/symbiont');
+const { Symbiont } = require('sdk/deprecated/symbiont');
 const self = require('sdk/self');
 const fixtures = require("./fixtures");
 const { close } = require('sdk/window/helpers');
 const app = require("sdk/system/xul-app");
 
 function makeWindow() {
   let content =
     '<?xml version="1.0"?>' +
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -7,16 +7,17 @@
 // Skipping due to window creation being unsupported in Fennec
 module.metadata = {
   engines: {
     'Firefox': '*'
   }
 };
 
 const { Cc, Ci } = require("chrome");
+const { on } = require("sdk/event/core");
 const { setTimeout } = require("sdk/timers");
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 const { Worker } = require("sdk/content/worker");
 const { close } = require("sdk/window/helpers");
 const { set: setPref } = require("sdk/preferences/service");
 const { isArray } = require("sdk/lang/type");
 const { URL } = require('sdk/url');
 const fixtures = require("./fixtures");
@@ -111,16 +112,18 @@ exports["test:sample"] = WorkerTest(
         assert.equal(worker.url, window.location.href,
                          "worker.url still works");
         done();
       }
     });
 
     assert.equal(worker.url, window.location.href,
                      "worker.url works");
+    assert.equal(worker.contentURL, window.location.href,
+                     "worker.contentURL works");
     worker.postMessage("hi!");
   }
 );
 
 exports["test:emit"] = WorkerTest(
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
 
@@ -221,17 +224,17 @@ exports["test:post-json-values-only"] = 
 
     let worker =  Worker({
         window: browser.contentWindow,
         contentScript: "new " + function WorkerScope() {
           self.on("message", function (message) {
             self.postMessage([ message.fun === undefined,
                                typeof message.w,
                                message.w && "port" in message.w,
-                               message.w.url,
+                               message.w._url,
                                Array.isArray(message.array),
                                JSON.stringify(message.array)]);
           });
         }
       });
 
     // Validate worker.onMessage
     let array = [1, 2, 3];
@@ -242,16 +245,20 @@ exports["test:post-json-values-only"] = 
       assert.equal(message[3], DEFAULT_CONTENT_URL,
                        "jsonable attributes are accessible");
       // See bug 714891, Arrays may be broken over compartements:
       assert.ok(message[4], "Array keeps being an array");
       assert.equal(message[5], JSON.stringify(array),
                        "Array is correctly serialized");
       done();
     });
+    // Add a new url property sa the Class function used by 
+    // Worker doesn't set enumerables to true for non-functions
+    worker._url = DEFAULT_CONTENT_URL;
+
     worker.postMessage({ fun: function () {}, w: worker, array: array });
   }
 );
 
 exports["test:emit-json-values-only"] = WorkerTest(
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
 
@@ -259,17 +266,17 @@ exports["test:emit-json-values-only"] = 
         window: browser.contentWindow,
         contentScript: "new " + function WorkerScope() {
           // Validate self.on and self.emit
           self.port.on("addon-to-content", function (fun, w, obj, array) {
             self.port.emit("content-to-addon", [
                             fun === null,
                             typeof w,
                             "port" in w,
-                            w.url,
+                            w._url,
                             "fun" in obj,
                             Object.keys(obj.dom).length,
                             Array.isArray(array),
                             JSON.stringify(array)
                           ]);
           });
         }
       });
@@ -290,16 +297,19 @@ exports["test:emit-json-values-only"] = 
                        "Array is correctly serialized");
       done();
     });
 
     let obj = {
       fun: function () {},
       dom: browser.contentWindow.document.createElement("div")
     };
+    // Add a new url property sa the Class function used by 
+    // Worker doesn't set enumerables to true for non-functions
+    worker._url = DEFAULT_CONTENT_URL;
     worker.port.emit("addon-to-content", function () {}, worker, obj, array);
   }
 );
 
 exports["test:content is wrapped"] = WorkerTest(
   "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
   function(assert, browser, done) {
 
@@ -824,9 +834,42 @@ exports['test:conentScriptFile as URL in
         assert.equal(msg, "msg from contentScriptFile", 
             "received a wrong message from contentScriptFile");
         done();
       }
     });
   }
 );
 
+exports.testWorkerEvents = WorkerTest(DEFAULT_CONTENT_URL, function (assert, browser, done) {
+  let window = browser.contentWindow;
+  let events = [];
+  let worker = Worker({
+    window: window,
+    contentScript: 'new ' + function WorkerScope() {
+      self.postMessage('start');
+    },
+    onAttach: win => {
+      events.push('attach');
+      assert.pass('attach event called when attached');
+      assert.equal(window, win, 'attach event passes in attached window');
+    },
+    onError: err => {
+      assert.equal(err.message, 'Custom',
+        'Error passed into error event');
+      worker.detach();
+    },
+    onMessage: msg => {
+      assert.pass('`onMessage` handles postMessage')
+      throw new Error('Custom');
+    },
+    onDetach: _ => {
+      assert.pass('`onDetach` called when worker detached');
+      done();
+    }
+  });
+  // `attach` event is called synchronously during instantiation,
+  // so we can't listen to that, TODO FIX?
+  //  worker.on('attach', obj => console.log('attach', obj));
+});
+
+
 require("test").run(exports);
--- a/addon-sdk/source/test/test-event-core.js
+++ b/addon-sdk/source/test/test-event-core.js
@@ -1,15 +1,15 @@
 /* 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/. */
 
 'use strict';
 
-const { on, once, off, emit, count, amass } = require('sdk/event/core');
+const { on, once, off, emit, count } = require('sdk/event/core');
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 
 exports['test add a listener'] = function(assert) {
   let events = [ { name: 'event#1' }, 'event#2' ];
   let target = { name: 'target' };
 
   on(target, 'message', function(message) {
     assert.equal(this, target, 'this is a target object');
--- a/addon-sdk/source/test/test-event-utils.js
+++ b/addon-sdk/source/test/test-event-utils.js
@@ -1,16 +1,16 @@
 /* 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/. */
 
 'use strict';
 
 const { on, emit } = require("sdk/event/core");
-const { filter, map, merge, expand, pipe } = require("sdk/event/utils");
+const { filter, map, merge, expand, pipe, stripListeners } = require("sdk/event/utils");
 const $ = require("./event/helpers");
 
 function isEven(x) !(x % 2)
 function inc(x) x + 1
 
 exports["test filter events"] = function(assert) {
   let input = {};
   let evens = filter(input, isEven);
@@ -163,55 +163,55 @@ exports["test expand"] = function(assert
 
   assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"],
                    "all inputs data merged into one");
 };
 
 exports["test pipe"] = function (assert, done) {
   let src = {};
   let dest = {};
-  
+
   let aneventCount = 0, multiargsCount = 0;
   let wildcardCount = {};
 
   on(dest, 'an-event', arg => {
     assert.equal(arg, 'my-arg', 'piped argument to event');
     ++aneventCount;
     check();
   });
   on(dest, 'multiargs', (...data) => {
     assert.equal(data[0], 'a', 'multiple arguments passed via pipe');
     assert.equal(data[1], 'b', 'multiple arguments passed via pipe');
     assert.equal(data[2], 'c', 'multiple arguments passed via pipe');
     ++multiargsCount;
     check();
   });
-  
+
   on(dest, '*', (name, ...data) => {
     wildcardCount[name] = (wildcardCount[name] || 0) + 1;
     if (name === 'multiargs') {
       assert.equal(data[0], 'a', 'multiple arguments passed via pipe, wildcard');
       assert.equal(data[1], 'b', 'multiple arguments passed via pipe, wildcard');
       assert.equal(data[2], 'c', 'multiple arguments passed via pipe, wildcard');
     }
     if (name === 'an-event')
       assert.equal(data[0], 'my-arg', 'argument passed via pipe, wildcard');
     check();
   });
 
   pipe(src, dest);
 
   for (let i = 0; i < 3; i++)
     emit(src, 'an-event', 'my-arg');
-  
+
   emit(src, 'multiargs', 'a', 'b', 'c');
 
   function check () {
     if (aneventCount === 3 && multiargsCount === 1 &&
-        wildcardCount['an-event'] === 3 && 
+        wildcardCount['an-event'] === 3 &&
         wildcardCount['multiargs'] === 1)
       done();
   }
 };
 
 exports["test pipe multiple targets"] = function (assert) {
   let src1 = {};
   let src2 = {};
@@ -232,27 +232,51 @@ exports["test pipe multiple targets"] = 
   on(middle, '*', () => middleFired++);
   on(dest, '*', () => destFired++);
 
   emit(src1, 'ev');
   assert.equal(src1Fired, 1, 'event triggers in source in pipe chain');
   assert.equal(middleFired, 1, 'event passes through the middle of pipe chain');
   assert.equal(destFired, 1, 'event propagates to end of pipe chain');
   assert.equal(src2Fired, 0, 'event does not fire on alternative chain routes');
-  
+
   emit(src2, 'ev');
   assert.equal(src2Fired, 1, 'event triggers in source in pipe chain');
   assert.equal(middleFired, 2,
     'event passes through the middle of pipe chain from different src');
   assert.equal(destFired, 2,
     'event propagates to end of pipe chain from different src');
   assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes');
-  
+
   emit(middle, 'ev');
   assert.equal(middleFired, 3,
     'event triggers in source of pipe chain');
   assert.equal(destFired, 3,
     'event propagates to end of pipe chain from middle src');
   assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes');
   assert.equal(src2Fired, 1, 'event does not fire on alternative chain routes');
 };
 
+exports['test stripListeners'] = function (assert) {
+  var options = {
+    onAnEvent: noop1,
+    onMessage: noop2,
+    verb: noop1,
+    value: 100
+  };
+
+  var stripped = stripListeners(options);
+  assert.ok(stripped !== options, 'stripListeners should return a new object');
+  assert.equal(options.onAnEvent, noop1, 'stripListeners does not affect original');
+  assert.equal(options.onMessage, noop2, 'stripListeners does not affect original');
+  assert.equal(options.verb, noop1, 'stripListeners does not affect original');
+  assert.equal(options.value, 100, 'stripListeners does not affect original');
+
+  assert.ok(!stripped.onAnEvent, 'stripListeners removes `on*` values');
+  assert.ok(!stripped.onMessage, 'stripListeners removes `on*` values');
+  assert.equal(stripped.verb, noop1, 'stripListeners leaves not `on*` values');
+  assert.equal(stripped.value, 100, 'stripListeners leaves not `on*` values');
+
+  function noop1 () {}
+  function noop2 () {}
+};
+
 require('test').run(exports);
--- a/addon-sdk/source/test/test-loader.js
+++ b/addon-sdk/source/test/test-loader.js
@@ -96,16 +96,23 @@ exports['test syntax errors'] = function
     assert.equal(stack.pop().fileName, module.uri,
                  "previous to it is a test module");
 
   } finally {
     unload(loader);
   }
 }
 
+exports['test sandboxes are not added if error'] = function (assert) {
+  let uri = root + '/fixtures/loader/missing-twice/';
+  let loader = Loader({ paths: { '': uri } });
+  let program = main(loader, 'main');
+  assert.ok(!(uri + 'not-found.js' in loader.sandboxes), 'not-found.js not in loader.sandboxes');
+}
+
 exports['test missing module'] = function(assert) {
   let uri = root + '/fixtures/loader/missing/'
   let loader = Loader({ paths: { '': uri } });
 
   try {
     let program = main(loader, 'main')
   } catch (error) {
     assert.equal(error.message, "Module `not-found` is not found at " +
@@ -123,16 +130,36 @@ exports['test missing module'] = functio
 
     assert.equal(stack.pop().fileName, module.uri,
                  "previous in the stack is test module");
   } finally {
     unload(loader);
   }
 }
 
+exports["test invalid module not cached and throws everytime"] = function(assert) {
+  let uri = root + "/fixtures/loader/missing-twice/";
+  let loader = Loader({ paths: { "": uri } });
+
+  let { firstError, secondError, invalidJSON1, invalidJSON2 } = main(loader, "main");
+  assert.equal(firstError.message, "Module `not-found` is not found at " +
+    uri + "not-found.js", "throws on first invalid require");
+  assert.equal(firstError.lineNumber, 8, "first error is on line 7");
+  assert.equal(secondError.message, "Module `not-found` is not found at " +
+    uri + "not-found.js", "throws on second invalid require");
+  assert.equal(secondError.lineNumber, 14, "second error is on line 14");
+
+  assert.equal(invalidJSON1.message,
+    "JSON.parse: unexpected character at line 1 column 1 of the JSON data",
+    "throws on invalid JSON");
+  assert.equal(invalidJSON2.message,
+    "JSON.parse: unexpected character at line 1 column 1 of the JSON data",
+    "throws on invalid JSON second time");
+};
+
 exports['test exceptions in modules'] = function(assert) {
   let uri = root + '/fixtures/loader/exceptions/'
 
   let loader = Loader({ paths: { '': uri } });
 
   try {
     let program = main(loader, 'main')
   } catch (error) {
--- a/addon-sdk/source/test/test-memory.js
+++ b/addon-sdk/source/test/test-memory.js
@@ -1,21 +1,22 @@
 /* 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/. */
+"use strict";
 
-var memory = require("sdk/deprecated/memory");
+const memory = require("sdk/deprecated/memory");
+const { gc } = require("sdk/test/memory");
 
 exports.testMemory = function(assert) {
-  assert.pass("Skipping this test until Gecko memory debugging issues " +
-            "are resolved (see bug 592774).");
-  return;
-
   var obj = {};
   memory.track(obj, "testMemory.testObj");
+
   var objs = memory.getObjects("testMemory.testObj");
   assert.equal(objs[0].weakref.get(), obj);
   obj = null;
-  memory.gc();
-  assert.equal(objs[0].weakref.get(), null);
+
+  gc().then(function() {
+    assert.equal(objs[0].weakref.get(), null);
+  });
 };
 
 require('sdk/test').run(exports);
deleted file mode 100644
--- a/addon-sdk/source/test/test-observer-service.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/* 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/. */
-
-const observers = require("sdk/deprecated/observer-service");
-const { Cc, Ci } = require("chrome");
-const { LoaderWithHookedConsole2 } = require("sdk/test/loader");
-
-exports.testUnloadAndErrorLogging = function(assert) {
-  let { loader, messages } = LoaderWithHookedConsole2(module);
-  var sbobsvc = loader.require("sdk/deprecated/observer-service");
-
-  var timesCalled = 0;
-  var cb = function(subject, data) {
-    timesCalled++;
-  };
-  var badCb = function(subject, data) {
-    throw new Error("foo");
-  };
-  sbobsvc.add("blarg", cb);
-  observers.notify("blarg", "yo yo");
-  assert.equal(timesCalled, 1);
-  sbobsvc.add("narg", badCb);
-  observers.notify("narg", "yo yo");
-
-  assert.equal(messages[0], "console.error: " + require("sdk/self").name + ": \n");
-  var lines = messages[1].split("\n");
-  assert.equal(lines[0], "  Message: Error: foo");
-  assert.equal(lines[1], "  Stack:");
-  // Keep in mind to update "18" to the line of "throw new Error("foo")"
-  assert.ok(lines[2].indexOf(module.uri + ":18") != -1);
-
-  loader.unload();
-  observers.notify("blarg", "yo yo");
-  assert.equal(timesCalled, 1);
-};
-
-exports.testObserverService = function(assert) {
-  var ios = Cc['@mozilla.org/network/io-service;1']
-            .getService(Ci.nsIIOService);
-  var service = Cc["@mozilla.org/observer-service;1"].
-                getService(Ci.nsIObserverService);
-  var uri = ios.newURI("http://www.foo.com", null, null);
-  var timesCalled = 0;
-  var lastSubject = null;
-  var lastData = null;
-
-  var cb = function(subject, data) {
-    timesCalled++;
-    lastSubject = subject;
-    lastData = data;
-  };
-
-  observers.add("blarg", cb);
-  service.notifyObservers(uri, "blarg", "some data");
-  assert.equal(timesCalled, 1,
-                   "observer-service.add() should call callback");
-  assert.equal(lastSubject, uri,
-                   "observer-service.add() should pass subject");
-  assert.equal(lastData, "some data",
-                   "observer-service.add() should pass data");
-
-  function customSubject() {}
-  function customData() {}
-  observers.notify("blarg", customSubject, customData);
-  assert.equal(timesCalled, 2,
-                   "observer-service.notify() should work");
-  assert.equal(lastSubject, customSubject,
-                   "observer-service.notify() should pass+wrap subject");
-  assert.equal(lastData, customData,
-                   "observer-service.notify() should pass data");
-
-  observers.remove("blarg", cb);
-  service.notifyObservers(null, "blarg", "some data");
-  assert.equal(timesCalled, 2,
-                   "observer-service.remove() should work");
-};
-
-require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-simple-prefs.js
+++ b/addon-sdk/source/test/test-simple-prefs.js
@@ -1,16 +1,16 @@
 /* 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/. */
 "use strict";
 
 const { Loader } = require("sdk/test/loader");
 const { setTimeout } = require("sdk/timers");
-const { notify } = require("sdk/deprecated/observer-service");
+const { emit } = require("sdk/system/events");
 const { id } = require("sdk/self");
 const simplePrefs = require("sdk/simple-prefs");
 const { prefs: sp } = simplePrefs;
 
 const specialChars = "!@#$%^&*()_-=+[]{}~`\'\"<>,./?;:";
 
 exports.testIterations = function(assert) {
   sp["test"] = true;
@@ -127,17 +127,17 @@ exports.testPrefListener = function(asse
 
 exports.testBtnListener = function(assert, done) {
   let name = "test-btn-listen";
   simplePrefs.on(name, function listener() {
     simplePrefs.removeListener(name, listener);
     assert.pass("Button press event was heard");
     done();
   });
-  notify((id + "-cmdPressed"), "", name);
+  emit((id + "-cmdPressed"), { subject: "", data: name });
 };
 
 exports.testPrefRemoveListener = function(assert, done) {
   let counter = 0;
 
   let listener = function() {
     assert.pass("The prefs listener was not removed yet");
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-test-memory.js
@@ -0,0 +1,23 @@
+/* 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/. */
+'use strict';
+
+const { Cc, Ci, Cu, components } = require('chrome');
+const { gc } = require('sdk/test/memory');
+
+exports.testGC = function(assert, done) {
+  let weakref;
+  let (tempObj = {}) {
+    weakref = Cu.getWeakReference(tempObj);
+    assert.equal(weakref.get(), tempObj, 'the weakref returned the tempObj');
+  }
+
+  gc().then(function(arg) {
+    assert.equal(arg, undefined, 'there is no argument');
+    assert.pass('gc() returns a promise which eventually resolves');
+    assert.equal(weakref.get(), undefined, 'the weakref returned undefined');
+  }).then(done).then(null, assert.fail);
+};
+
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-traceback.js
+++ b/addon-sdk/source/test/test-traceback.js
@@ -1,46 +1,47 @@
 /* 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/. */
+ "use strict";
 
 var traceback = require("sdk/console/traceback");
 var {Cc,Ci,Cr,Cu} = require("chrome");
+const { on, off } = require("sdk/system/events");
 
 function throwNsIException() {
   var ios = Cc['@mozilla.org/network/io-service;1']
             .getService(Ci.nsIIOService);
   ios.newURI("i'm a malformed URI", null, null);
 }
 
 function throwError() {
   throw new Error("foob");
 }
 
 exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
-  var observers = require("sdk/deprecated/observer-service");
   ["http", "https"].forEach(
     function(scheme) {
       var httpRequests = 0;
       function onHttp() {
         httpRequests++;
       }
 
-      observers.add("http-on-modify-request", onHttp);
+      on("http-on-modify-request", onHttp);
 
       try {
         var tb = [{filename: scheme + "://www.mozilla.org/",
                    lineNumber: 1,
                    name: "blah"}];
         traceback.format(tb);
       } catch (e) {
         assert.fail(e);
       }
 
-      observers.remove("http-on-modify-request", onHttp);
+      off("http-on-modify-request", onHttp);
 
       assert.equal(httpRequests, 0,
                        "traceback.format() does not make " +
                        scheme + " request");
     });
 };
 
 exports.testFromExceptionWithString = function(assert) {
--- a/addon-sdk/source/test/test-window-utils2.js
+++ b/addon-sdk/source/test/test-window-utils2.js
@@ -6,17 +6,17 @@
 // Opening new windows in Fennec causes issues
 module.metadata = {
   engines: {
     'Firefox': '*'
   }
 };
 
 const { Ci } = require('chrome');
-const { open, backgroundify, windows, isBrowser,
+const { open, windows, isBrowser,
         getXULWindow, getBaseWindow, getToplevelWindow, getMostRecentWindow,
         getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { close } = require('sdk/window/helpers');
 const windowUtils = require('sdk/deprecated/window-utils');
 
 exports['test get nsIBaseWindow from nsIDomWindow'] = function(assert) {
   let active = windowUtils.activeBrowserWindow;
 
@@ -73,50 +73,34 @@ exports['test new top window with variou
   assert.throws(function () {
     open('foo');
   }, msg);
   assert.throws(function () {
     open('http://foo');
   }, msg);
   assert.throws(function () {
     open('https://foo');
-  }, msg); 
+  }, msg);
   assert.throws(function () {
     open('ftp://foo');
   }, msg);
   assert.throws(function () {
     open('//foo');
   }, msg);
 
   let chromeWindow = open('chrome://foo/content/');
   assert.ok(~windows().indexOf(chromeWindow), 'chrome URI works');
-  
+
   let resourceWindow = open('resource://foo');
   assert.ok(~windows().indexOf(resourceWindow), 'resource URI works');
 
   // Wait for the window unload before ending test
   close(chromeWindow).then(close.bind(null, resourceWindow)).then(done);
 };
 
-exports.testBackgroundify = function(assert, done) {
-  let window = open('data:text/html;charset=utf-8,backgroundy');
-  assert.ok(~windows().indexOf(window),
-            'window is in the list of windows');
-  let backgroundy = backgroundify(window);
-  assert.equal(backgroundy, window, 'backgroundify returs give window back');
-  assert.ok(!~windows().indexOf(window),
-            'backgroundifyied window is in the list of windows');
-
-  // Wait for the window unload before ending test
-  // backgroundified windows doesn't dispatch domwindowclosed event
-  // so that we have to manually wait for unload event
-  window.onunload = done;
-  window.close();
-};
-
 exports.testIsBrowser = function(assert) {
   // dummy window, bad type
   assert.equal(isBrowser({ document: { documentElement: { getAttribute: function() {
     return 'navigator:browserx';
   }}}}), false, 'dummy object with correct stucture and bad type does not pass');
 
   assert.ok(isBrowser(getMostRecentBrowserWindow()), 'active browser window is a browser window');
   assert.ok(!isBrowser({}), 'non window is not a browser window');
--- a/addon-sdk/source/test/test-windows-common.js
+++ b/addon-sdk/source/test/test-windows-common.js
@@ -1,20 +1,16 @@
 /* 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/. */
 'use strict';
 
 const { Loader } = require('sdk/test/loader');
 const { browserWindows } = require('sdk/windows');
-const { viewFor } = require('sdk/view/core');
 const { Ci } = require("chrome");
-const { isBrowser, getWindowTitle } = require("sdk/window/utils");
-const { defer } = require("sdk/lang/functional");
-
 
 // TEST: browserWindows Iterator
 exports.testBrowserWindowsIterator = function(assert) {
   let activeWindowCount = 0;
   let windows = [];
   let i = 0;
   for each (let window in browserWindows) {
     if (window === browserWindows.activeWindow)
@@ -56,31 +52,9 @@ exports.testWindowActivateMethod_simple 
   window.activate();
 
   assert.equal(browserWindows.activeWindow, window,
                'Active window is active after window.activate() call');
   assert.equal(window.tabs.activeTab, tab,
                'Active tab is active after window.activate() call');
 };
 
-
-exports["test getView(window)"] = function(assert, done) {
-  browserWindows.once("open", window => {
-    const view = viewFor(window);
-
-    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
-    assert.ok(isBrowser(view), "view is a browser window");
-    assert.equal(getWindowTitle(view), window.title,
-                 "window has a right title");
-
-    window.close();
-    // Defer handler cause window is destroyed after event is dispatched.
-    browserWindows.once("close", defer(_ => {
-      assert.equal(viewFor(window), null, "window view is gone");
-      done();
-    }));
-  });
-
-
-  browserWindows.open({ url: "data:text/html,<title>yo</title>" });
-};
-
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/windows/test-firefox-windows.js
+++ b/addon-sdk/source/test/windows/test-firefox-windows.js
@@ -1,24 +1,26 @@
 /* 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/. */
 'use strict';
 
 const { Cc, Ci } = require('chrome');
 const { setTimeout } = require('sdk/timers');
 const { Loader } = require('sdk/test/loader');
-const { onFocus, getMostRecentWindow, windows } = require('sdk/window/utils');
+const { onFocus, getMostRecentWindow, windows, isBrowser, getWindowTitle } = require('sdk/window/utils');
 const { open, close, focus } = require('sdk/window/helpers');
 const { browserWindows } = require("sdk/windows");
 const tabs = require("sdk/tabs");
 const winUtils = require("sdk/deprecated/window-utils");
 const { WindowTracker } = winUtils;
 const { isPrivate } = require('sdk/private-browsing');
 const { isWindowPBSupported } = require('sdk/private-browsing/utils');
+const { viewFor } = require("sdk/view/core");
+const { defer } = require("sdk/lang/functional");
 
 // TEST: open & close window
 exports.testOpenAndCloseWindow = function(assert, done) {
   assert.equal(browserWindows.length, 1, "Only one window open");
   let title = 'testOpenAndCloseWindow';
 
   browserWindows.open({
     url: "data:text/html;charset=utf-8,<title>" + title + "</title>",
@@ -413,11 +415,31 @@ exports.testWindowIteratorPrivateDefault
     assert.equal(windows(null, { includePrivate: true }).length, 2);
 
     // test that all windows in iterator are not private
     for (let window of browserWindows)
       assert.ok(!isPrivate(window), 'no window in browserWindows is private');
 
     close(window).then(done);
   });
-}
+};
+
+exports["test getView(window)"] = function(assert, done) {
+  browserWindows.once("open", window => {
+    const view = viewFor(window);
+
+    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
+    assert.ok(isBrowser(view), "view is a browser window");
+    assert.equal(getWindowTitle(view), window.title,
+                 "window has a right title");
+
+    window.close();
+    // Defer handler cause window is destroyed after event is dispatched.
+    browserWindows.once("close", defer(_ => {
+      assert.equal(viewFor(window), null, "window view is gone");
+      done();
+    }));
+  });
+
+  browserWindows.open({ url: "data:text/html,<title>yo</title>" });
+};
 
 require('sdk/test').run(exports);
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fce1a137746dbd354bca1918f02f96d51c40bad2">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.1" path="prebuilts/clang/darwin-x86/3.1" revision="8a10d50e8caab8c18224588f0531f1c9363965b5"/>
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.2" path="prebuilts/clang/darwin-x86/3.2" revision="2d96fcbab6efee560c2004725b21bdc06d090933"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "18bd82325a82f5b9a3a4b976e213515cd3e5866b", 
+    "revision": "f1421b9d57e81c3823a32eb02e6ab6e3c74b12f1", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -5,17 +5,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fce1a137746dbd354bca1918f02f96d51c40bad2">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.1" path="prebuilts/clang/darwin-x86/3.1" revision="8a10d50e8caab8c18224588f0531f1c9363965b5"/>
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.2" path="prebuilts/clang/darwin-x86/3.2" revision="2d96fcbab6efee560c2004725b21bdc06d090933"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ef8bb31b462f364b57432a0724c78034d3f4f303"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -161,16 +161,17 @@
 @BINPATH@/components/dom_wifi.xpt
 @BINPATH@/components/dom_system_gonk.xpt
 #endif
 #ifdef MOZ_B2G_RIL
 @BINPATH@/components/dom_voicemail.xpt
 @BINPATH@/components/dom_icc.xpt
 @BINPATH@/components/dom_cellbroadcast.xpt
 @BINPATH@/components/dom_wappush.xpt
+@BINPATH@/components/dom_mobileconnection.xpt
 #endif
 #ifdef MOZ_B2G_BT
 @BINPATH@/components/dom_bluetooth.xpt
 #endif
 @BINPATH@/components/dom_camera.xpt
 @BINPATH@/components/dom_canvas.xpt
 @BINPATH@/components/dom_contacts.xpt
 @BINPATH@/components/dom_alarm.xpt
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -837,19 +837,16 @@ pref("browser.rights.3.shown", false);
 pref("browser.rights.override", true);
 #endif
 
 pref("browser.sessionstore.resume_from_crash", true);
 pref("browser.sessionstore.resume_session_once", false);
 
 // minimal interval between two save operations in milliseconds
 pref("browser.sessionstore.interval", 15000);
-// maximum amount of POSTDATA to be saved in bytes per history entry (-1 = all of it)
-// (NB: POSTDATA will be saved either entirely or not at all)
-pref("browser.sessionstore.postdata", 0);
 // on which sites to save text data, POSTDATA and cookies
 // 0 = everywhere, 1 = unencrypted sites, 2 = nowhere
 pref("browser.sessionstore.privacy_level", 0);
 // the same as browser.sessionstore.privacy_level, but for saving deferred session data
 pref("browser.sessionstore.privacy_level_deferred", 1);
 // how many tabs can be reopened (per window)
 pref("browser.sessionstore.max_tabs_undo", 10);
 // how many windows can be reopened (per session) - on non-OS X platforms this
--- a/browser/base/content/browser-thumbnails.js
+++ b/browser/base/content/browser-thumbnails.js
@@ -84,17 +84,17 @@ let gBrowserThumbnails = {
 
   observe: function Thumbnails_observe() {
     this._sslDiskCacheEnabled =
       Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL);
   },
 
   filterForThumbnailExpiration:
   function Thumbnails_filterForThumbnailExpiration(aCallback) {
-    aCallback([browser.currentURI.spec for (browser of gBrowser.browsers)]);
+    aCallback(this._topSiteURLs);
   },
 
   /**
    * State change progress listener for all tabs.
    */
   onStateChange: function Thumbnails_onStateChange(aBrowser, aWebProgress,
                                                    aRequest, aStateFlags, aStatus) {
     if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
@@ -117,23 +117,22 @@ let gBrowserThumbnails = {
       this._clearTimeout(aBrowser);
       this._capture(aBrowser);
     }.bind(this), this._captureDelayMS);
 
     this._timeouts.set(aBrowser, timeout);
   },
 
   _shouldCapture: function Thumbnails_shouldCapture(aBrowser) {
-    // Capture only if it's a top site in about:newtab.
-    if (!NewTabUtils.links.getLinks().some(
-          (link) => link && link.url == aBrowser.currentURI.spec))
+    // Capture only if it's the currently selected tab.
+    if (aBrowser != gBrowser.selectedBrowser)
       return false;
 
-    // Capture only if it's the currently selected tab.
-    if (aBrowser != gBrowser.selectedBrowser)
+    // Only capture about:newtab top sites.
+    if (this._topSiteURLs.indexOf(aBrowser.currentURI.spec) < 0)
       return false;
 
     // Don't capture in per-window private browsing mode.
     if (PrivateBrowsingUtils.isWindowPrivate(window))
       return false;
 
     let doc = aBrowser.contentDocument;
 
@@ -185,16 +184,24 @@ let gBrowserThumbnails = {
       // Don't capture HTTPS pages unless the user explicitly enabled it.
       if (uri.schemeIs("https") && !this._sslDiskCacheEnabled)
         return false;
     }
 
     return true;
   },
 
+  get _topSiteURLs() {
+    return NewTabUtils.links.getLinks().reduce((urls, link) => {
+      if (link)
+        urls.push(link.url);
+      return urls;
+    }, []);
+  },
+
   _clearTimeout: function Thumbnails_clearTimeout(aBrowser) {
     if (this._timeouts.has(aBrowser)) {
       aBrowser.removeEventListener("scroll", this, false);
       clearTimeout(this._timeouts.get(aBrowser));
       this._timeouts.delete(aBrowser);
     }
   }
 };
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -898,16 +898,17 @@ chatbox:-moz-full-screen-ancestor > .cha
 
 #customization-panelWrapper,
 #customization-panelWrapper > .panel-arrowcontent {
   -moz-box-flex: 1;
 }
 
 #customization-panelWrapper > .panel-arrowcontent {
   padding: 0 !important;
+  overflow: hidden;
 }
 
 #customization-panelHolder > #PanelUI-mainView {
   display: flex;
   flex-direction: column;
   /* Hack alert - by manually setting the preferred height to 0, we convince
      #PanelUI-mainView to shrink when the window gets smaller in customization
      mode. Not sure why that is - might have to do with our intermingling of
@@ -931,16 +932,20 @@ toolbarpaletteitem[dragover] {
 }
 
 #customization-palette:not([hidden]) {
   display: block;
   overflow: auto;
   min-height: 3em;
 }
 
+#customization-toolbar-visibility-button > .box-inherit > .button-menu-dropmarker {
+  display: -moz-box;
+}
+
 toolbarpaletteitem[place="palette"] {
   width: 10em;
   height: calc(40px + 2em);
   margin-bottom: 5px;
   overflow: hidden;
   display: inline-block;
 }
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4224,52 +4224,55 @@ nsBrowserAccess.prototype = {
     return gBrowser.browsers.some(function (browser) browser.contentWindow == aWindow);
   },
 
   get contentWindow() {
     return gBrowser.contentWindow;
   }
 }
 
+function getTogglableToolbars() {
+  let toolbarNodes = Array.slice(gNavToolbox.childNodes);
+  toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
+  toolbarNodes = toolbarNodes.filter(node => node.getAttribute("toolbarname"));
+  return toolbarNodes;
+}
+
 function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
   var popup = aEvent.target;
   if (popup != aEvent.currentTarget)
     return;
 
   // Empty the menu
   for (var i = popup.childNodes.length-1; i >= 0; --i) {
     var deadItem = popup.childNodes[i];
     if (deadItem.hasAttribute("toolbarId"))
       popup.removeChild(deadItem);
   }
 
   var firstMenuItem = aInsertPoint || popup.firstChild;
 
-  let toolbarNodes = Array.slice(gNavToolbox.childNodes);
-  toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
+  let toolbarNodes = getTogglableToolbars();
 
   for (let toolbar of toolbarNodes) {
-    let toolbarName = toolbar.getAttribute("toolbarname");
-    if (toolbarName) {
-      let menuItem = document.createElement("menuitem");
-      let hidingAttribute = toolbar.getAttribute("type") == "menubar" ?
-                            "autohide" : "collapsed";
-      menuItem.setAttribute("id", "toggle_" + toolbar.id);
-      menuItem.setAttribute("toolbarId", toolbar.id);
-      menuItem.setAttribute("type", "checkbox");
-      menuItem.setAttribute("label", toolbarName);
-      menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true");
-      menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
-      if (popup.id != "toolbar-context-menu")
-        menuItem.setAttribute("key", toolbar.getAttribute("key"));
-
-      popup.insertBefore(menuItem, firstMenuItem);
-
-      menuItem.addEventListener("command", onViewToolbarCommand, false);
-    }
+    let menuItem = document.createElement("menuitem");
+    let hidingAttribute = toolbar.getAttribute("type") == "menubar" ?
+                          "autohide" : "collapsed";
+    menuItem.setAttribute("id", "toggle_" + toolbar.id);
+    menuItem.setAttribute("toolbarId", toolbar.id);
+    menuItem.setAttribute("type", "checkbox");
+    menuItem.setAttribute("label", toolbar.getAttribute("toolbarname"));
+    menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true");
+    menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
+    if (popup.id != "toolbar-context-menu")
+      menuItem.setAttribute("key", toolbar.getAttribute("key"));
+
+    popup.insertBefore(menuItem, firstMenuItem);
+
+    menuItem.addEventListener("command", onViewToolbarCommand, false);
   }
 
 
   let moveToPanel = popup.querySelector(".customize-context-moveToPanel");
   let removeFromToolbar = popup.querySelector(".customize-context-removeFromToolbar");
   // View -> Toolbars menu doesn't have the moveToPanel or removeFromToolbar items.
   if (!moveToPanel || !removeFromToolbar) {
     return;
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -1,17 +1,23 @@
 <!-- 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/. -->
 
 <hbox id="customization-container" flex="1" hidden="true">
   <vbox flex="1" id="customization-palette-container">
-    <label id="customization-header" value="&customizeMode.menuAndToolbars.header;"/>
+    <label id="customization-header">
+      &customizeMode.menuAndToolbars.header;
+    </label>
     <vbox id="customization-palette" flex="1"/>
-    <hbox pack="start">
+    <hbox>
+      <button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
+        <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
+      </button>
+      <spacer flex="1"/>
       <button id="customization-reset-button" oncommand="gCustomizeMode.reset();" label="&customizeMode.restoreDefaults;" class="customizationmode-button"/>
     </hbox>
   </vbox>
   <vbox id="customization-panel-container">
     <vbox id="customization-panelWrapper">
       <html:style html:type="text/html" scoped="scoped">
         @import url(chrome://global/skin/popup.css);
       </html:style>
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -10,16 +10,17 @@ const {classes: Cc, interfaces: Ci, util
 
 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
 const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation";
 const kPaletteId = "customization-palette";
 const kAboutURI = "about:customizing";
 const kDragDataTypePrefix = "text/toolbarwrapper-id/";
 const kPlaceholderClass = "panel-customization-placeholder";
 const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
+const kToolbarVisibilityBtn = "customization-toolbar-visibility-button";
 const kMaxTransitionDurationMs = 2000;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/CustomizableUI.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
@@ -108,16 +109,26 @@ CustomizeMode.prototype = {
             Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
             delayedStartupDeferred.resolve();
           }
         }.bind(this);
         Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
         yield delayedStartupDeferred.promise;
       }
 
+      let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
+      let togglableToolbars = window.getTogglableToolbars();
+      let bookmarksToolbar = document.getElementById("PersonalToolbar");
+      if (togglableToolbars.length == 0 ||
+          (togglableToolbars.length == 1 && togglableToolbars[0] == bookmarksToolbar)) {
+        toolbarVisibilityBtn.setAttribute("hidden", "true");
+      } else {
+        toolbarVisibilityBtn.removeAttribute("hidden");
+      }
+
       // Disable lightweight themes while in customization mode since
       // they don't have large enough images to pad the full browser window.
       if (this.document.documentElement._lightweightTheme)
         this.document.documentElement._lightweightTheme.disable();
 
       this.dispatchToolboxEvent("beforecustomization");
       CustomizableUI.notifyStartCustomizing(this.window);
 
@@ -566,16 +577,19 @@ CustomizeMode.prototype = {
     if (aNode.hasAttribute("title")) {
       wrapper.setAttribute("title", aNode.getAttribute("title"));
     } else if (aNode.hasAttribute("label")) {
       wrapper.setAttribute("title", aNode.getAttribute("label"));
     }
 
     if (aNode.hasAttribute("flex")) {
       wrapper.setAttribute("flex", aNode.getAttribute("flex"));
+      if (aPlace == "palette") {
+        aNode.removeAttribute("flex");
+      }
     }
 
 
     const kPanelItemContextMenu = "customizationPanelItemContextMenu";
     const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
     let contextMenuAttrName = aNode.getAttribute("context") ? "context" :
                                 aNode.getAttribute("contextmenu") ? "contextmenu" : "";
     let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
@@ -632,16 +646,20 @@ CustomizeMode.prototype = {
     if (aWrapper.hasAttribute("itemobserves")) {
       toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves"));
     }
 
     if (aWrapper.hasAttribute("itemchecked")) {
       toolbarItem.checked = true;
     }
 
+    if (aWrapper.hasAttribute("flex") && !toolbarItem.hasAttribute("flex")) {
+      toolbarItem.setAttribute("flex", aWrapper.getAttribute("flex"));
+    }
+
     if (aWrapper.hasAttribute("itemcommand")) {
       let commandID = aWrapper.getAttribute("itemcommand");
       toolbarItem.setAttribute("command", commandID);
 
       //XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
       let command = this.document.getElementById(commandID);
       if (command && command.hasAttribute("disabled")) {
         toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
@@ -910,16 +928,17 @@ CustomizeMode.prototype = {
 
     if (item.classList.contains(kPlaceholderClass)) {
       return;
     }
 
     let dt = aEvent.dataTransfer;
     let documentId = aEvent.target.ownerDocument.documentElement.id;
     let draggedItem = item.firstChild;
+    let isInToolbar = CustomizableUI.getPlaceForItem(item) == "toolbar";
 
     dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
     dt.effectAllowed = "move";
 
     let itemRect = draggedItem.getBoundingClientRect();
     let itemCenter = {x: itemRect.left + itemRect.width / 2,
                       y: itemRect.top + itemRect.height / 2};
     this._dragOffset = {x: aEvent.clientX - itemCenter.x,
@@ -930,16 +949,19 @@ CustomizeMode.prototype = {
     this._initializeDragAfterMove = function() {
       // For automated tests, we sometimes start exiting customization mode
       // before this fires, which leaves us with placeholders inserted after
       // we've exited. So we need to check that we are indeed customizing.
       if (this._customizing && !this._transitioning) {
         item.hidden = true;
         this._showPanelCustomizationPlaceholders();
         DragPositionManager.start(this.window);
+        if (!isInToolbar && item.nextSibling) {
+          this._setDragActive(item.nextSibling, "before", draggedItem.id, false);
+        }
       }
       this._initializeDragAfterMove = null;
       this.window.clearTimeout(this._dragInitializeTimeout);
     }.bind(this);
     this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0);
   },
 
   _onDragOver: function(aEvent) {
@@ -1279,17 +1301,17 @@ CustomizeMode.prototype = {
     }
 
     if (aItem.hasAttribute("dragover") != aValue) {
       aItem.setAttribute("dragover", aValue);
 
       let window = aItem.ownerDocument.defaultView;
       let draggedItem = window.document.getElementById(aDraggedItemId);
       if (!aInToolbar) {
-        this._setPanelDragActive(aItem, draggedItem, aValue);
+        this._setGridDragActive(aItem, draggedItem, aValue);
       } else {
         // Calculate width of the item when it'd be dropped in this position
         let width = this._getDragItemSize(aItem, draggedItem).width;
         let direction = window.getComputedStyle(aItem).direction;
         let prop, otherProp;
         // If we're inserting before in ltr, or after in rtl:
         if ((aValue == "before") == (direction == "ltr")) {
           prop = "borderLeftWidth";
@@ -1325,17 +1347,17 @@ CustomizeMode.prototype = {
         }
       }
       // Otherwise, clear everything out:
       let positionManager = DragPositionManager.getManagerForArea(currentArea);
       positionManager.clearPlaceholders(currentArea, aNoTransition);
     }
   },
 
-  _setPanelDragActive: function(aDragOverNode, aDraggedItem, aValue) {
+  _setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) {
     let targetArea = this._getCustomizableParent(aDragOverNode);
     let positionManager = DragPositionManager.getManagerForArea(targetArea);
     let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
     let isWide = aDraggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
     positionManager.insertPlaceholder(targetArea, aDragOverNode, isWide, draggedSize);
   },
 
   _getDragItemSize: function(aDragOverNode, aDraggedItem) {
--- a/browser/components/customizableui/src/DragPositionManager.jsm
+++ b/browser/components/customizableui/src/DragPositionManager.jsm
@@ -152,25 +152,37 @@ AreaPositionManager.prototype = {
         isShifted = false;
       }
       if (isShifted) {
         // Conversely, if we're adding something before a wide node, for
         // simplicity's sake we move everything including the wide node down:
         if (this.__moveDown) {
           shiftDown = true;
         }
+        if (!this._lastPlaceholderInsertion) {
+          child.setAttribute("notransition", "true");
+        }
         // Determine the CSS transform based on the next node:
         child.style.transform = this._getNextPos(child, shiftDown, aSize);
       } else {
         // If we're not shifting this node, reset the transform
         child.style.transform = "";
       }
     }
+    if (aContainer.lastChild && !this._lastPlaceholderInsertion) {
+      // Flush layout:
+      aContainer.lastChild.getBoundingClientRect();
+      // then remove all the [notransition]
+      for (let child of aContainer.children) {
+        child.removeAttribute("notransition");
+      }
+    }
     delete this.__moveDown;
     delete this.__undoShift;
+    this._lastPlaceholderInsertion = aBefore;
   },
 
   isWide: function(aNode) {
     return this._wideCache.has(aNode.id);
   },
 
   _checkIfWide: function(aNode) {
     return this._inPanel && aNode && aNode.firstChild &&
@@ -191,16 +203,21 @@ AreaPositionManager.prototype = {
       }
       child.style.transform = "";
       if (aNoTransition) {
         // Need to force a reflow otherwise this won't work.
         child.getBoundingClientRect();
         child.removeAttribute("notransition");
       }
     }
+    // We snapped back, so we can assume there's no more
+    // "last" placeholder insertion point to keep track of.
+    if (aNoTransition) {
+      this._lastPlaceholderInsertion = null;
+    }
   },
 
   _getNextPos: function(aNode, aShiftDown, aSize) {
     // Shifting down is easy:
     if (this._inPanel && aShiftDown) {
       return "translate(0, " + aSize.height + "px)";
     }
     return this._diffWithNext(aNode, aSize);
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -37,16 +37,18 @@ skip-if = true
 [browser_923857_customize_mode_event_wrapping_during_reset.js]
 [browser_927717_customize_drag_empty_toolbar.js]
 
 [browser_934113_menubar_removable.js]
 # Because this test is about the menubar, it can't be run on mac
 skip-if = os == "mac"
 
 [browser_946320_tabs_from_other_computers.js]
+skip-if = os == "linux"
+
 [browser_934951_zoom_in_toolbar.js]
 [browser_938980_navbar_collapsed.js]
 [browser_938995_indefaultstate_nonremovable.js]
 [browser_940013_registerToolbarNode_calls_registerArea.js]
 [browser_940107_home_button_in_bookmarks_toolbar.js]
 [browser_940946_removable_from_navbar_customizemode.js]
 [browser_941083_invalidate_wrapper_cache_createWidget.js]
 [browser_942581_unregisterArea_keeps_placements.js]
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -13,26 +13,26 @@ let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Timer.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
   "resource:///modules/sessionstore/DocShellCapabilities.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormData",
+  "resource:///modules/sessionstore/FormData.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
   "resource:///modules/sessionstore/PageStyle.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
   "resource:///modules/sessionstore/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource:///modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
-  "resource:///modules/sessionstore/TextAndScrollData.jsm");
 
 Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
 let gFrameTree = new FrameTree(this);
 
 Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
 XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
                             () => { return new ContentRestore(this) });
 
@@ -69,22 +69,19 @@ function isSessionStorageEvent(event) {
 }
 
 /**
  * Listens for and handles content events that we need for the
  * session store service to be notified of state changes in content.
  */
 let EventListener = {
 
-  DOM_EVENTS: [
-    "load", "pageshow", "change", "input"
-  ],
-
   init: function () {
-    this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
+    addEventListener("load", this, true);
+    addEventListener("pageshow", this, true);
   },
 
   handleEvent: function (event) {
     switch (event.type) {
       case "load":
         // Ignore load events from subframes.
         if (event.target == content.document) {
           // If we're in the process of restoring, this load may signal
@@ -101,20 +98,16 @@ let EventListener = {
           // Send a load message for all loads so we can invalidate the TabStateCache.
           sendAsyncMessage("SessionStore:load");
         }
         break;
       case "pageshow":
         if (event.persisted && event.target == content.document)
           sendAsyncMessage("SessionStore:pageshow");
         break;
-      case "input":
-      case "change":
-        sendAsyncMessage("SessionStore:input");
-        break;
       default:
         debug("received unknown event '" + event.type + "'");
         break;
     }
   }
 };
 
 /**
@@ -134,24 +127,16 @@ let MessageListener = {
     this.MESSAGES.forEach(m => addMessageListener(m, this));
   },
 
   receiveMessage: function ({name, data}) {
     let id = data ? data.id : 0;
     switch (name) {
       case "SessionStore:collectSessionHistory":
         let history = SessionHistory.collect(docShell);
-        if ("index" in history) {
-          let tabIndex = history.index - 1;
-          // Don't include private data. It's only needed when duplicating
-          // tabs, which collects data synchronously.
-          TextAndScrollData.updateFrame(history.entries[tabIndex],
-                                        content,
-                                        docShell.isAppTab);
-        }
         sendAsyncMessage(name, {id: id, data: history});
         break;
       case "SessionStore:restoreHistory":
         let reloadCallback = () => {
           // Inform SessionStore.jsm about the reload. It will send
           // restoreTabContent in response.
           sendAsyncMessage("SessionStore:reloadPendingTab", {epoch: data.epoch});
         };
@@ -207,25 +192,17 @@ let SyncHandler = {
     // Send this object as a CPOW to chrome. In single-process mode,
     // the synchronous send ensures that the handler object is
     // available in SessionStore.jsm immediately upon loading
     // content-sessionStore.js.
     sendSyncMessage("SessionStore:setupSyncHandler", {}, {handler: this});
   },
 
   collectSessionHistory: function (includePrivateData) {
-    let history = SessionHistory.collect(docShell);
-    if ("index" in history) {
-      let tabIndex = history.index - 1;
-      TextAndScrollData.updateFrame(history.entries[tabIndex],
-                                    content,
-                                    docShell.isAppTab,
-                                    {includePrivateData: includePrivateData});
-    }
-    return history;
+    return SessionHistory.collect(docShell);
   },
 
   /**
    * This function is used to make the tab process flush all data that
    * hasn't been sent to the parent process, yet.
    *
    * @param id (int)
    *        A unique id that represents the last message received by the chrome
@@ -303,16 +280,61 @@ let ScrollPositionListener = {
   },
 
   collect: function () {
     return gFrameTree.map(ScrollPosition.collect);
   }
 };
 
 /**
+ * Listens for changes to input elements. Whenever the value of an input
+ * element changes we will re-collect data for the current frame tree and send
+ * a message to the parent process.
+ *
+ * Causes a SessionStore:update message to be sent that contains the form data
+ * for all reachable frames.
+ *
+ * Example:
+ *   {
+ *     formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}},
+ *     children: [
+ *       null,
+ *       {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
+ *     ]
+ *   }
+ */
+let FormDataListener = {
+  init: function () {
+    addEventListener("input", this, true);
+    addEventListener("change", this, true);
+    gFrameTree.addObserver(this);
+  },
+
+  handleEvent: function (event) {
+    let frame = event.target &&
+                event.target.ownerDocument &&
+                event.target.ownerDocument.defaultView;
+
+    // Don't collect form data for frames created at or after the load event
+    // as SessionStore can't restore form data for those.
+    if (frame && gFrameTree.contains(frame)) {
+      MessageQueue.push("formdata", () => this.collect());
+    }
+  },
+
+  onFrameTreeReset: function () {
+    MessageQueue.push("formdata", () => null);
+  },
+
+  collect: function () {
+    return gFrameTree.map(FormData.collect);
+  }
+};
+
+/**
  * Listens for changes to the page style. Whenever a different page style is
  * selected or author styles are enabled/disabled we send a message with the
  * currently applied style to the chrome process.
  *
  * Causes a SessionStore:update message to be sent that contains the currently
  * selected pageStyle for all reachable frames.
  *
  * Example:
@@ -621,15 +643,16 @@ let MessageQueue = {
     }
 
     this.send();
   }
 };
 
 EventListener.init();
 MessageListener.init();
+FormDataListener.init();
 SyncHandler.init();
 ProgressListener.init();
 PageStyleListener.init();
 SessionStorageListener.init();
 ScrollPositionListener.init();
 DocShellCapabilitiesListener.init();
 PrivacyListener.init();
--- a/browser/components/sessionstore/nsISessionStartup.idl
+++ b/browser/components/sessionstore/nsISessionStartup.idl
@@ -1,21 +1,21 @@
 /* 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/. */
 
 #include "nsISupports.idl"
 
 /**
  * nsISessionStore keeps track of the current browsing state - i.e.
- * tab history, cookies, scroll state, form data, POSTDATA and window features
+ * tab history, cookies, scroll state, form data, and window features
  * - and allows to restore everything into one window.
  */
 
-[scriptable, uuid(6c79d4c1-f071-4c5c-a7fb-676adb144584)]
+[scriptable, uuid(934697e4-3807-47f8-b6c9-6caa8d83ccd1)]
 interface nsISessionStartup: nsISupports
 {
   /**
    * Return a promise that is resolved once initialization
    * is complete.
    */
   readonly attribute jsval onceInitialized;
 
@@ -57,9 +57,10 @@ interface nsISessionStartup: nsISupports
    *                  without explicit action (with the exception of pinned tabs)
    */
   const unsigned long NO_SESSION = 0;
   const unsigned long RECOVER_SESSION = 1;
   const unsigned long RESUME_SESSION = 2;
   const unsigned long DEFER_SESSION = 3;
 
   readonly attribute unsigned long sessionType;
+  readonly attribute bool previousSessionCrashed;
 };
--- a/browser/components/sessionstore/nsISessionStore.idl
+++ b/browser/components/sessionstore/nsISessionStore.idl
@@ -4,17 +4,17 @@
 
 #include "nsISupports.idl"
 
 interface nsIDOMWindow;
 interface nsIDOMNode;
 
 /**
  * nsISessionStore keeps track of the current browsing state - i.e.
- * tab history, cookies, scroll state, form data, POSTDATA and window features
+ * tab history, cookies, scroll state, form data, and window features
  * - and allows to restore everything into one browser window.
  *
  * The nsISessionStore API operates mostly on browser windows and the tabbrowser
  * tabs contained in them:
  *
  * * "Browser windows" are those DOM windows having loaded
  * chrome://browser/content/browser.xul . From overlays you can just pass the
  * global |window| object to the API, though (or |top| from a sidebar).
--- a/browser/components/sessionstore/src/ContentRestore.jsm
+++ b/browser/components/sessionstore/src/ContentRestore.jsm
@@ -8,26 +8,26 @@ this.EXPORTED_SYMBOLS = ["ContentRestore
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
   "resource:///modules/sessionstore/DocShellCapabilities.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormData",
+  "resource:///modules/sessionstore/FormData.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
   "resource:///modules/sessionstore/PageStyle.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
   "resource:///modules/sessionstore/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource:///modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
-  "resource:///modules/sessionstore/TextAndScrollData.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource:///modules/sessionstore/Utils.jsm");
 
 /**
  * This module implements the content side of session restoration. The chrome
  * side is handled by SessionStore.jsm. The functions in this module are called
  * by content-sessionStore.js based on messages received from SessionStore.jsm
  * (or, in one case, based on a "load" event). Each tab has its own
@@ -86,17 +86,17 @@ function ContentRestoreInternal(chromeGl
 
   // The epoch that was passed into restoreHistory. Removed in restoreDocument.
   this._epoch = 0;
 
   // The tabData for the restore. Set in restoreHistory and removed in
   // restoreTabContent.
   this._tabData = null;
 
-  // Contains {entry, pageStyle, scrollPositions}, where entry is a
+  // Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a
   // single entry from the tabData.entries array. Set in
   // restoreTabContent and removed in restoreDocument.
   this._restoringDocument = null;
 
   // This listener is used to detect reloads on restoring tabs. Set in
   // restoreHistory and removed in restoreTabContent.
   this._historyListener = null;
 
@@ -202,16 +202,17 @@ ContentRestoreInternal.prototype = {
         // Load userTypedValue and fix up the URL if it's partial/broken.
         webNavigation.loadURI(tabData.userTypedValue,
                               Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
                               null, null, null);
       } else if (tabData.entries.length) {
         // Stash away the data we need for restoreDocument.
         let activeIndex = tabData.index - 1;
         this._restoringDocument = {entry: tabData.entries[activeIndex] || {},
+                                   formdata: tabData.formdata || {},
                                    pageStyle: tabData.pageStyle || {},
                                    scrollPositions: tabData.scroll || {}};
 
         // In order to work around certain issues in session history, we need to
         // force session history to update its internal index and call reload
         // instead of gotoIndex. See bug 597315.
         history.getEntryAtIndex(activeIndex, true);
         history.reloadCurrentEntry();
@@ -272,31 +273,47 @@ ContentRestoreInternal.prototype = {
    * called when the "load" event fires for the restoring tab.
    */
   restoreDocument: function () {
     this._epoch = 0;
 
     if (!this._restoringDocument) {
       return;
     }
-    let {entry, pageStyle, scrollPositions} = this._restoringDocument;
+    let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument;
     this._restoringDocument = null;
 
     let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
     let frameList = this.getFramesToRestore(window, entry);
 
     // Support the old pageStyle format.
     if (typeof(pageStyle) === "string") {
       PageStyle.restore(this.docShell, frameList, pageStyle);
     } else {
       PageStyle.restoreTree(this.docShell, pageStyle);
     }
 
+    FormData.restoreTree(window, formdata);
     ScrollPosition.restoreTree(window, scrollPositions);
-    TextAndScrollData.restore(frameList);
+
+    // We need to support the old form and scroll data for a while at least.
+    for (let [frame, data] of frameList) {
+      if (data.hasOwnProperty("formdata") || data.hasOwnProperty("innerHTML")) {
+        let formdata = data.formdata || {};
+        formdata.url = data.url;
+
+        if (data.hasOwnProperty("innerHTML")) {
+          formdata.innerHTML = data.innerHTML;
+        }
+
+        FormData.restore(frame, formdata);
+      }
+
+      ScrollPosition.restore(frame, data.scroll || "");
+    }
   },
 
   /**
    * Cancel an ongoing restore. This function can be called any time between
    * restoreHistory and restoreDocument.
    *
    * This function is called externally (if a restore is canceled) and
    * internally (when the loads for a restore have finished). In the latter
deleted file mode 100644
--- a/browser/components/sessionstore/src/DocumentUtils.jsm
+++ /dev/null
@@ -1,233 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = [ "DocumentUtils" ];
-
-const Cu = Components.utils;
-const Ci = Components.interfaces;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm");
-
-this.DocumentUtils = {
-  /**
-   * Obtain form data for a DOMDocument instance.
-   *
-   * The returned object has 2 keys, "id" and "xpath". Each key holds an object
-   * which further defines form data.
-   *
-   * The "id" object maps element IDs to values. The "xpath" object maps the
-   * XPath of an element to its value.
-   *
-   * @param  aDocument
-   *         DOMDocument instance to obtain form data for.
-   * @return object
-   *         Form data encoded in an object.
-   */
-  getFormData: function DocumentUtils_getFormData(aDocument) {
-    let formNodes = aDocument.evaluate(
-      XPathGenerator.restorableFormNodes,
-      aDocument,
-      XPathGenerator.resolveNS,
-      Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
-    );
-
-    let node;
-    let ret = {id: {}, xpath: {}};
-
-    // Limit the number of XPath expressions for performance reasons. See
-    // bug 477564.
-    const MAX_TRAVERSED_XPATHS = 100;
-    let generatedCount = 0;
-
-    while (node = formNodes.iterateNext()) {
-      let nId = node.id;
-      let hasDefaultValue = true;
-      let value;
-
-      // Only generate a limited number of XPath expressions for perf reasons
-      // (cf. bug 477564)
-      if (!nId && generatedCount > MAX_TRAVERSED_XPATHS) {
-        continue;
-      }
-
-      if (node instanceof Ci.nsIDOMHTMLInputElement ||
-          node instanceof Ci.nsIDOMHTMLTextAreaElement ||
-          node instanceof Ci.nsIDOMXULTextBoxElement) {
-        switch (node.type) {
-          case "checkbox":
-          case "radio":
-            value = node.checked;
-            hasDefaultValue = value == node.defaultChecked;
-            break;
-          case "file":
-            value = { type: "file", fileList: node.mozGetFileNameArray() };
-            hasDefaultValue = !value.fileList.length;
-            break;
-          default: // text, textarea
-            value = node.value;
-            hasDefaultValue = value == node.defaultValue;
-            break;
-        }
-      } else if (!node.multiple) {
-        // <select>s without the multiple attribute are hard to determine the
-        // default value, so assume we don't have the default.
-        hasDefaultValue = false;
-        value = { selectedIndex: node.selectedIndex, value: node.value };
-      } else {
-        // <select>s with the multiple attribute are easier to determine the
-        // default value since each <option> has a defaultSelected
-        let options = Array.map(node.options, function(aOpt, aIx) {
-          let oSelected = aOpt.selected;
-          hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected);
-          return oSelected ? aOpt.value : -1;
-        });
-        value = options.filter(function(aIx) aIx !== -1);
-      }
-
-      // In order to reduce XPath generation (which is slow), we only save data
-      // for form fields that have been changed. (cf. bug 537289)
-      if (!hasDefaultValue) {
-        if (nId) {
-          ret.id[nId] = value;
-        } else {
-          generatedCount++;
-          ret.xpath[XPathGenerator.generate(node)] = value;
-        }
-      }
-    }
-
-    return ret;
-  },
-
-  /**
-   * Merges form data on a document from previously obtained data.
-   *
-   * This is the inverse of getFormData(). The data argument is the same object
-   * type which is returned by getFormData(): an object containing the keys
-   * "id" and "xpath" which are each objects mapping element identifiers to
-   * form values.
-   *
-   * Where the document has existing form data for an element, the value
-   * will be replaced. Where the document has a form element but no matching
-   * data in the passed object, the element is untouched.
-   *
-   * @param  aDocument
-   *         DOMDocument instance to which to restore form data.
-   * @param  aData
-   *         Object defining form data.
-   */
-  mergeFormData: function DocumentUtils_mergeFormData(aDocument, aData) {
-    if ("xpath" in aData) {
-      for each (let [xpath, value] in Iterator(aData.xpath)) {
-        let node = XPathGenerator.resolve(aDocument, xpath);
-
-        if (node) {
-          this.restoreFormValue(node, value, aDocument);
-        }
-      }
-    }
-
-    if ("id" in aData) {
-      for each (let [id, value] in Iterator(aData.id)) {
-        let node = aDocument.getElementById(id);
-
-        if (node) {
-          this.restoreFormValue(node, value, aDocument);
-        }
-      }
-    }
-  },
-
-  /**
-   * Low-level function to restore a form value to a DOMNode.
-   *
-   * If you want a higher-level interface, see mergeFormData().
-   *
-   * When the value is changed, the function will fire the appropriate DOM
-   * events.
-   *
-   * @param  aNode
-   *         DOMNode to set form value on.
-   * @param  aValue
-   *         Value to set form element to.
-   * @param  aDocument [optional]
-   *         DOMDocument node belongs to. If not defined, node.ownerDocument
-   *         is used.
-   */
-  restoreFormValue: function DocumentUtils_restoreFormValue(aNode, aValue, aDocument) {
-    aDocument = aDocument || aNode.ownerDocument;
-
-    let eventType;
-
-    if (typeof aValue == "string" && aNode.type != "file") {
-      // Don't dispatch an input event if there is no change.
-      if (aNode.value == aValue) {
-        return;
-      }
-
-      aNode.value = aValue;
-      eventType = "input";
-    } else if (typeof aValue == "boolean") {
-      // Don't dispatch a change event for no change.
-      if (aNode.checked == aValue) {
-        return;
-      }
-
-      aNode.checked = aValue;
-      eventType = "change";
-    } else if (typeof aValue == "number") {
-      // handle select backwards compatibility, example { "#id" : index }
-      // We saved the value blindly since selects take more work to determine
-      // default values. So now we should check to avoid unnecessary events.
-      if (aNode.selectedIndex == aValue) {
-        return;
-      }
-
-      if (aValue < aNode.options.length) {
-        aNode.selectedIndex = aValue;
-        eventType = "change";
-      }
-    } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
-      // handle select new format
-
-      // Don't dispatch a change event for no change
-      if (aNode.options[aNode.selectedIndex].value == aValue.value) {
-        return;
-      }
-
-      // find first option with matching aValue if possible
-      for (let i = 0; i < aNode.options.length; i++) {
-        if (aNode.options[i].value == aValue.value) {
-          aNode.selectedIndex = i;
-          eventType = "change";
-          break;
-        }
-      }
-    } else if (aValue && aValue.fileList && aValue.type == "file" &&
-      aNode.type == "file") {
-      aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
-      eventType = "input";
-    } else if (aValue && typeof aValue.indexOf == "function" && aNode.options) {
-      Array.forEach(aNode.options, function(opt, index) {
-        // don't worry about malformed options with same values
-        opt.selected = aValue.indexOf(opt.value) > -1;
-
-        // Only fire the event here if this wasn't selected by default
-        if (!opt.defaultSelected) {
-          eventType = "change";
-        }
-      });
-    }
-
-    // Fire events for this node if applicable
-    if (eventType) {
-      let event = aDocument.createEvent("UIEvents");
-      event.initUIEvent(eventType, true, true, aDocument.defaultView, 0);
-      aNode.dispatchEvent(event);
-    }
-  }
-};
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/src/FormData.jsm
@@ -0,0 +1,364 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FormData"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm");
+
+/**
+ * Returns whether the given URL very likely has input
+ * fields that contain serialized session store data.
+ */
+function isRestorationPage(url) {
+  return url == "about:sessionrestore" || url == "about:welcomeback";
+}
+
+/**
+ * Returns whether the given form |data| object contains nested restoration
+ * data for a page like about:sessionrestore or about:welcomeback.
+ */
+function hasRestorationData(data) {
+  if (isRestorationPage(data.url) && data.id) {
+    return typeof(data.id.sessionData) == "object";
+  }
+
+  return false;
+}
+
+/**
+ * Returns the given document's current URI and strips
+ * off the URI's anchor part, if any.
+ */
+function getDocumentURI(doc) {
+  return doc.documentURI.replace(/#.*$/, "");
+}
+
+/**
+ * The public API exported by this module that allows to collect
+ * and restore form data for a document and its subframes.
+ */
+this.FormData = Object.freeze({
+  collect: function (frame) {
+    return FormDataInternal.collect(frame);
+  },
+
+  restore: function (frame, data) {
+    FormDataInternal.restore(frame, data);
+  },
+
+  restoreTree: function (root, data) {
+    FormDataInternal.restoreTree(root, data);
+  }
+});
+
+/**
+ * This module's internal API.
+ */
+let FormDataInternal = {
+  /**
+   * Collect form data for a given |frame| *not* including any subframes.
+   *
+   * The returned object may have an "id", "xpath", or "innerHTML" key or a
+   * combination of those three. Form data stored under "id" is for input
+   * fields with id attributes. Data stored under "xpath" is used for input
+   * fields that don't have a unique id and need to be queried using XPath.
+   * The "innerHTML" key is used for editable documents (designMode=on).
+   *
+   * Example:
+   *   {
+   *     id: {input1: "value1", input3: "value3"},
+   *     xpath: {
+   *       "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value2",
+   *       "/xhtml:html/xhtml:body/xhtml:input[@name='input4']" : "value4"
+   *     }
+   *   }
+   *
+   * @param  doc
+   *         DOMDocument instance to obtain form data for.
+   * @return object
+   *         Form data encoded in an object.
+   */
+  collect: function ({document: doc}) {
+    let formNodes = doc.evaluate(
+      XPathGenerator.restorableFormNodes,
+      doc,
+      XPathGenerator.resolveNS,
+      Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
+    );
+
+    let node;
+    let ret = {};
+
+    // Limit the number of XPath expressions for performance reasons. See
+    // bug 477564.
+    const MAX_TRAVERSED_XPATHS = 100;
+    let generatedCount = 0;
+
+    while (node = formNodes.iterateNext()) {
+      let hasDefaultValue = true;
+      let value;
+
+      // Only generate a limited number of XPath expressions for perf reasons
+      // (cf. bug 477564)
+      if (!node.id && generatedCount > MAX_TRAVERSED_XPATHS) {
+        continue;
+      }
+
+      if (node instanceof Ci.nsIDOMHTMLInputElement ||
+          node instanceof Ci.nsIDOMHTMLTextAreaElement ||
+          node instanceof Ci.nsIDOMXULTextBoxElement) {
+        switch (node.type) {
+          case "checkbox":
+          case "radio":
+            value = node.checked;
+            hasDefaultValue = value == node.defaultChecked;
+            break;
+          case "file":
+            value = { type: "file", fileList: node.mozGetFileNameArray() };
+            hasDefaultValue = !value.fileList.length;
+            break;
+          default: // text, textarea
+            value = node.value;
+            hasDefaultValue = value == node.defaultValue;
+            break;
+        }
+      } else if (!node.multiple) {
+        // <select>s without the multiple attribute are hard to determine the
+        // default value, so assume we don't have the default.
+        hasDefaultValue = false;
+        value = { selectedIndex: node.selectedIndex, value: node.value };
+      } else {
+        // <select>s with the multiple attribute are easier to determine the
+        // default value since each <option> has a defaultSelected property
+        let options = Array.map(node.options, opt => {
+          hasDefaultValue = hasDefaultValue && (opt.selected == opt.defaultSelected);
+          return opt.selected ? opt.value : -1;
+        });
+        value = options.filter(ix => ix > -1);
+      }
+
+      // In order to reduce XPath generation (which is slow), we only save data
+      // for form fields that have been changed. (cf. bug 537289)
+      if (hasDefaultValue) {
+        continue;
+      }
+
+      if (node.id) {
+        ret.id = ret.id || {};
+        ret.id[node.id] = value;
+      } else {
+        generatedCount++;
+        ret.xpath = ret.xpath || {};
+        ret.xpath[XPathGenerator.generate(node)] = value;
+      }
+    }
+
+    // designMode is undefined e.g. for XUL documents (as about:config)
+    if ((doc.designMode || "") == "on" && doc.body) {
+      ret.innerHTML = doc.body.innerHTML;
+    }
+
+    // Return |null| if no form data has been found.
+    if (Object.keys(ret).length === 0) {
+      return null;
+    }
+
+    // Store the frame's current URL with its form data so that we can compare
+    // it when restoring data to not inject form data into the wrong document.
+    ret.url = getDocumentURI(doc);
+
+    // We want to avoid saving data for about:sessionrestore as a string.
+    // Since it's stored in the form as stringified JSON, stringifying further
+    // causes an explosion of escape characters. cf. bug 467409
+    if (isRestorationPage(ret.url)) {
+      ret.id.sessionData = JSON.parse(ret.id.sessionData);
+    }
+
+    return ret;
+  },
+
+  /**
+   * Restores form |data| for the given frame. The data is expected to be in
+   * the same format that FormData.collect() returns.
+   *
+   * @param frame (DOMWindow)
+   *        The frame to restore form data to.
+   * @param data (object)
+   *        An object holding form data.
+   */
+  restore: function ({document: doc}, data) {
+    // Don't restore any data for the given frame if the URL
+    // stored in the form data doesn't match its current URL.
+    if (!data.url || data.url != getDocumentURI(doc)) {
+      return;
+    }
+
+    // For about:{sessionrestore,welcomeback} we saved the field as JSON to
+    // avoid nested instances causing humongous sessionstore.js files.
+    // cf. bug 467409
+    if (hasRestorationData(data)) {
+      data.id.sessionData = JSON.stringify(data.id.sessionData);
+    }
+
+    if ("id" in data) {
+      let retrieveNode = id => doc.getElementById(id);
+      this.restoreManyInputValues(data.id, retrieveNode);
+    }
+
+    if ("xpath" in data) {
+      let retrieveNode = xpath => XPathGenerator.resolve(doc, xpath);
+      this.restoreManyInputValues(data.xpath, retrieveNode);
+    }
+
+    if ("innerHTML" in data) {
+      // We know that the URL matches data.url right now, but the user
+      // may navigate away before the setTimeout handler runs. We do
+      // a simple comparison against savedURL to check for that.
+      let savedURL = doc.documentURI;
+
+      setTimeout(() => {
+        if (doc.body && doc.designMode == "on" && doc.documentURI == savedURL) {
+          doc.body.innerHTML = data.innerHTML;
+        }
+      });
+    }
+  },
+
+  /**
+   * Iterates the given form data, retrieving nodes for all the keys and
+   * restores their appropriate values.
+   *
+   * @param data (object)
+   *        A subset of the form data as collected by FormData.collect(). This
+   *        is either data stored under "id" or under "xpath".
+   * @param retrieve (function)
+   *        The function used to retrieve the input field belonging to a key
+   *        in the given |data| object.
+   */
+  restoreManyInputValues: function (data, retrieve) {
+    for (let key of Object.keys(data)) {
+      let input = retrieve(key);
+      if (input) {
+        this.restoreSingleInputValue(input, data[key]);
+      }
+    }
+  },
+
+  /**
+   * Restores a given form value to a given DOMNode and takes care of firing
+   * the appropriate DOM event should the input's value change.
+   *
+   * @param  aNode
+   *         DOMNode to set form value on.
+   * @param  aValue
+   *         Value to set form element to.
+   */
+  restoreSingleInputValue: function (aNode, aValue) {
+    let eventType;
+
+    if (typeof aValue == "string" && aNode.type != "file") {
+      // Don't dispatch an input event if there is no change.
+      if (aNode.value == aValue) {
+        return;
+      }
+
+      aNode.value = aValue;
+      eventType = "input";
+    } else if (typeof aValue == "boolean") {
+      // Don't dispatch a change event for no change.
+      if (aNode.checked == aValue) {
+        return;
+      }
+
+      aNode.checked = aValue;
+      eventType = "change";
+    } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
+      // Don't dispatch a change event for no change
+      if (aNode.options[aNode.selectedIndex].value == aValue.value) {
+        return;
+      }
+
+      // find first option with matching aValue if possible
+      for (let i = 0; i < aNode.options.length; i++) {
+        if (aNode.options[i].value == aValue.value) {
+          aNode.selectedIndex = i;
+          eventType = "change";
+          break;
+        }
+      }
+    } else if (aValue && aValue.fileList && aValue.type == "file" &&
+      aNode.type == "file") {
+      aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
+      eventType = "input";
+    } else if (Array.isArray(aValue) && aNode.options) {
+      Array.forEach(aNode.options, function(opt, index) {
+        // don't worry about malformed options with same values
+        opt.selected = aValue.indexOf(opt.value) > -1;
+
+        // Only fire the event here if this wasn't selected by default
+        if (!opt.defaultSelected) {
+          eventType = "change";
+        }
+      });
+    }
+
+    // Fire events for this node if applicable
+    if (eventType) {
+      let doc = aNode.ownerDocument;
+      let event = doc.createEvent("UIEvents");
+      event.initUIEvent(eventType, true, true, doc.defaultView, 0);
+      aNode.dispatchEvent(event);
+    }
+  },
+
+  /**
+   * Restores form data for the current frame hierarchy starting at |root|
+   * using the given form |data|.
+   *
+   * If the given |root| frame's hierarchy doesn't match that of the given
+   * |data| object we will silently discard data for unreachable frames. For
+   * security reasons we will never restore form data to the wrong frames as
+   * we bail out silently if the stored URL doesn't match the frame's current
+   * URL.
+   *
+   * @param root (DOMWindow)
+   * @param data (object)
+   *        {
+   *          formdata: {id: {input1: "value1"}},
+   *          children: [
+   *            {formdata: {id: {input2: "value2"}}},
+   *            null,
+   *            {formdata: {xpath: { ... }}, children: [ ... ]}
+   *          ]
+   *        }
+   */
+  restoreTree: function (root, data) {
+    // Don't restore any data for the root frame and its subframes if there
+    // is a URL stored in the form data and it doesn't match its current URL.
+    if (data.url && data.url != getDocumentURI(root.document)) {
+      return;
+    }
+
+    if (data.url) {
+      this.restore(root, data);
+    }
+
+    if (!data.hasOwnProperty("children")) {
+      return;
+    }
+
+    let frames = root.frames;
+    for (let index of Object.keys(data.children)) {
+      if (index < frames.length) {
+        this.restoreTree(frames[index], data.children[index]);
+      }
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/src/GlobalState.jsm
@@ -0,0 +1,84 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["GlobalState"];
+
+const EXPORTED_METHODS = ["getState", "clear", "get", "set", "delete", "setFromState"];
+/**
+ * Module that contains global session data.
+ */
+function GlobalState() {
+  let internal = new GlobalStateInternal();
+  let external = {};
+  for (let method of EXPORTED_METHODS) {
+    external[method] = internal[method].bind(internal);
+  }
+  return Object.freeze(external);
+}
+
+function GlobalStateInternal() {
+  // Storage for global state.
+  this.state = {};
+}
+
+GlobalStateInternal.prototype = {
+  /**
+   * Get all value from the global state.
+   */
+  getState: function() {
+    return this.state;
+  },
+
+  /**
+   * Clear all currently stored global state.
+   */
+  clear: function() {
+    this.state = {};
+  },
+
+  /**
+   * Retrieve a value from the global state.
+   *
+   * @param aKey
+   *        A key the value is stored under.
+   * @return The value stored at aKey, or an empty string if no value is set.
+   */
+  get: function(aKey) {
+    return this.state[aKey] || "";
+  },
+
+  /**
+   * Set a global value.
+   *
+   * @param aKey
+   *        A key to store the value under.
+   */
+  set: function(aKey, aStringValue) {
+    this.state[aKey] = aStringValue;
+  },
+
+  /**
+   * Delete a global value.
+   *
+   * @param aKey
+   *        A key to delete the value for.
+   */
+  delete: function(aKey) {
+    delete this.state[aKey];
+  },
+
+  /**
+   * Set the current global state from a state object. Any previous global
+   * state will be removed, even if the new state does not contain a matching
+   * key.
+   *
+   * @param aState
+   *        A state object to extract global state from to be set.
+   */
+  setFromState: function (aState) {
+    this.state = (aState && aState.global) || {};
+  }
+};
--- a/browser/components/sessionstore/src/PrivacyLevel.jsm
+++ b/browser/components/sessionstore/src/PrivacyLevel.jsm
@@ -10,17 +10,17 @@ const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 const PREF_NORMAL = "browser.sessionstore.privacy_level";
 const PREF_DEFERRED = "browser.sessionstore.privacy_level_deferred";
 
 // The following constants represent the different possible privacy levels that
 // can be set by the user and that we need to consider when collecting text
-// data, cookies, and POSTDATA.
+// data, and cookies.
 //
 // Collect data from all sites (http and https).
 const PRIVACY_NONE = 0;
 // Collect data from unencrypted sites (http), only.
 const PRIVACY_ENCRYPTED = 1;
 // Collect no data.
 const PRIVACY_FULL = 2;
 
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/src/PrivacyLevelFilter.jsm
@@ -0,0 +1,91 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PrivacyLevelFilter"];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
+  "resource:///modules/sessionstore/PrivacyLevel.jsm");
+
+/**
+ * Returns whether the current privacy level allows saving data for the given
+ * |url|.
+ *
+ * @param url The URL we want to save data for.
+ * @param isPinned Whether the given |url| is contained in a pinned tab.
+ * @return bool
+ */
+function checkPrivacyLevel(url, isPinned) {
+  let isHttps = url.startsWith("https:");
+  return PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned});
+}
+
+/**
+ * A module that provides methods to filter various kinds of data collected
+ * from a tab by the current privacy level as set by the user.
+ */
+this.PrivacyLevelFilter = Object.freeze({
+  /**
+   * Filters the given (serialized) session storage |data| according to the
+   * current privacy level and returns a new object containing only data that
+   * we're allowed to store.
+   *
+   * @param data The session storage data as collected from a tab.
+   * @param isPinned Whether the tab we collected from is pinned.
+   * @return object
+   */
+  filterSessionStorageData: function (data, isPinned) {
+    let retval = {};
+
+    for (let host of Object.keys(data)) {
+      if (checkPrivacyLevel(host, isPinned)) {
+        retval[host] = data[host];
+      }
+    }
+
+    return Object.keys(retval).length ? retval : null;
+  },
+
+  /**
+   * Filters the given (serialized) form |data| according to the current
+   * privacy level and returns a new object containing only data that we're
+   * allowed to store.
+   *
+   * @param data The form data as collected from a tab.
+   * @param isPinned Whether the tab we collected from is pinned.
+   * @return object
+   */
+  filterFormData: function (data, isPinned) {
+    // If the given form data object has an associated URL that we are not
+    // allowed to store data for, bail out. We explicitly discard data for any
+    // children as well even if storing data for those frames would be allowed.
+    if (data.url && !checkPrivacyLevel(data.url, isPinned)) {
+      return;
+    }
+
+    let retval = {};
+
+    for (let key of Object.keys(data)) {
+      if (key === "children") {
+        let recurse = child => this.filterFormData(child, isPinned);
+        let children = data.children.map(recurse).filter(child => child);
+
+        if (children.length) {
+          retval.children = children;
+        }
+      // Only copy keys other than "children" if we have a valid URL in
+      // data.url and we thus passed the privacy level check.
+      } else if (data.url) {
+        retval[key] = data[key];
+      }
+    }
+
+    return Object.keys(retval).length ? retval : null;
+  }
+});
--- a/browser/components/sessionstore/src/SessionFile.jsm
+++ b/browser/components/sessionstore/src/SessionFile.jsm
@@ -70,23 +70,16 @@ this.SessionFile = {
    * @return {Promise}
    * @promise {object} An object holding all the information to be submitted
    * to Telemetry.
    */
   gatherTelemetry: function(aData) {
     return SessionFileInternal.gatherTelemetry(aData);
   },
   /**
-   * Writes the initial state to disk again only to change the session's load
-   * state. This must only be called once, it will throw an error otherwise.
-   */
-  writeLoadStateOnceAfterStartup: function (aLoadState) {
-    SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
-  },
-  /**
    * Create a backup copy, asynchronously.
    * This is designed to perform backup on upgrade.
    */
   createBackupCopy: function (ext) {
     return SessionFileInternal.createBackupCopy(ext);
   },
   /**
    * Remove a backup copy, asynchronously.
@@ -176,23 +169,16 @@ let SessionFileInternal = {
       }
 
       if (isFinalWrite) {
         Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
       }
     }.bind(this));
   },
 
-  writeLoadStateOnceAfterStartup: function (aLoadState) {
-    SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]).then(msg => {
-      this._recordTelemetry(msg.telemetry);
-      return msg;
-    }, console.error);
-  },
-
   createBackupCopy: function (ext) {
     return SessionWorker.post("createBackupCopy", [ext]);
   },
 
   removeBackupCopy: function (ext) {
     return SessionWorker.post("removeBackupCopy", [ext]);
   },
 
--- a/browser/components/sessionstore/src/SessionHistory.jsm
+++ b/browser/components/sessionstore/src/SessionHistory.jsm
@@ -8,73 +8,57 @@ this.EXPORTED_SYMBOLS = ["SessionHistory
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
-  "resource:///modules/sessionstore/PrivacyLevel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource:///modules/sessionstore/Utils.jsm");
 
 function debug(msg) {
   Services.console.logStringMessage("SessionHistory: " + msg);
 }
 
-// The preference value that determines how much post data to save.
-XPCOMUtils.defineLazyGetter(this, "gPostData", function () {
-  const PREF = "browser.sessionstore.postdata";
-
-  // Observer that updates the cached value when the preference changes.
-  Services.prefs.addObserver(PREF, () => {
-    this.gPostData = Services.prefs.getIntPref(PREF);
-  }, false);
-
-  return Services.prefs.getIntPref(PREF);
-});
-
 /**
  * The external API exported by this module.
  */
 this.SessionHistory = Object.freeze({
-  collect: function (docShell, includePrivateData) {
-    return SessionHistoryInternal.collect(docShell, includePrivateData);
+  collect: function (docShell) {
+    return SessionHistoryInternal.collect(docShell);
   },
 
   restore: function (docShell, tabData) {
     SessionHistoryInternal.restore(docShell, tabData);
   }
 });
 
 /**
  * The internal API for the SessionHistory module.
  */
 let SessionHistoryInternal = {
   /**
    * Collects session history data for a given docShell.
    *
    * @param docShell
    *        The docShell that owns the session history.
-   * @param includePrivateData (optional)
-   *        True to always include private data and skip any privacy checks.
    */
-  collect: function (docShell, includePrivateData = false) {
+  collect: function (docShell) {
     let data = {entries: []};
     let isPinned = docShell.isAppTab;
     let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
     let history = webNavigation.sessionHistory;
 
     if (history && history.count > 0) {
       try {
         for (let i = 0; i < history.count; i++) {
           let shEntry = history.getEntryAtIndex(i, false);
-          let entry = this.serializeEntry(shEntry, includePrivateData, isPinned);
+          let entry = this.serializeEntry(shEntry, isPinned);
           data.entries.push(entry);
         }
       } catch (ex) {
         // In some cases, getEntryAtIndex will throw. This seems to be due to
         // history.count being higher than it should be. By doing this in a
         // try-catch, we'll update history to where it breaks, print an error
         // message, and still save sessionstore.js.
         debug("SessionStore failed gathering complete history " +
@@ -104,23 +88,21 @@ let SessionHistoryInternal = {
     return data;
   },
 
   /**
    * Get an object that is a serialized representation of a History entry.
    *
    * @param shEntry
    *        nsISHEntry instance
-   * @param includePrivateData
-   *        Always return privacy sensitive data (use with care).
    * @param isPinned
    *        The tab is pinned and should be treated differently for privacy.
    * @return object
    */
-  serializeEntry: function (shEntry, includePrivateData, isPinned) {
+  serializeEntry: function (shEntry, isPinned) {
     let entry = { url: shEntry.URI.spec };
 
     // Save some bytes and don't include the title property
     // if that's identical to the current entry's URL.
     if (shEntry.title && shEntry.title != entry.url) {
       entry.title = shEntry.title;
     }
     if (shEntry.isSubFrame) {
@@ -151,27 +133,16 @@ let SessionHistoryInternal = {
     if (shEntry.contentType)
       entry.contentType = shEntry.contentType;
 
     let x = {}, y = {};
     shEntry.getScrollPosition(x, y);
     if (x.value != 0 || y.value != 0)
       entry.scroll = x.value + "," + y.value;
 
-    // Collect post data for the current history entry.
-    try {
-      let postdata = this.serializePostData(shEntry, isPinned);
-      if (postdata) {
-        entry.postdata_b64 = postdata;
-      }
-    } catch (ex) {
-      // POSTDATA is tricky - especially since some extensions don't get it right
-      debug("Failed serializing post data: " + ex);
-    }
-
     // Collect owner data for the current history entry.
     try {
       let owner = this.serializeOwner(shEntry);
       if (owner) {
         entry.owner_b64 = owner;
       }
     } catch (ex) {
       // Not catching anything specific here, just possible errors
@@ -198,63 +169,29 @@ let SessionHistoryInternal = {
         if (child) {
           // Don't try to restore framesets containing wyciwyg URLs.
           // (cf. bug 424689 and bug 450595)
           if (child.URI.schemeIs("wyciwyg")) {
             children.length = 0;
             break;
           }
 
-          children.push(this.serializeEntry(child, includePrivateData, isPinned));
+          children.push(this.serializeEntry(child, isPinned));
         }
       }
 
       if (children.length) {
         entry.children = children;
       }
     }
 
     return entry;
   },
 
   /**
-   * Serialize post data contained in the given session history entry.
-   *
-   * @param shEntry
-   *        The session history entry.
-   * @param isPinned
-   *        Whether the docShell is owned by a pinned tab.
-   * @return The base64 encoded post data.
-   */
-  serializePostData: function (shEntry, isPinned) {
-    let isHttps = shEntry.URI.schemeIs("https");
-    if (!shEntry.postData || !gPostData ||
-        !PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
-      return null;
-    }
-
-    shEntry.postData.QueryInterface(Ci.nsISeekableStream)
-                    .seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
-    let stream = Cc["@mozilla.org/binaryinputstream;1"]
-                   .createInstance(Ci.nsIBinaryInputStream);
-    stream.setInputStream(shEntry.postData);
-    let postBytes = stream.readByteArray(stream.available());
-    let postdata = String.fromCharCode.apply(null, postBytes);
-    if (gPostData != -1 &&
-        postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length > gPostData) {
-      return null;
-    }
-
-    // We can stop doing base64 encoding once our serialization into JSON
-    // is guaranteed to handle all chars in strings, including embedded
-    // nulls.
-    return btoa(postdata);
-  },
-
-  /**
    * Serialize owner data contained in the given session history entry.
    *
    * @param shEntry
    *        The session history entry.
    * @return The base64 encoded owner data.
    */
   serializeOwner: function (shEntry) {
     if (!shEntry.owner) {
@@ -370,24 +307,16 @@ let SessionHistoryInternal = {
     }
 
     if (entry.scroll) {
       var scrollPos = (entry.scroll || "0,0").split(",");
       scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
       shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
     }
 
-    if (entry.postdata_b64) {
-      var postdata = atob(entry.postdata_b64);
-      var stream = Cc["@mozilla.org/io/string-input-stream;1"].
-                   createInstance(Ci.nsIStringInputStream);
-      stream.setData(postdata, postdata.length);
-      shEntry.postData = stream;
-    }
-
     let childDocIdents = {};
     if (entry.docIdentifier) {
       // If we have a serialized document identifier, try to find an SHEntry
       // which matches that doc identifier and adopt that SHEntry's
       // BFCacheEntry.  If we don't find a match, insert shEntry as the match
       // for the document identifier.
       let matchingEntry = docIdentMap[entry.docIdentifier];
       if (!matchingEntry) {
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -10,19 +10,16 @@ const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 const STATE_STOPPED = 0;
 const STATE_RUNNING = 1;
 const STATE_QUITTING = -1;
 
-const STATE_STOPPED_STR = "stopped";
-const STATE_RUNNING_STR = "running";
-
 const TAB_STATE_NEEDS_RESTORE = 1;
 const TAB_STATE_RESTORING = 2;
 
 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
 
 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
@@ -47,21 +44,16 @@ const WINDOW_ATTRIBUTES = ["width", "hei
 
 // Hideable window features to (re)store
 // Restored in restoreWindowFeatures()
 const WINDOW_HIDEABLE_FEATURES = [
   "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
 ];
 
 const MESSAGES = [
-  // The content script tells us that its form data (or that of one of its
-  // subframes) might have changed. This can be the contents or values of
-  // standard form fields or of ContentEditables.
-  "SessionStore:input",
-
   // The content script has received a pageshow event. This happens when a
   // page is loaded from bfcache without any network activity, i.e. when
   // clicking the back or forward button.
   "SessionStore:pageshow",
 
   // The content script tells us that a new page just started loading in a
   // browser.
   "SessionStore:loadStart",
@@ -124,16 +116,18 @@ XPCOMUtils.defineLazyServiceGetter(this,
   "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
 XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
   "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
   "@mozilla.org/base/telemetry;1", "nsITelemetry");
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/devtools/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "GlobalState",
+  "resource:///modules/sessionstore/GlobalState.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Messenger",
   "resource:///modules/sessionstore/Messenger.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
   "resource:///modules/devtools/scratchpad-manager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
   "resource:///modules/sessionstore/SessionSaver.jsm");
@@ -316,16 +310,18 @@ let SessionStoreInternal = {
     Ci.nsIDOMEventListener,
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ]),
 
   // set default load state
   _loadState: STATE_STOPPED,
 
+  _globalState: new GlobalState(),
+
   // During the initial restore and setBrowserState calls tracks the number of
   // windows yet to be restored
   _restoreCount: -1,
 
   // This number gets incremented each time we start to restore a tab.
   _nextRestoreEpoch: 1,
 
   // For each <browser> element being restored, records the current epoch.
@@ -404,17 +400,20 @@ let SessionStoreInternal = {
     OBSERVING.forEach(function(aTopic) {
       Services.obs.addObserver(this, aTopic, true);
     }, this);
 
     this._initPrefs();
     this._initialized = true;
   },
 
-  initSession: function ssi_initSession() {
+  /**
+   * Initialize the session using the state provided by SessionStartup
+   */
+  initSession: function () {
     let state;
     let ss = gSessionStartup;
 
     try {
       if (ss.doRestore() ||
           ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
         state = ss.state;
     }
@@ -437,20 +436,17 @@ let SessionStoreInternal = {
             LastSession.setState(remainingState);
           }
         }
         else {
           // Get the last deferred session in case the user still wants to
           // restore it
           LastSession.setState(state.lastSessionState);
 
-          let lastSessionCrashed =
-            state.session && state.session.state &&
-            state.session.state == STATE_RUNNING_STR;
-          if (lastSessionCrashed) {
+          if (ss.previousSessionCrashed) {
             this._recentCrashes = (state.session &&
                                    state.session.recentCrashes || 0) + 1;
 
             if (this._needsRestorePage(state, this._recentCrashes)) {
               // replace the crashed session with a restore-page-only session
               let pageData = {
                 url: "about:sessionrestore",
                 formdata: {
@@ -609,19 +605,16 @@ let SessionStoreInternal = {
   receiveMessage: function ssi_receiveMessage(aMessage) {
     var browser = aMessage.target;
     var win = browser.ownerDocument.defaultView;
 
     switch (aMessage.name) {
       case "SessionStore:pageshow":
         this.onTabLoad(win, browser);
         break;
-      case "SessionStore:input":
-        this.onTabInput(win, browser);
-        break;
       case "SessionStore:loadStart":
         TabStateCache.delete(browser);
         break;
       case "SessionStore:setupSyncHandler":
         TabState.setSyncHandler(browser, aMessage.objects.handler);
         break;
       case "SessionStore:update":
         this.recordTelemetry(aMessage.data.telemetry);
@@ -827,26 +820,21 @@ let SessionStoreInternal = {
           // Nothing to restore now, notify observers things are complete.
           Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
         } else {
           TelemetryTimestamps.add("sessionRestoreRestoring");
           this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0;
 
           // global data must be restored before restoreWindow is called so that
           // it happens before observers are notified
-          GlobalState.setFromState(aInitialState);
+          this._globalState.setFromState(aInitialState);
 
           let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
           let options = {firstWindow: true, overwriteTabs: overwrite};
           this.restoreWindow(aWindow, aInitialState, options);
-
-          // _loadState changed from "stopped" to "running". Save the session's
-          // load state immediately so that crashes happening during startup
-          // are correctly counted.
-          SessionFile.writeLoadStateOnceAfterStartup(STATE_RUNNING_STR);
         }
       }
       else {
         // Nothing to restore, notify observers things are complete.
         Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
 
         // The next delayed save request should execute immediately.
         SessionSaver.clearLastSaveTime();
@@ -861,17 +849,17 @@ let SessionStoreInternal = {
     // The user opened another, non-private window after starting up with
     // a single private one. Let's restore the session we actually wanted to
     // restore at startup.
     else if (this._deferredInitialState && !isPrivateWindow &&
              aWindow.toolbar.visible) {
 
       // global data must be restored before restoreWindow is called so that
       // it happens before observers are notified
-      GlobalState.setFromState(this._deferredInitialState);
+      this._globalState.setFromState(this._deferredInitialState);
 
       this._restoreCount = this._deferredInitialState.windows ?
         this._deferredInitialState.windows.length : 0;
       this.restoreWindow(aWindow, this._deferredInitialState, {firstWindow: true});
       this._deferredInitialState = null;
     }
     else if (this._restoreLastWindow && aWindow.toolbar.visible &&
              this._closedWindows.length && !isPrivateWindow) {
@@ -1440,28 +1428,16 @@ let SessionStoreInternal = {
     delete aBrowser.__SS_data;
     this.saveStateDelayed(aWindow);
 
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
 
   /**
-   * Called when a browser sends the "input" notification
-   * @param aWindow
-   *        Window reference
-   * @param aBrowser
-   *        Browser reference
-   */
-  onTabInput: function ssi_onTabInput(aWindow, aBrowser) {
-    TabStateCache.delete(aBrowser);
-    this.saveStateDelayed(aWindow);
-  },
-
-  /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
   onTabSelect: function ssi_onTabSelect(aWindow) {
     if (this._loadState == STATE_RUNNING) {
       this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
 
@@ -1573,17 +1549,17 @@ let SessionStoreInternal = {
     // make sure closed window data isn't kept
     this._closedWindows = [];
 
     // determine how many windows are meant to be restored
     this._restoreCount = state.windows ? state.windows.length : 0;
 
     // global data must be restored before restoreWindow is called so that
     // it happens before observers are notified
-    GlobalState.setFromState(state);
+    this._globalState.setFromState(state);
 
     // restore to the given state
     this.restoreWindow(window, state, {overwriteTabs: true});
   },
 
   getWindowState: function ssi_getWindowState(aWindow) {
     if ("__SSi" in aWindow) {
       return this._toJSONString(this._getWindowState(aWindow));
@@ -1878,26 +1854,26 @@ let SessionStoreInternal = {
         TabStateCache.removeField(aTab, "extData");
       }
 
       this.saveStateDelayed(aTab.ownerDocument.defaultView);
     }
   },
 
   getGlobalValue: function ssi_getGlobalValue(aKey) {
-    return GlobalState.get(aKey);
+    return this._globalState.get(aKey);
   },
 
   setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
-    GlobalState.set(aKey, aStringValue);
+    this._globalState.set(aKey, aStringValue);
     this.saveStateDelayed();
   },
 
   deleteGlobalValue: function ssi_deleteGlobalValue(aKey) {
-    GlobalState.delete(aKey);
+    this._globalState.delete(aKey);
     this.saveStateDelayed();
   },
 
   persistTabAttribute: function ssi_persistTabAttribute(aName) {
     if (TabAttributes.persist(aName)) {
       TabStateCache.clear();
       this.saveStateDelayed();
     }
@@ -1940,17 +1916,17 @@ let SessionStoreInternal = {
     // We will do more processing via _prepWindowToRestoreInto if we need to use
     // the lastWindow.
     let lastWindow = this._getMostRecentBrowserWindow();
     let canUseLastWindow = lastWindow &&
                            !lastWindow.__SS_lastSessionWindowID;
 
     // global data must be restored before restoreWindow is called so that
     // it happens before observers are notified
-    GlobalState.setFromState(lastSessionState);
+    this._globalState.setFromState(lastSessionState);
 
     // Restore into windows or open new ones as needed.
     for (let i = 0; i < lastSessionState.windows.length; i++) {
       let winState = lastSessionState.windows[i];
       let lastSessionWindowID = winState.__lastSessionWindowID;
       // delete lastSessionWindowID so we don't add that to the window again
       delete winState.__lastSessionWindowID;
 
@@ -2245,32 +2221,31 @@ let SessionStoreInternal = {
     }
     ix = ids.indexOf(this.activeWindowSSiCache);
     // We don't want to restore focus to a minimized window or a window which had all its
     // tabs stripped out (doesn't exist).
     if (ix != -1 && total[ix] && total[ix].sizemode == "minimized")
       ix = -1;
 
     let session = {
-      state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR,
       lastUpdate: Date.now(),
       startTime: this._sessionStartTime,
       recentCrashes: this._recentCrashes
     };
 
     // get open Scratchpad window states too
     var scratchpads = ScratchpadManager.getSessionState();
 
     let state = {
       windows: total,
       selectedWindow: ix + 1,
       _closedWindows: lastClosedWindowsCopy,
       session: session,
       scratchpads: scratchpads,
-      global: GlobalState.state
+      global: this._globalState.getState()
     };
 
     // Persist the last session if we deferred restoring it
     if (LastSession.canRestore) {
       state.lastSessionState = LastSession.getState();
     }
 
     // If we were called by the SessionSaver and started with only a private
@@ -2734,16 +2709,17 @@ let SessionStoreInternal = {
       browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
       browser.setAttribute("pending", "true");
       tab.setAttribute("pending", "true");
 
       // Update the persistent tab state cache with |tabData| information.
       TabStateCache.updatePersistent(browser, {
         scroll: tabData.scroll || null,
         storage: tabData.storage || null,
+        formdata: tabData.formdata || null,
         disallow: tabData.disallow || null,
         pageStyle: tabData.pageStyle || null
       });
 
       browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
                                               {tabData: tabData, epoch: epoch});
 
       // wall-paper fix for bug 439675: make sure that the URL to be loaded
@@ -3849,67 +3825,8 @@ let LastSession = {
 
   clear: function () {
     if (this._state) {
       this._state = null;
       Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null);
     }
   }
 };
-
-/**
- * Module that contains global session data.
- */
-let GlobalState = {
-
-  // Storage for global state.
-  state: {},
-
-  /**
-   * Clear all currently stored global state.
-   */
-  clear: function() {
-    this.state = {};
-  },
-
-  /**
-   * Retrieve a value from the global state.
-   *
-   * @param aKey
-   *        A key the value is stored under.
-   * @return The value stored at aKey, or an empty string if no value is set.
-   */
-  get: function(aKey) {
-    return this.state[aKey] || "";
-  },
-
-  /**
-   * Set a global value.
-   *
-   * @param aKey
-   *        A key to store the value under.
-   */
-  set: function(aKey, aStringValue) {
-    this.state[aKey] = aStringValue;
-  },
-
-  /**
-   * Delete a global value.
-   *
-   * @param aKey
-   *        A key to delete the value for.
-   */
-  delete: function(aKey) {
-    delete this.state[aKey];
-  },
-
-  /**
-   * Set the current global state from a state object. Any previous global
-   * state will be removed, even if the new state does not contain a matching
-   * key.
-   *
-   * @param aState
-   *        A state object to extract global state from to be set.
-   */
-  setFromState: function (aState) {
-    this.state = (aState && aState.global) || {};
-  }
-};
--- a/browser/components/sessionstore/src/SessionWorker.js
+++ b/browser/components/sessionstore/src/SessionWorker.js
@@ -49,23 +49,16 @@ self.onmessage = function (msg) {
   self.postMessage({
     ok: result.result,
     id: id,
     telemetry: result.telemetry || {}
   });
 };
 
 let Agent = {
-  // The initial session string as read from disk.
-  initialState: null,
-
-  // Boolean that tells whether we already wrote
-  // the loadState to disk once after startup.
-  hasWrittenLoadStateOnce: false,
-
   // Boolean that tells whether we already made a
   // call to write(). We will only attempt to move
   // sessionstore.js to sessionstore.bak on the
   // first write.
   hasWrittenState: false,
 
   // The path to sessionstore.js
   path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
@@ -78,20 +71,19 @@ let Agent = {
    * In case sessionstore.js does not exist, attempt to read sessionstore.bak.
    */
   read: function () {
     for (let path of [this.path, this.backupPath]) {
       try {
         let durationMs = Date.now();
         let bytes = File.read(path);
         durationMs = Date.now() - durationMs;
-        this.initialState = Decoder.decode(bytes);
 
         return {
-          result: this.initialState,
+          result: Decoder.decode(bytes),
           telemetry: {FX_SESSION_RESTORE_READ_FILE_MS: durationMs,
                       FX_SESSION_RESTORE_FILE_SIZE_BYTES: bytes.byteLength}
         };
       } catch (ex if isNoSuchFileEx(ex)) {
         // Ignore exceptions about non-existent files.
       }
     }
     // No sessionstore data files found. Return an empty string.
@@ -136,47 +128,16 @@ let Agent = {
    *
    * @return {object}
    */
   gatherTelemetry: function (stateString) {
     return Statistics.collect(stateString);
   },
 
   /**
-   * Writes the session state to disk again but changes session.state to
-   * 'running' before doing so. This is intended to be called only once, shortly
-   * after startup so that we detect crashes on startup correctly.
-   */
-  writeLoadStateOnceAfterStartup: function (loadState) {
-    if (this.hasWrittenLoadStateOnce) {
-      throw new Error("writeLoadStateOnceAfterStartup() must only be called once.");
-    }
-
-    if (!this.initialState) {
-      throw new Error("writeLoadStateOnceAfterStartup() must not be called " +
-                      "without a valid session state or before it has been " +
-                      "read from disk.");
-    }
-
-    // Make sure we can't call this function twice.
-    this.hasWrittenLoadStateOnce = true;
-
-    let state;
-    try {
-      state = JSON.parse(this.initialState);
-    } finally {
-      this.initialState = null;
-    }
-
-    state.session = state.session || {};
-    state.session.state = loadState;
-    return this._write(JSON.stringify(state));
-  },
-
-  /**
    * Write a stateString to disk
    */
   _write: function (stateString, telemetry = {}) {
     let bytes = Encoder.encode(stateString);
     let startMs = Date.now();
     let result = File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
     telemetry.FX_SESSION_RESTORE_WRITE_FILE_MS = Date.now() - startMs;
     return {result: result, telemetry: telemetry};
@@ -347,18 +308,16 @@ let Statistics = {
   /**
    * Collect data that requires walking through the data structure
    */
   gatherComplexData: function(state, subsets) {
     // The subset of sessionstore.js dealing with DOM storage
     subsets.DOM_STORAGE = [];
     // The subset of sessionstore.js storing form data
     subsets.FORMDATA = [];
-    // The subset of sessionstore.js storing POST data in history
-    subsets.POSTDATA = [];
     // The subset of sessionstore.js storing history
     subsets.HISTORY = [];
 
 
     this.walk(state, function(k, value) {
       let dest;
       switch (k) {
         case "entries":
@@ -367,19 +326,16 @@ let Statistics = {
         case "storage":
           subsets.DOM_STORAGE.push(value);
           // Never visit storage, it's full of weird stuff
           return false;
         case "formdata":
           subsets.FORMDATA.push(value);
           // Never visit formdata, it's full of weird stuff
           return false;
-        case "postdata_b64":
-          subsets.POSTDATA.push(value);
-          return false; // Nothing to visit anyway
         case "cookies": // Don't visit these places, they are full of weird stuff
         case "extData":
           return false;
         default:
           return true;
       }
     });
 
--- a/browser/components/sessionstore/src/TabState.jsm
+++ b/browser/components/sessionstore/src/TabState.jsm
@@ -11,18 +11,18 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/devtools/Console.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Messenger",
   "resource:///modules/sessionstore/Messenger.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
-  "resource:///modules/sessionstore/PrivacyLevel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevelFilter",
+  "resource:///modules/sessionstore/PrivacyLevelFilter.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
   "resource:///modules/sessionstore/TabStateCache.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
   "resource:///modules/sessionstore/TabAttributes.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource:///modules/sessionstore/Utils.jsm");
 
 /**
@@ -161,18 +161,17 @@ let TabStateInternal = {
     if (!this._tabNeedsExtraCollection(tab)) {
       let tabData = this._collectBaseTabData(tab);
       return Promise.resolve(tabData);
     }
 
     let browser = tab.linkedBrowser;
 
     let promise = Task.spawn(function task() {
-      // Collect session history data asynchronously. Also collects
-      // text and scroll data.
+      // Collect session history data asynchronously.
       let history = yield Messenger.send(tab, "SessionStore:collectSessionHistory");
 
       // The tab could have been closed while waiting for a response.
       if (!tab.linkedBrowser) {
         return;
       }
 
       // Collect basic tab data, without session history and storage.
@@ -349,43 +348,37 @@ let TabStateInternal = {
    *        The tab belonging to the given |tabData| object.
    * @param tabData (object)
    *        The tab data belonging to the given |tab|.
    * @param options (object)
    *        {includePrivateData: true} to always include private data
    */
   _copyFromPersistentCache: function (tab, tabData, options = {}) {
     let data = TabStateCache.getPersistent(tab.linkedBrowser);
-
-    // Nothing to do without any cached data.
     if (!data) {
       return;
     }
 
+    // The caller may explicitly request to omit privacy checks.
     let includePrivateData = options && options.includePrivateData;
 
     for (let key of Object.keys(data)) {
-      if (key != "storage" || includePrivateData) {
-        tabData[key] = data[key];
-      } else {
-        let storage = {};
-        let isPinned = tab.pinned;
+      let value = data[key];
 
-        // If we're not allowed to include private data, let's filter out hosts
-        // based on the given tab's pinned state and the privacy level.
-        for (let host of Object.keys(data.storage)) {
-          let isHttps = host.startsWith("https:");
-          if (PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
-            storage[host] = data.storage[host];
-          }
+      // Filter sensitive data according to the current privacy level.
+      if (!includePrivateData) {
+        if (key === "storage") {
+          value = PrivacyLevelFilter.filterSessionStorageData(value, tab.pinned);
+        } else if (key === "formdata") {
+          value = PrivacyLevelFilter.filterFormData(value, tab.pinned);
         }
+      }
 
-        if (Object.keys(storage).length) {
-          tabData.storage = storage;
-        }
+      if (value) {
+        tabData[key] = value;
       }
     }
   },
 
   /*
    * Returns true if the xul:tab element is newly added (i.e., if it's
    * showing about:blank with no history).
    */
deleted file mode 100644
--- a/browser/components/sessionstore/src/TextAndScrollData.jsm
+++ /dev/null
@@ -1,145 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["TextAndScrollData"];
-
-const Cu = Components.utils;
-const Ci = Components.interfaces;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
-  "resource:///modules/sessionstore/DocumentUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
-  "resource:///modules/sessionstore/PrivacyLevel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
-  "resource:///modules/sessionstore/ScrollPosition.jsm");
-
-/**
- * The external API exported by this module.
- */
-this.TextAndScrollData = Object.freeze({
-  updateFrame: function (entry, content, isPinned, options) {
-    return TextAndScrollDataInternal.updateFrame(entry, content, isPinned, options);
-  },
-
-  restore: function (frameList) {
-    TextAndScrollDataInternal.restore(frameList);
-  },
-});
-
-let TextAndScrollDataInternal = {
-  /**
-   * Go through all subframes and store all form data, the current
-   * scroll positions and innerHTML content of WYSIWYG editors.
-   *
-   * @param entry
-   *        the object into which to store the collected data
-   * @param content
-   *        frame reference
-   * @param isPinned
-   *        the tab is pinned and should be treated differently for privacy
-   * @param includePrivateData
-   *        {includePrivateData:true} include privacy sensitive data (use with care)
-   */
-  updateFrame: function (entry, content, isPinned, options = null) {
-    let includePrivateData = options && options.includePrivateData;
-
-    for (let i = 0; i < content.frames.length; i++) {
-      if (entry.children && entry.children[i]) {
-        this.updateFrame(entry.children[i], content.frames[i], includePrivateData, isPinned);
-      }
-    }
-
-    let href = (content.parent || content).document.location.href;
-    let isHttps = Services.io.newURI(href, null, null).schemeIs("https");
-    let topURL = content.top.document.location.href;
-    let isAboutSR = this.isAboutSessionRestore(topURL);
-    if (includePrivateData || isAboutSR ||
-        PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
-      let formData = DocumentUtils.getFormData(content.document);
-
-      // We want to avoid saving data for about:sessionrestore as a string.
-      // Since it's stored in the form as stringified JSON, stringifying further
-      // causes an explosion of escape characters. cf. bug 467409
-      if (formData && isAboutSR) {
-        formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
-      }
-
-      if (Object.keys(formData.id).length ||
-          Object.keys(formData.xpath).length) {
-        entry.formdata = formData;
-      }
-
-      // designMode is undefined e.g. for XUL documents (as about:config)
-      if ((content.document.designMode || "") == "on" && content.document.body) {
-        entry.innerHTML = content.document.body.innerHTML;
-      }
-    }
-  },
-
-  isAboutSessionRestore: function (url) {
-    return url == "about:sessionrestore" || url == "about:welcomeback";
-  },
-
-  restore: function (frameList) {
-    for (let [frame, data] of frameList) {
-      this.restoreFrame(frame, data);
-    }
-  },
-
-  restoreFrame: function (content, data) {
-    if (data.formdata) {
-      let formdata = data.formdata;
-
-      // handle backwards compatibility
-      // this is a migration from pre-firefox 15. cf. bug 742051
-      if (!("xpath" in formdata || "id" in formdata)) {
-        formdata = { xpath: {}, id: {} };
-
-        for each (let [key, value] in Iterator(data.formdata)) {
-          if (key.charAt(0) == "#") {
-            formdata.id[key.slice(1)] = value;
-          } else {
-            formdata.xpath[key] = value;
-          }
-        }
-      }
-
-      // for about:sessionrestore we saved the field as JSON to avoid
-      // nested instances causing humongous sessionstore.js files.
-      // cf. bug 467409
-      if (this.isAboutSessionRestore(data.url) &&
-          "sessionData" in formdata.id &&
-          typeof formdata.id["sessionData"] == "object") {
-        formdata.id["sessionData"] = JSON.stringify(formdata.id["sessionData"]);
-      }
-
-      // update the formdata
-      data.formdata = formdata;
-      // merge the formdata
-      DocumentUtils.mergeFormData(content.document, formdata);
-    }
-
-    if (data.innerHTML) {
-      // We know that the URL matches data.url right now, but the user
-      // may navigate away before the setTimeout handler runs. We do
-      // a simple comparison against savedURL to check for that.
-      let savedURL = content.document.location.href;
-
-      setTimeout(function() {
-        if (content.document.designMode == "on" &&
-            content.document.location.href == savedURL &&
-            content.document.body) {
-          content.document.body.innerHTML = data.innerHTML;
-        }
-      }, 0);
-    }
-
-    ScrollPosition.restore(content, data.scroll || "");
-  },
-};
--- a/browser/components/sessionstore/src/moz.build
+++ b/browser/components/sessionstore/src/moz.build
@@ -10,33 +10,34 @@ EXTRA_COMPONENTS += [
     'nsSessionStore.manifest',
 ]
 
 JS_MODULES_PATH = 'modules/sessionstore'
 
 EXTRA_JS_MODULES = [
     'ContentRestore.jsm',
     'DocShellCapabilities.jsm',
-    'DocumentUtils.jsm',
+    'FormData.jsm',
     'FrameTree.jsm',
+    'GlobalState.jsm',
     'Messenger.jsm',
     'PageStyle.jsm',
     'PrivacyLevel.jsm',
+    'PrivacyLevelFilter.jsm',
     'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
     'ScrollPosition.jsm',
     'SessionCookies.jsm',
     'SessionFile.jsm',
     'SessionHistory.jsm',
     'SessionMigration.jsm',
     'SessionStorage.jsm',
     'SessionWorker.js',
     'TabAttributes.jsm',
     'TabState.jsm',
     'TabStateCache.jsm',
-    'TextAndScrollData.jsm',
     'Utils.jsm',
     'XPathGenerator.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'SessionSaver.jsm',
     'SessionStore.jsm',
 ]
--- a/browser/components/sessionstore/src/nsSessionStartup.js
+++ b/browser/components/sessionstore/src/nsSessionStartup.js
@@ -9,22 +9,20 @@
  *
  * Overview
  * This service reads user's session file at startup, and makes a determination
  * as to whether the session should be restored. It will restore the session
  * under the circumstances described below.  If the auto-start Private Browsing
  * mode is active, however, the session is never restored.
  *
  * Crash Detection
- * The session file stores a session.state property, that
- * indicates whether the browser is currently running. When the browser shuts
- * down, the field is changed to "stopped". At startup, this field is read, and
- * if its value is "running", then it's assumed that the browser had previously
- * crashed, or at the very least that something bad happened, and that we should
- * restore the session.
+ * The CrashMonitor is used to check if the final session state was successfully
+ * written at shutdown of the last session. If we did not reach
+ * 'sessionstore-final-state-write-complete', then it's assumed that the browser
+ * has previously crashed and we should restore the session.
  *
  * Forced Restarts
  * In the event that a restart is required due to application update or extension
  * installation, set the browser.sessionstore.resume_session_once pref to true,
  * and the session will be restored the next time the browser starts.
  *
  * Always Resume
  * This service will always resume the session if the integer pref
@@ -42,16 +40,18 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/devtools/Console.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
   "resource:///modules/sessionstore/SessionFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
+  "resource://gre/modules/CrashMonitor.jsm");
 
 const STATE_RUNNING_STR = "running";
 
 // 'browser.startup.page' preference value to resume the previous session.
 const BROWSER_STARTUP_RESUME_SESSION = 3;
 
 function debug(aMsg) {
   aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n");
@@ -67,16 +67,19 @@ function SessionStartup() {
 
 SessionStartup.prototype = {
 
   // the state to restore at startup
   _initialState: null,
   _sessionType: Ci.nsISessionStartup.NO_SESSION,
   _initialized: false,
 
+  // Stores whether the previous session crashed.
+  _previousSessionCrashed: null,
+
 /* ........ Global Event Handlers .............. */
 
   /**
    * Initialize the component
    */
   init: function sss_init() {
     // do not need to initialize anything in auto-started private browsing sessions
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
@@ -94,97 +97,126 @@ SessionStartup.prototype = {
   // Wrap a string as a nsISupports
   _createSupportsString: function ssfi_createSupportsString(aData) {
     let string = Cc["@mozilla.org/supports-string;1"]
                    .createInstance(Ci.nsISupportsString);
     string.data = aData;
     return string;
   },
 
-  _onSessionFileRead: function sss_onSessionFileRead(aStateString) {
-    if (this._initialized) {
-      // Initialization is complete, nothing else to do
+  /**
+   * Complete initialization once the Session File has been read
+   *
+   * @param stateString
+   *        string The Session State string read from disk
+   */
+  _onSessionFileRead: function (stateString) {
+    this._initialized = true;
+
+    // Let observers modify the state before it is used
+    let supportsStateString = this._createSupportsString(stateString);
+    Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
+    stateString = supportsStateString.data;
+
+    // No valid session found.
+    if (!stateString) {
+      this._sessionType = Ci.nsISessionStartup.NO_SESSION;
+      Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
+      gOnceInitializedDeferred.resolve();
       return;
     }
-    try {
-      this._initialized = true;
+
+    this._initialState =  this._parseStateString(stateString);
 
-      // Let observers modify the state before it is used
-      let supportsStateString = this._createSupportsString(aStateString);
-      Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
-      aStateString = supportsStateString.data;
+    let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
+    let shouldResumeSession = shouldResumeSessionOnce ||
+          Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
+
+    // If this is a normal restore then throw away any previous session
+    if (!shouldResumeSessionOnce)
+      delete this._initialState.lastSessionState;
+
+    let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
 
-      // No valid session found.
-      if (!aStateString) {
-        this._sessionType = Ci.nsISessionStartup.NO_SESSION;
-        return;
-      }
+    CrashMonitor.previousCheckpoints.then(checkpoints => {
+      if (checkpoints) {
+        // If the previous session finished writing the final state, we'll
+        // assume there was no crash.
+        this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"];
+      } else {
+        // If the Crash Monitor could not load a checkpoints file it will
+        // provide null. This could occur on the first run after updating to
+        // a version including the Crash Monitor, or if the checkpoints file
+        // was removed.
+        //
+        // If this is the first run after an update, sessionstore.js should
+        // still contain the session.state flag to indicate if the session
+        // crashed. If it is not present, we will assume this was not the first
+        // run after update and the checkpoints file was somehow corrupted or
+        // removed by a crash.
+        //
+        // If the session.state flag is present, we will fallback to using it
+        // for crash detection - If the last write of sessionstore.js had it
+        // set to "running", we crashed.
+        let stateFlagPresent = (this._initialState &&
+                                this._initialState.session &&
+                                this._initialState.session.state);
 
-      // parse the session state into a JS object
-      // remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0)
-      if (aStateString.charAt(0) == '(')
-        aStateString = aStateString.slice(1, -1);
-      let corruptFile = false;
-      try {
-        this._initialState = JSON.parse(aStateString);
+
+        this._previousSessionCrashed = !stateFlagPresent ||
+                                       (this._initialState.session.state == STATE_RUNNING_STR);
       }
-      catch (ex) {
-        debug("The session file contained un-parse-able JSON: " + ex);
-        // This is not valid JSON, but this might still be valid JavaScript,
-        // as used in FF2/FF3, so we need to eval.
-        // evalInSandbox will throw if aStateString is not parse-able.
-        try {
-          var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'});
-          this._initialState = Cu.evalInSandbox("(" + aStateString + ")", s);
-        } catch(ex) {
-          debug("The session file contained un-eval-able JSON: " + ex);
-          corruptFile = true;
-        }
-      }
-      Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
-
-      let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
-      let doResumeSession = doResumeSessionOnce ||
-            Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
-
-      // If this is a normal restore then throw away any previous session
-      if (!doResumeSessionOnce)
-        delete this._initialState.lastSessionState;
-
-      let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
-      let lastSessionCrashed =
-        this._initialState && this._initialState.session &&
-        this._initialState.session.state &&
-        this._initialState.session.state == STATE_RUNNING_STR;
 
       // Report shutdown success via telemetry. Shortcoming here are
       // being-killed-by-OS-shutdown-logic, shutdown freezing after
       // session restore was written, etc.
-      Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!lastSessionCrashed);
+      Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed);
 
       // set the startup type
-      if (lastSessionCrashed && resumeFromCrash)
+      if (this._previousSessionCrashed && resumeFromCrash)
         this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
-      else if (!lastSessionCrashed && doResumeSession)
+      else if (!this._previousSessionCrashed && shouldResumeSession)
         this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
       else if (this._initialState)
         this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
       else
         this._initialState = null; // reset the state
 
       Services.obs.addObserver(this, "sessionstore-windows-restored", true);
 
       if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
         Services.obs.addObserver(this, "browser:purge-session-history", true);
 
-    } finally {
       // We're ready. Notify everyone else.
       Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
       gOnceInitializedDeferred.resolve();
+    });
+  },
+
+
+  /**
+   * Convert the Session State string into a state object
+   *
+   * @param stateString
+   *        string The Session State string read from disk
+   * @returns {State} a Session State object
+   */
+  _parseStateString: function (stateString) {
+    let state = null;
+    let corruptFile = false;
+
+    try {
+      state = JSON.parse(stateString);
+    } catch (ex) {
+      debug("The session file contained un-parse-able JSON: " + ex);
+      corruptFile = true;
     }
+    Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
+
+    return state;
   },
 
   /**
    * Handle notifications
    */
   observe: function sss_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
     case "app-startup":
@@ -287,16 +319,24 @@ SessionStartup.prototype = {
   /**
    * Get the type of pending session store, if any.
    */
   get sessionType() {
     this._ensureInitialized();
     return this._sessionType;
   },
 
+  /**
+   * Get whether the previous session crashed.
+   */
+  get previousSessionCrashed() {
+    this._ensureInitialized();
+    return this._previousSessionCrashed;
+  },
+
   // Ensure that initialization is complete. If initialization is not complete
   // yet, something is attempting to use the old synchronous initialization,
   // throw an error.
   _ensureInitialized: function sss__ensureInitialized() {
     if (!this._initialized) {
       throw new Error("Session Store is not initialized.");
     }
   },
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -6,67 +6,67 @@
 # browser_526613.js is disabled because of frequent failures (bug 534489)
 # browser_589246.js is disabled for leaking browser windows (bug 752467)
 # browser_580512.js is disabled for leaking browser windows (bug 752467)
 
 [DEFAULT]
 support-files =
   head.js
   content.js
+  content-forms.js
+  browser_formdata_sample.html
+  browser_formdata_xpath_sample.html
   browser_frametree_sample.html
   browser_frametree_sample_frameset.html
   browser_form_restore_events_sample.html
   browser_formdata_format_sample.html
-  browser_input_sample.html
   browser_pageStyle_sample.html
   browser_pageStyle_sample_nested.html
   browser_scrollPositions_sample.html
   browser_scrollPositions_sample_frameset.html
   browser_sessionStorage.html
   browser_248970_b_sample.html
   browser_339445_sample.html
-  browser_346337_sample.html
   browser_423132_sample.html
   browser_447951_sample.html
   browser_454908_sample.html
   browser_456342_sample.xhtml
-  browser_463205_helper.html
   browser_463205_sample.html
   browser_463206_sample.html
   browser_466937_sample.html
   browser_485482_sample.html
   browser_597315_index.html
   browser_597315_a.html
   browser_597315_b.html
   browser_597315_c.html
   browser_597315_c1.html
   browser_597315_c2.html
   browser_662743_sample.html
   browser_739531_sample.html
-  browser_916390_sample.html
 
 #NB: the following are disabled
 #  browser_464620_a.html
 #  browser_464620_b.html 
 #  browser_464620_xd.html
 
 
 #disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html
 #disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html
 #disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html
 
 [browser_attributes.js]
 [browser_broadcast.js]
 [browser_capabilities.js]
 [browser_dying_cache.js]
 [browser_form_restore_events.js]
+[browser_formdata.js]
 [browser_formdata_format.js]
+[browser_formdata_xpath.js]
 [browser_frametree.js]
 [browser_global_store.js]
-[browser_input.js]
 [browser_merge_closed_tabs.js]
 [browser_pageshow.js]
 [browser_pageStyle.js]
 [browser_privatetabs.js]
 [browser_scrollPositions.js]
 [browser_sessionStorage.js]
 [browser_swapDocShells.js]
 [browser_tabStateCache.js]
@@ -167,17 +167,16 @@ skip-if = true
 [browser_701377.js]
 [browser_705597.js]
 [browser_707862.js]
 [browser_739531.js]
 [browser_739805.js]
 [browser_819510_perwindowpb.js]
 skip-if = os == "linux" # Intermittent failures, bug 894063
 [browser_833286_atomic_backup.js]
-[browser_916390_form_data_loss.js]
 
 # Disabled for frequent intermittent failures
 [browser_464620_a.js]
 skip-if = true
 [browser_464620_b.js]
 skip-if = true
 
 # Disabled on OS X:
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_346337.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* 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/. */
-
-function test() {
-  /** Test for Bug 346337 **/
-
-  var file = Components.classes["@mozilla.org/file/directory_service;1"]
-               .getService(Components.interfaces.nsIProperties)
-               .get("TmpD", Components.interfaces.nsILocalFile);
-  file.append("346337_test1.file");
-  file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
-  var filePath1 = file.path;
-  file = Components.classes["@mozilla.org/file/directory_service;1"]
-             .getService(Components.interfaces.nsIProperties)
-             .get("TmpD", Components.interfaces.nsILocalFile);
-  file.append("346337_test2.file");
-  file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
-  var filePath2 = file.path;
-
-  let fieldList = {
-    "//input[@name='input']":     Date.now().toString(),
-    "//input[@name='spaced 1']":  Math.random().toString(),
-    "//input[3]":                 "three",
-    "//input[@type='checkbox']":  true,
-    "//input[@name='uncheck']":   false,
-    "//input[@type='radio'][1]":  false,
-    "//input[@type='radio'][2]":  true,
-    "//input[@type='radio'][3]":  false,
-    "//select":                   2,
-    "//select[@multiple]":        [1, 3],
-    "//textarea[1]":              "",
-    "//textarea[2]":              "Some text... " + Math.random(),
-    "//textarea[3]":              "Some more text\n" + new Date(),
-    "//input[@type='file'][1]":   [filePath1],
-    "//input[@type='file'][2]":   [filePath1, filePath2]
-  };
-
-  function getElementByXPath(aTab, aQuery) {
-    let doc = aTab.linkedBrowser.contentDocument;
-    let xptype = Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE;
-    return doc.evaluate(aQuery, doc, null, xptype, null).singleNodeValue;
-  }
-
-  function setFormValue(aTab, aQuery, aValue) {
-    let node = getElementByXPath(aTab, aQuery);
-    if (typeof aValue == "string")
-      node.value = aValue;
-    else if (typeof aValue == "boolean")
-      node.checked = aValue;
-    else if (typeof aValue == "number")
-      node.selectedIndex = aValue;
-    else if (node instanceof Ci.nsIDOMHTMLInputElement && node.type == "file")
-      node.mozSetFileNameArray(aValue, aValue.length);
-    else
-      Array.forEach(node.options, function(aOpt, aIx)
-                                    (aOpt.selected = aValue.indexOf(aIx) > -1));
-  }
-
-  function compareFormValue(aTab, aQuery, aValue) {
-    let node = getElementByXPath(aTab, aQuery);
-    if (!node)
-      return false;
-    if (node instanceof Ci.nsIDOMHTMLInputElement) {
-      if (node.type == "file") {
-        let fileNames = node.mozGetFileNameArray();
-        return fileNames.length == aValue.length &&
-               Array.every(fileNames, function(aFile) aValue.indexOf(aFile) >= 0);
-      }
-      return aValue == (node.type == "checkbox" || node.type == "radio" ?
-                        node.checked : node.value);
-    }
-    if (node instanceof Ci.nsIDOMHTMLTextAreaElement)
-      return aValue == node.value;
-    if (!node.multiple)
-      return aValue == node.selectedIndex;
-    return Array.every(node.options, function(aOpt, aIx)
-                                       (aValue.indexOf(aIx) > -1) == aOpt.selected);
-  }
-
-  // test setup
-  let tabbrowser = gBrowser;
-  waitForExplicitFinish();
-
-  // make sure we don't save form data at all (except for tab duplication)
-  gPrefService.setIntPref("browser.sessionstore.privacy_level", 2);
-
-  let rootDir = getRootDirectory(gTestPath);
-  let testURL = rootDir + "browser_346337_sample.html";
-  let tab = tabbrowser.addTab(testURL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    for (let xpath in fieldList)
-      setFormValue(tab, xpath, fieldList[xpath]);
-
-    let tab2 = tabbrowser.duplicateTab(tab);
-    whenTabRestored(tab2, function() {
-      for (let xpath in fieldList)
-        ok(compareFormValue(tab2, xpath, fieldList[xpath]),
-           "The value for \"" + xpath + "\" was correctly restored");
-
-      // clean up
-      tabbrowser.removeTab(tab2);
-      tabbrowser.removeTab(tab);
-
-      tab = undoCloseTab();
-      whenTabRestored(tab, function() {
-        for (let xpath in fieldList)
-          if (fieldList[xpath])
-            ok(!compareFormValue(tab, xpath, fieldList[xpath]),
-               "The value for \"" + xpath + "\" was correctly discarded");
-
-        if (gPrefService.prefHasUserValue("browser.sessionstore.privacy_level"))
-          gPrefService.clearUserPref("browser.sessionstore.privacy_level");
-        // undoCloseTab can reuse a single blank tab, so we have to
-        // make sure not to close the window when closing our last tab
-        if (tabbrowser.tabs.length == 1)
-          tabbrowser.addTab();
-        tabbrowser.removeTab(tab);
-        finish();
-      });
-    });
-  });
-}
--- a/browser/components/sessionstore/test/browser_393716.js
+++ b/browser/components/sessionstore/test/browser_393716.js
@@ -1,72 +1,70 @@
-function test() {
-  /** Test for Bug 393716 **/
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
 
-  waitForExplicitFinish();
+const URL = "about:config";
 
-  /////////////////
-  // getTabState //
-  /////////////////
+/**
+ * Bug 393716 - Basic tests for getTabState(), setTabState(), and duplicateTab().
+ */
+add_task(function test_set_tabstate() {
   let key = "Unique key: " + Date.now();
   let value = "Unique value: " + Math.random();
-  let testURL = "about:config";
 
   // create a new tab
-  let tab = gBrowser.addTab(testURL);
+  let tab = gBrowser.addTab(URL);
   ss.setTabValue(tab, key, value);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    // get the tab's state
-    let state = ss.getTabState(tab);
-    ok(state, "get the tab's state");
+  yield promiseBrowserLoaded(tab.linkedBrowser);
+
+  // get the tab's state
+  let state = ss.getTabState(tab);
+  ok(state, "get the tab's state");
 
-    // verify the tab state's integrity
-    state = JSON.parse(state);
-    ok(state instanceof Object && state.entries instanceof Array && state.entries.length > 0,
-       "state object seems valid");
-    ok(state.entries.length == 1 && state.entries[0].url == testURL,
-       "Got the expected state object (test URL)");
-    ok(state.extData && state.extData[key] == value,
-       "Got the expected state object (test manually set tab value)");
+  // verify the tab state's integrity
+  state = JSON.parse(state);
+  ok(state instanceof Object && state.entries instanceof Array && state.entries.length > 0,
+     "state object seems valid");
+  ok(state.entries.length == 1 && state.entries[0].url == URL,
+     "Got the expected state object (test URL)");
+  ok(state.extData && state.extData[key] == value,
+     "Got the expected state object (test manually set tab value)");
 
-    // clean up
-    gBrowser.removeTab(tab);
-  });
+  // clean up
+  gBrowser.removeTab(tab);
+});
 
-  //////////////////////////////////
-  // setTabState and duplicateTab //
-  //////////////////////////////////
+add_task(function test_set_tabstate_and_duplicate() {
   let key2 = "key2";
   let value2 = "Value " + Math.random();
   let value3 = "Another value: " + Date.now();
-  let state = { entries: [{ url: testURL }], extData: { key2: value2 } };
+  let state = { entries: [{ url: URL }], extData: { key2: value2 } };
 
   // create a new tab
-  let tab2 = gBrowser.addTab();
+  let tab = gBrowser.addTab();
   // set the tab's state
-  ss.setTabState(tab2, JSON.stringify(state));
-  whenTabRestored(tab2, function() {
-    // verify the correctness of the restored tab
-    ok(ss.getTabValue(tab2, key2) == value2 && tab2.linkedBrowser.currentURI.spec == testURL,
-       "the tab's state was correctly restored");
+  ss.setTabState(tab, JSON.stringify(state));
+  yield promiseBrowserLoaded(tab.linkedBrowser);
 
-    // add text data
-    let textbox = tab2.linkedBrowser.contentDocument.getElementById("textbox");
-    textbox.value = value3;
+  // verify the correctness of the restored tab
+  ok(ss.getTabValue(tab, key2) == value2 && tab.linkedBrowser.currentURI.spec == URL,
+     "the tab's state was correctly restored");
+
+  // add text data
+  yield setInputValue(tab.linkedBrowser, {id: "textbox", value: value3});
 
-    // duplicate the tab
-    let duplicateTab = ss.duplicateTab(window, tab2);
-    gBrowser.removeTab(tab2);
+  // duplicate the tab
+  let tab2 = ss.duplicateTab(window, tab);
+  yield promiseTabRestored(tab2);
 
-    whenTabRestored(duplicateTab, function() {
-      // verify the correctness of the duplicated tab
-      ok(ss.getTabValue(duplicateTab, key2) == value2 &&
-         duplicateTab.linkedBrowser.currentURI.spec == testURL,
-         "correctly duplicated the tab's state");
-      let textbox = duplicateTab.linkedBrowser.contentDocument.getElementById("textbox");
-      is(textbox.value, value3, "also duplicated text data");
+  // verify the correctness of the duplicated tab
+  ok(ss.getTabValue(tab2, key2) == value2 &&
+     tab2.linkedBrowser.currentURI.spec == URL,
+     "correctly duplicated the tab's state");
+  let textbox = yield getInputValue(tab2.linkedBrowser, {id: "textbox"});
+  is(textbox, value3, "also duplicated text data");
 
-      // clean up
-      gBrowser.removeTab(duplicateTab);
-      finish();
-    });
-  });
-}
+  // clean up
+  gBrowser.removeTab(tab2);
+  gBrowser.removeTab(tab);
+});
--- a/browser/components/sessionstore/test/browser_394759_basic.js
+++ b/browser/components/sessionstore/test/browser_394759_basic.js
@@ -1,12 +1,14 @@
 /* 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/. */
 
+"use strict";
+
 const TEST_URL = "data:text/html;charset=utf-8,<input%20id=txt>" +
                  "<input%20type=checkbox%20id=chk>";
 
 /**
  * This test ensures that closing a window is a reversible action. We will
  * close the the window, restore it and check that all data has been restored.
  * This includes window-specific data as well as form data for tabs.
  */
@@ -25,21 +27,18 @@ function test() {
   provideWindow(function onTestURLLoaded(newWin) {
     newWin.gBrowser.addTab().linkedBrowser.stop();
 
     // mark the window with some unique data to be restored later on
     ss.setWindowValue(newWin, uniqueKey, uniqueValue);
     let [txt, chk] = newWin.content.document.querySelectorAll("#txt, #chk");
     txt.value = uniqueText;
 
-    // Toggle the checkbox to cause a SessionStore:input message to be sent.
-    EventUtils.sendMouseEvent({type: "click"}, chk);
-
     let browser = newWin.gBrowser.selectedBrowser;
-    promiseContentMessage(browser, "SessionStore:input").then(() => {
+    setInputChecked(browser, {id: "chk", checked: true}).then(() => {
       newWin.close();
 
       // Now give it time to close
       executeSoon(function() {
         is(ss.getClosedWindowCount(), 1,
            "The closed window was added to Recently Closed Windows");
         let data = JSON.parse(ss.getClosedWindowData())[0];
         ok(data.title == TEST_URL && JSON.stringify(data).indexOf(uniqueText) > -1,
@@ -80,8 +79,12 @@ function test() {
           // clean up
           newWin2.close();
           finish();
         }, true);
       });
     });
   }, TEST_URL);
 }
+
+function setInputChecked(browser, data) {
+  return sendMessage(browser, "ss-test:setInputChecked", data);
+}
--- a/browser/components/sessionstore/test/browser_454908.js
+++ b/browser/components/sessionstore/test/browser_454908.js
@@ -1,50 +1,53 @@
-/* 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/. */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
 
-function test() {
-  /** Test for Bug 454908 **/
+let tmp = {};
+Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", tmp);
+let {SessionSaver} = tmp;
 
-  waitForExplicitFinish();
+const URL = ROOT + "browser_454908_sample.html";
+const PASS = "pwd-" + Math.random();
 
-  let fieldValues = {
-    username: "User " + Math.random(),
-    passwd:   "pwd" + Date.now()
-  };
-
-  // make sure we do save form data
-  gPrefService.setIntPref("browser.sessionstore.privacy_level", 0);
+/**
+ * Bug 454908 - Don't save/restore values of password fields.
+ */
+add_task(function test_dont_save_passwords() {
+  // Make sure we do save form data.
+  Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
 
-  let rootDir = getRootDirectory(gTestPath);
-  let testURL = rootDir + "browser_454908_sample.html";
-  let tab = gBrowser.addTab(testURL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    let doc = tab.linkedBrowser.contentDocument;
-    for (let id in fieldValues)
-      doc.getElementById(id).value = fieldValues[id];
+  // Add a tab with a password field.
+  let tab = gBrowser.addTab(URL);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
 
-    gBrowser.removeTab(tab);
+  // Fill in some values.
+  let usernameValue = "User " + Math.random();
+  yield setInputValue(browser, {id: "username", value: usernameValue});
+  yield setInputValue(browser, {id: "passwd", value: PASS});
 
-    tab = undoCloseTab();
-    whenTabRestored(tab, function() {
-      let doc = tab.linkedBrowser.contentDocument;
-      for (let id in fieldValues) {
-        let node = doc.getElementById(id);
-        if (node.type == "password")
-          is(node.value, "", "password wasn't saved/restored");
-        else
-          is(node.value, fieldValues[id], "username was saved/restored");
-      }
+  // Close and restore the tab.
+  gBrowser.removeTab(tab);
+  tab = ss.undoCloseTab(window, 0);
+  browser = tab.linkedBrowser;
+  yield promiseTabRestored(tab);
 
-      // clean up
-      if (gPrefService.prefHasUserValue("browser.sessionstore.privacy_level"))
-        gPrefService.clearUserPref("browser.sessionstore.privacy_level");
-      // undoCloseTab can reuse a single blank tab, so we have to
-      // make sure not to close the window when closing our last tab
-      if (gBrowser.tabs.length == 1)
-        gBrowser.addTab();
-      gBrowser.removeTab(tab);
-      finish();
-    });
-  });
-}
+  // Check that password fields aren't saved/restored.
+  let username = yield getInputValue(browser, {id: "username"});
+  is(username, usernameValue, "username was saved/restored");
+  let passwd = yield getInputValue(browser, {id: "passwd"});
+  is(passwd, "", "password wasn't saved/restored");
+
+  // Write to disk and read our file.
+  yield SessionSaver.run();
+  let path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
+  let data = yield OS.File.read(path);
+  let state = new TextDecoder().decode(data);
+
+  // Ensure that sessionstore.js doesn't contain our password.
+  is(state.indexOf(PASS), -1, "password has not been written to disk");
+
+  // Cleanup.
+  gBrowser.removeTab(tab);
+});
--- a/browser/components/sessionstore/test/browser_456342.js
+++ b/browser/components/sessionstore/test/browser_456342.js
@@ -1,51 +1,49 @@
-/* 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/. */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  /** Test for Bug 456342 **/
+"use strict";
 
-  waitForExplicitFinish();
-
-  // make sure we do save form data
-  gPrefService.setIntPref("browser.sessionstore.privacy_level", 0);
+const URL = ROOT + "browser_456342_sample.xhtml";
 
-  let rootDir = getRootDirectory(gTestPath);
-  let testURL = rootDir + "browser_456342_sample.xhtml";
-  let tab = gBrowser.addTab(testURL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    let expectedValue = "try to save me";
-    // Since bug 537289 we only save non-default values, so we need to set each
-    // form field's value after load.
-    let formEls = tab.linkedBrowser.contentDocument.forms[0].elements;
-    for (let i = 0; i < formEls.length; i++)
-      formEls[i].value = expectedValue;
+/**
+ * Bug 456342 - Restore values from non-standard input field types.
+ */
+add_task(function test_restore_nonstandard_input_values() {
+  // Add tab with various non-standard input field types.
+  let tab = gBrowser.addTab(URL);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
 
-    gBrowser.removeTab(tab);
+  // Fill in form values.
+  let expectedValue = Math.random();
+  yield setFormElementValues(browser, {value: expectedValue});
+
+  // Remove tab and check collected form data.
+  gBrowser.removeTab(tab);
+  let undoItems = JSON.parse(ss.getClosedTabData(window));
+  let savedFormData = undoItems[0].state.formdata;
 
-    let undoItems = JSON.parse(ss.getClosedTabData(window));
-    let savedFormData = undoItems[0].state.entries[0].formdata;
-
-    let countGood = 0, countBad = 0;
-    for each (let value in savedFormData.id) {
-      if (value == expectedValue)
-        countGood++;
-      else
-        countBad++;
+  let countGood = 0, countBad = 0;
+  for (let id of Object.keys(savedFormData.id)) {
+    if (savedFormData.id[id] == expectedValue) {
+      countGood++;
+    } else {
+      countBad++;
     }
-    for each (let value in savedFormData.xpath) {
-      if (value == expectedValue)
-        countGood++;
-      else
-        countBad++;
-    }
+  }
 
-    is(countGood, 4, "Saved text for non-standard input fields");
-    is(countBad,  0, "Didn't save text for ignored field types");
+  for (let exp of Object.keys(savedFormData.xpath)) {
+    if (savedFormData.xpath[exp] == expectedValue) {
+      countGood++;
+    } else {
+      countBad++;
+    }
+  }
 
-    // clean up
-    if (gPrefService.prefHasUserValue("browser.sessionstore.privacy_level"))
-      gPrefService.clearUserPref("browser.sessionstore.privacy_level");
-    finish();
-  });
+  is(countGood, 4, "Saved text for non-standard input fields");
+  is(countBad,  0, "Didn't save text for ignored field types");
+});
+
+function setFormElementValues(browser, data) {
+  return sendMessage(browser, "ss-test:setFormElementValues", data);
 }
--- a/browser/components/sessionstore/test/browser_463205.js
+++ b/browser/components/sessionstore/test/browser_463205.js
@@ -1,123 +1,42 @@
-/* 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/. */
-
-function test() {
-  /** Test for Bug 463205 **/
-
-  waitForExplicitFinish();
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-  let rootDir = "http://mochi.test:8888/browser/browser/components/sessionstore/test/";
-  let testURL = rootDir + "browser_463205_sample.html";
-
-  let doneURL = "done";
+"use strict";
 
-  let mainURL = testURL;
-  let frame1URL = "data:text/html;charset=utf-8,<input%20id='original'>";
-  let frame2URL = rootDir + "browser_463205_helper.html";
-  let frame3URL = "data:text/html;charset=utf-8,mark2";
-
-  let frameCount = 0;
+const URL = ROOT + "browser_463205_sample.html";
 
-  let tab = gBrowser.addTab(testURL);
-  tab.linkedBrowser.addEventListener("load", function(aEvent) {
-    // wait for all frames to load completely
-    if (frame1URL != doneURL && aEvent.target.location.href == frame1URL) {
-      frame1URL = doneURL;
-      if (frameCount++ < 3) {
-        return;
-      }
-    }
-    if (frame2URL != doneURL && aEvent.target.location.href == frame2URL) {
-      frame2URL = doneURL;
-      if (frameCount++ < 3) {
-        return;
-      }
-    }
-    if (frame3URL != doneURL && aEvent.target.location.href == frame3URL) {
-      frame3URL = doneURL;
-      if (frameCount++ < 3) {
-        return;
-      }
-    }
-    if (mainURL != doneURL && aEvent.target.location.href == mainURL) {
-      mainURL = doneURL;
-      if (frameCount++ < 3) {
-        return;
-      }
-    }
-    if (frameCount < 3) {
-      return;
-    }
-    tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
-
-    function typeText(aTextField, aValue) {
-      aTextField.value = aValue;
-
-      let event = aTextField.ownerDocument.createEvent("UIEvents");
-      event.initUIEvent("input", true, true, aTextField.ownerDocument.defaultView, 0);
-      aTextField.dispatchEvent(event);
-    }
+/**
+ * Bug 463205 - Check URLs before restoring form data to make sure a malicious
+ * website can't modify frame URLs and make us inject form data into the wrong
+ * web pages.
+ */
+add_task(function test_check_urls_before_restoring() {
+  // Add a blank tab.
+  let tab = gBrowser.addTab("about:blank");
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
 
-    let uniqueValue = "Unique: " + Math.random();
-    let win = tab.linkedBrowser.contentWindow;
-    typeText(win.frames[0].document.getElementById("original"), uniqueValue);
-    typeText(win.frames[1].document.getElementById("original"), uniqueValue);
+  // Restore form data with a valid URL.
+  ss.setTabState(tab, getState(URL));
+  yield promiseTabRestored(tab);
 
-    mainURL = testURL;
-    frame1URL = "http://mochi.test:8888/browser/" +
-      "browser/components/sessionstore/test/browser_463205_helper.html";
-    frame2URL = rootDir + "browser_463205_helper.html";
-    frame3URL = "data:text/html;charset=utf-8,mark2";
+  let value = yield getInputValue(browser, {id: "text"});
+  is(value, "foobar", "value was restored");
 
-    frameCount = 0;
+  // Restore form data with an invalid URL.
+  ss.setTabState(tab, getState("http://example.com/"));
+  yield promiseTabRestored(tab);
 
-    let tab2 = gBrowser.duplicateTab(tab);
-    tab2.linkedBrowser.addEventListener("load", function(aEvent) {
-      // wait for all frames to load (and reload!) completely
-      if (frame1URL != doneURL && aEvent.target.location.href == frame1URL) {
-        frame1URL = doneURL;
-        if (frameCount++ < 3) {
-          return;
-        }
-      }
-      if (frame2URL != doneURL && (aEvent.target.location.href == frame2URL ||
-          aEvent.target.location.href == frame2URL + "#original")) {
-        frame2URL = doneURL;
-        if (frameCount++ < 3) {
-          return;
-        }
-      }
-      if (frame3URL != doneURL && aEvent.target.location.href == frame3URL) {
-        frame3URL = doneURL;
-        if (frameCount++ < 3) {
-          return;
-        }
-      }
-      if (mainURL != doneURL && aEvent.target.location.href == mainURL) {
-        mainURL = doneURL;
-        if (frameCount++ < 3) {
-          return;
-        }
-      }
-      if (frameCount < 3) {
-        return;
-      }
-      tab2.linkedBrowser.removeEventListener("load", arguments.callee, true);
+  let value = yield getInputValue(browser, {id: "text"});
+  is(value, "", "value was not restored");
+
+  // Cleanup.
+  gBrowser.removeTab(tab);
+});
 
-      let win = tab2.linkedBrowser.contentWindow;
-      isnot(win.frames[0].document.getElementById("original").value, uniqueValue,
-            "subframes must match URL to get text restored");
-      is(win.frames[0].document.getElementById("original").value, "preserve me",
-         "subframes must match URL to get text restored");
-      is(win.frames[1].document.getElementById("original").value, uniqueValue,
-         "text still gets restored for all other subframes");
-
-      // clean up
-      gBrowser.removeTab(tab2);
-      gBrowser.removeTab(tab);
-
-      finish();
-    }, true);
-  }, true);
+function getState(url) {
+  return JSON.stringify({
+    entries: [{url: URL}],
+    formdata: {url: url, id: {text: "foobar"}}
+  });
 }
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_463205_helper.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>Test for bug 463205 (cross domain)</title>
-
-<input id="original" value="preserve me">
--- a/browser/components/sessionstore/test/browser_463205_sample.html
+++ b/browser/components/sessionstore/test/browser_463205_sample.html
@@ -1,24 +1,7 @@
-<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
-
 <!DOCTYPE html>
 <meta charset="utf-8">
-<title>Test for bug 463205</title>
-
-<body onload="onLoad()">
-<iframe src="data:text/html;charset=utf-8,<input%20id='original'>"></iframe>
-<iframe src="browser_463205_helper.html"></iframe>
-<iframe src="data:text/html;charset=utf-8,mark1"></iframe>
+<title>bug 463205</title>
 
-<script type="application/javascript">
-  function onLoad() {
-    if (frames[2].document.location.href == "data:text/html;charset-utf-8,mark1") {
-      frames[2].document.location = "data:text/html;charset=utf-8,mark2";
-    }
-    else {
-      frames[1].document.location.hash = "#original";
-      frames[0].document.location = "http://mochi.test:8888/browser/" +
-        "browser/components/sessionstore/test/browser_463205_helper.html";
-    }
-  }
-</script>
+<body>
+  <input type="text" id="text" />
 </body>
--- a/browser/components/sessionstore/test/browser_466937.js
+++ b/browser/components/sessionstore/test/browser_466937.js
@@ -1,43 +1,42 @@
-/* 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/. */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  /** Test for Bug 466937 **/
+"use strict";
+
+const URL = ROOT + "browser_466937_sample.html";
 
-  waitForExplicitFinish();
+/**
+ * Bug 466937 - Prevent file stealing with sessionstore.
+ */
+add_task(function test_prevent_file_stealing() {
+  // Add a tab with some file input fields.
+  let tab = gBrowser.addTab(URL);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
 
-  var file = Components.classes["@mozilla.org/file/directory_service;1"]
-             .getService(Components.interfaces.nsIProperties)
-             .get("TmpD", Components.interfaces.nsILocalFile);
+  // Generate a path to a 'secret' file.
+  let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
   file.append("466937_test.file");
-  file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0666);
+  file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
   let testPath = file.path;
 
-  let testURL = "http://mochi.test:8888/browser/" +
-    "browser/components/sessionstore/test/browser_466937_sample.html";
+  // Fill in form values.
+  yield setInputValue(browser, {id: "reverse_thief", value: "/home/user/secret2"});
+  yield setInputValue(browser, {id: "bystander", value: testPath});
 
-  let tab = gBrowser.addTab(testURL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    let doc = tab.linkedBrowser.contentDocument;
-    doc.getElementById("reverse_thief").value = "/home/user/secret2";
-    doc.getElementById("bystander").value = testPath;
+  // Duplicate and check form values.
+  let tab2 = gBrowser.duplicateTab(tab);
+  let browser2 = tab2.linkedBrowser;
+  yield promiseTabRestored(tab2);
 
-    let tab2 = gBrowser.duplicateTab(tab);
-    whenTabRestored(tab2, function() {
-      doc = tab2.linkedBrowser.contentDocument;
-      is(doc.getElementById("thief").value, "",
-         "file path wasn't set to text field value");
-      is(doc.getElementById("reverse_thief").value, "",
-         "text field value wasn't set to full file path");
-      is(doc.getElementById("bystander").value, testPath,
-         "normal case: file path was correctly preserved");
+  let thief = yield getInputValue(browser2, {id: "thief"});
+  is(thief, "", "file path wasn't set to text field value");
+  let reverse_thief = yield getInputValue(browser2, {id: "reverse_thief"});
+  is(reverse_thief, "", "text field value wasn't set to full file path");
+  let bystander = yield getInputValue(browser2, {id: "bystander"});
+  is(bystander, testPath, "normal case: file path was correctly preserved");
 
-      // clean up
-      gBrowser.removeTab(tab2);
-      gBrowser.removeTab(tab);
-
-      finish();
-    });
-  });
-}
+  // Cleanup.
+  gBrowser.removeTab(tab);
+  gBrowser.removeTab(tab2);
+});
--- a/browser/components/sessionstore/test/browser_467409-backslashplosion.js
+++ b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
@@ -1,91 +1,77 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 // Test Summary:
-// 1.  Open about:sessionrestore via setBrowserState where formdata is a JS object, not a string
+// 1.  Open about:sessionrestore where formdata is a JS object, not a string
 // 1a. Check that #sessionData on the page is readable after JSON.parse (skipped, checking formdata is sufficient)
 // 1b. Check that there are no backslashes in the formdata
-// 1c. Check that formdata (via getBrowserState) doesn't require JSON.parse
+// 1c. Check that formdata doesn't require JSON.parse
 //
-// 2.  Use the current state (currently about:sessionrestore with data) and then open than in a new instance of about:sessionrestore
+// 2.  Use the current state (currently about:sessionrestore with data) and then open that in a new instance of about:sessionrestore
 // 2a. Check that there are no backslashes in the formdata
-// 2b. Check that formdata (via getBrowserState) doesn't require JSON.parse
+// 2b. Check that formdata doesn't require JSON.parse
 //
 // 3.  [backwards compat] Use a stringified state as formdata when opening about:sessionrestore
 // 3a. Make sure there are nodes in the tree on about:sessionrestore (skipped, checking formdata is sufficient)
 // 3b. Check that there are no backslashes in the formdata
-// 3c. Check that formdata (via getBrowserState) doesn't require JSON.parse
-
-function test() {
-  waitForExplicitFinish();
-  ignoreAllUncaughtExceptions();
-
-  let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
-  let crashState = { windows: [{ tabs: [{ entries: [{ url: "about:mozilla" }] }]}]};
-
-  let pagedata = { url: "about:sessionrestore",
-                   formdata: { id: {"sessionData": crashState } } };
-  let state = { windows: [{ tabs: [{ entries: [pagedata] }] }] };
-
-  // test1 calls test2 calls test3 calls finish
-  test1(state);
-
-
-  function test1(aState) {
-    waitForBrowserState(aState, function() {
-      checkState("test1", test2);
-    });
-  }
-
-  function test2(aState) {
-    let pagedata2 = { url: "about:sessionrestore",
-                      formdata: { id: { "sessionData": aState } } };
-    let state2 = { windows: [{ tabs: [{ entries: [pagedata2] }] }] };
-
-    waitForBrowserState(state2, function() {
-      checkState("test2", test3);
-    });
-  }
+// 3c. Check that formdata doesn't require JSON.parse
 
-  function test3(aState) {
-    let pagedata3 = { url: "about:sessionrestore",
-                      formdata: { id: { "sessionData": JSON.stringify(crashState) } } };
-    let state3 = { windows: [{ tabs: [{ entries: [pagedata3] }] }] };
-    waitForBrowserState(state3, function() {
-      // In theory we should do inspection of the treeview on about:sessionrestore,
-      // but we don't actually need to. If we fail tests in checkState then
-      // about:sessionrestore won't be able to turn the form value into a usable page.
-      checkState("test3", function() waitForBrowserState(blankState, finish));
-    });
-  }
-
-  function checkState(testName, callback) {
-    let curState = JSON.parse(ss.getBrowserState());
-    let formdata = curState.windows[0].tabs[0].entries[0].formdata;
-
-    ok(formdata.id["sessionData"], testName + ": we have form data for about:sessionrestore");
+const CRASH_STATE = {windows: [{tabs: [{entries: [{url: "about:mozilla" }]}]}]};
+const STATE = {entries: [createEntry(CRASH_STATE)]};
+const STATE2 = {entries: [createEntry({windows: [{tabs: [STATE]}]})]};
+const STATE3 = {entries: [createEntry(JSON.stringify(CRASH_STATE))]};
 
-    let sessionData_raw = JSON.stringify(formdata.id["sessionData"]);
-    ok(!/\\/.test(sessionData_raw), testName + ": #sessionData contains no backslashes");
-    info(sessionData_raw);
-
-    let gotError = false;
-    try {
-      JSON.parse(formdata.id["sessionData"]);
-    }
-    catch (e) {
-      info(testName + ": got error: " + e);
-      gotError = true;
-    }
-    ok(gotError, testName + ": attempting to JSON.parse form data threw error");
-
-    // Panorama sticks JSON into extData, which we stringify causing the
-    // naive backslash check to fail. extData doesn't matter in the grand
-    // scheme here, so we'll delete the extData so doesn't end up in future states.
-    delete curState.windows[0].extData;
-    delete curState.windows[0].tabs[0].extData;
-    callback(curState);
-  }
-
+function createEntry(sessionData) {
+  return {
+    url: "about:sessionrestore",
+    formdata: {id: {sessionData: sessionData}}
+  };
 }
 
+add_task(function test_nested_about_sessionrestore() {
+  // Prepare a blank tab.
+  let tab = gBrowser.addTab("about:blank");
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
+
+  // test 1
+  ss.setTabState(tab, JSON.stringify(STATE));
+  yield promiseTabRestored(tab);
+  checkState("test1", tab);
+
+  // test 2
+  ss.setTabState(tab, JSON.stringify(STATE2));
+  yield promiseTabRestored(tab);
+  checkState("test2", tab);
+
+  // test 3
+  ss.setTabState(tab, JSON.stringify(STATE3));
+  yield promiseTabRestored(tab);
+  checkState("test3", tab);
+
+  // Cleanup.
+  gBrowser.removeTab(tab);
+});
+
+function checkState(prefix, tab) {
+  // Flush and query tab state.
+  SyncHandlers.get(tab.linkedBrowser).flush();
+  let {formdata} = JSON.parse(ss.getTabState(tab));
+
+  ok(formdata.id["sessionData"], prefix + ": we have form data for about:sessionrestore");
+
+  let sessionData_raw = JSON.stringify(formdata.id["sessionData"]);
+  ok(!/\\/.test(sessionData_raw), prefix + ": #sessionData contains no backslashes");
+  info(sessionData_raw);
+
+  let gotError = false;
+  try {
+    JSON.parse(formdata.id["sessionData"]);
+  } catch (e) {
+    info(prefix + ": got error: " + e);
+    gotError = true;
+  }
+  ok(gotError, prefix + ": attempting to JSON.parse form data threw error");
+}
--- a/browser/components/sessionstore/test/browser_485482.js
+++ b/browser/components/sessionstore/test/browser_485482.js
@@ -1,34 +1,37 @@
-/* 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/. */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_485482_sample.html";
 
-function test() {
-  /** Test for Bug 485482 **/
-
-  waitForExplicitFinish();
-
-  let uniqueValue = Math.random();
+/**
+ * Bug 485482 - Make sure that we produce valid XPath expressions even for very
+ * weird HTML documents.
+ */
+add_task(function test_xpath_exp_for_strange_documents() {
+  // Load a page with weird tag names.
+  let tab = gBrowser.addTab(URL);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
 
-  let rootDir = getRootDirectory(gTestPath);
-  let testURL = rootDir + "browser_485482_sample.html";
-  let tab = gBrowser.addTab(testURL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    let doc = tab.linkedBrowser.contentDocument;
-    doc.querySelector("input[type=text]").value = uniqueValue;
-    doc.querySelector("input[type=checkbox]").checked = true;
+  // Fill in some values.
+  let uniqueValue = Math.random();
+  yield setInputValue(browser, {selector: "input[type=text]", value: uniqueValue});
+  yield setInputChecked(browser, {selector: "input[type=checkbox]", checked: true});
+
+  // Duplicate the tab.
+  let tab2 = gBrowser.duplicateTab(tab);
+  let browser2 = tab2.linkedBrowser;
+  yield promiseTabRestored(tab2);
 
-    let tab2 = gBrowser.duplicateTab(tab);
-    whenTabRestored(tab2, function() {
-      doc = tab2.linkedBrowser.contentDocument;
-      is(doc.querySelector("input[type=text]").value, uniqueValue,
-         "generated XPath expression was valid");
-      ok(doc.querySelector("input[type=checkbox]").checked,
-         "generated XPath expression was valid");
+  // Check that we generated valid XPath expressions to restore form values.
+  let text = yield getInputValue(browser2, {selector: "input[type=text]"});
+  is(text, uniqueValue, "generated XPath expression was valid");
+  let checkbox = yield getInputChecked(browser2, {selector: "input[type=checkbox]"});
+  ok(checkbox, "generated XPath expression was valid");
 
-      // clean up
-      gBrowser.removeTab(tab2);
-      gBrowser.removeTab(tab);
-      finish();
-    });
-  });
-}
+  // Cleanup.
+  gBrowser.removeTab(tab2);
+  gBrowser.removeTab(tab);
+});
--- a/browser/components/sessionstore/test/browser_662743.js
+++ b/browser/components/sessionstore/test/browser_662743.js
@@ -1,127 +1,109 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 // This tests that session restore component does restore the right <select> option.
 // Session store should not rely only on previous user's selectedIndex, it should
 // check its value as well.
 
 function test() {
   /** Tests selected options **/
   waitForExplicitFinish();
 
   let testTabCount = 0;
   let formData = [
   // default case
     { },
-  // old format
-    { "#select_id" : 0 },
-    { "#select_id" : 2 },
-    // invalid index
-    { "#select_id" : 8 },
-    { "/xhtml:html/xhtml:body/xhtml:select" : 5},
-    { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : 6},
 
   // new format
     // index doesn't match value (testing an option in between (two))
     { id:{ "select_id": {"selectedIndex":0,"value":"val2"} } },
     // index doesn't match value (testing an invalid value)
     { id:{ "select_id": {"selectedIndex":4,"value":"val8"} } },
     // index doesn't match value (testing an invalid index)
     { id:{ "select_id": {"selectedIndex":8,"value":"val5"} } },
     // index and value match position zero
     { id:{ "select_id": {"selectedIndex":0,"value":"val0"} }, xpath: {} },
     // index doesn't match value (testing the last option (seven))
     { id:{},"xpath":{ "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']": {"selectedIndex":1,"value":"val7"} } },
     // index and value match the default option "selectedIndex":3,"value":"val3"
     { xpath: { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : {"selectedIndex":3,"value":"val3"} } },
     // index matches default option however it doesn't match value
     { id:{ "select_id": {"selectedIndex":3,"value":"val4"} } },
-
-  // combinations
-    { "#select_id" : 3, id:{ "select_id": {"selectedIndex":1,"value":"val1"} } },
-    { "#select_id" : 5, xpath: { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : {"selectedIndex":4,"value":"val4"} } },
-    { "/xhtml:html/xhtml:body/xhtml:select" : 5, id:{ "select_id": {"selectedIndex":1,"value":"val1"} }},
-    { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : 2, xpath: { "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']" : {"selectedIndex":7,"value":"val7"} } }
   ];
 
   let expectedValues = [
-    [ "val3"], // default value
-    [ "val0"],
-    [ "val2"],
-    [ "val3"], // default value (invalid index)
-    [ "val5"],
-    [ "val6"],
-    [ "val2"],
-    [ "val3"], // default value (invalid value)
-    [ "val5"], // value is still valid (even it has an invalid index)
-    [ "val0"],
-    [ "val7"],
-    [ "val3"],
-    [ "val4"],
-    [ "val1"],
-    [ "val4"],
-    [ "val1"],
-    [ "val7"]
+    null,   // default value
+    "val2",
+    null,   // default value (invalid value)
+    "val5", // value is still valid (even it has an invalid index)
+    "val0",
+    "val7",
+    null,
+    "val4",
   ];
   let callback = function() {
     testTabCount--;
     if (testTabCount == 0) {
       finish();
     }
   };
 
   for (let i = 0; i < formData.length; i++) {
     testTabCount++;
     testTabRestoreData(formData[i], expectedValues[i], callback);
   }
 }
 
-function testTabRestoreData(aFormData, aExpectedValues, aCallback) {
+function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
   let testURL =
     getRootDirectory(gTestPath) + "browser_662743_sample.html";
   let tab = gBrowser.addTab(testURL);
   let tabState = { entries: [{ url: testURL, formdata: aFormData}] };
 
   whenBrowserLoaded(tab.linkedBrowser, function() {
     ss.setTabState(tab, JSON.stringify(tabState));
 
     whenTabRestored(tab, function() {
       let doc = tab.linkedBrowser.contentDocument;
       let select = doc.getElementById("select_id");
       let value = select.options[select.selectedIndex].value;
 
+      // Flush to make sure we have the latest form data.
+      SyncHandlers.get(tab.linkedBrowser).flush();
+      let restoredTabState = JSON.parse(ss.getTabState(tab));
+
+      // If aExpectedValue=null we don't expect any form data to be collected.
+      if (!aExpectedValue) {
+        ok(!restoredTabState.hasOwnProperty("formdata"), "no formdata collected");
+        gBrowser.removeTab(tab);
+        aCallback();
+        return;
+      }
+
       // test select options values
-      is(value, aExpectedValues[0],
+      is(value, aExpectedValue,
         "Select Option by selectedIndex &/or value has been restored correctly");
 
+      let restoredFormData = restoredTabState.formdata;
+      let selectIdFormData = restoredFormData.id.select_id;
+      let value = restoredFormData.id.select_id.value;
+
+      // test format
+      ok("id" in restoredFormData || "xpath" in restoredFormData,
+        "FormData format is valid");
+      // test format
+      ok("selectedIndex" in selectIdFormData && "value" in selectIdFormData,
+        "select format is valid");
+       // test set collection values
+      is(value, aExpectedValue,
+        "Collection has been saved correctly");
+
       // clean up
       gBrowser.removeTab(tab);
 
       aCallback();
     });
-
-    tab.addEventListener("TabClose", function(aEvent) {
-      tab.removeEventListener("TabClose", arguments.callee);
-      let restoredTabState = JSON.parse(ss.getTabState(tab));
-      let restoredFormData = restoredTabState.entries[0].formdata;
-      let selectIdFormData = restoredFormData.id.select_id;
-      let value = restoredFormData.id.select_id.value;
-
-      // test format
-      ok("id" in restoredFormData && "xpath" in restoredFormData,
-        "FormData format is valid");
-      // validate that there are no old keys
-      is(Object.keys(restoredFormData).length, 2,
-        "FormData key length is valid");
-      // test format
-      ok("selectedIndex" in selectIdFormData && "value" in selectIdFormData,
-        "select format is valid");
-      // validate that there are no old keys
-      is(Object.keys(selectIdFormData).length, 2,
-        "select_id length is valid");
-       // test set collection values
-      is(value, aExpectedValues[0],
-        "Collection has been saved correctly");
-    });
   });
 }
--- a/browser/components/sessionstore/test/browser_665702-state_session.js
+++ b/browser/components/sessionstore/test/browser_665702-state_session.js
@@ -13,12 +13,12 @@ function compareArray(a, b) {
   return true;
 }
 
 function test() {
   let currentState = JSON.parse(ss.getBrowserState());
   ok(currentState.session, "session data returned by getBrowserState");
 
   let keys = Object.keys(currentState.session);
-  let expectedKeys = ["state", "lastUpdate", "startTime", "recentCrashes"];
+  let expectedKeys = ["lastUpdate", "startTime", "recentCrashes"];
   ok(compareArray(keys.sort(), expectedKeys.sort()),
      "session object from getBrowserState has correct keys");
 }
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_916390_form_data_loss.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-let tmp;
-Cu.import("resource:///modules/sessionstore/TabStateCache.jsm", tmp);
-let {TabStateCache} = tmp;
-
-const URL = "http://mochi.test:8888/browser/" +
-            "browser/components/sessionstore/test/browser_916390_sample.html";
-
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
-  // Create a tab with some form fields.
-  let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
-  let browser = gBrowser.selectedBrowser;
-  yield waitForLoad(browser);
-
-  // Modify the text input field's state.
-  browser.contentDocument.getElementById("txt").focus();
-  EventUtils.synthesizeKey("m", {});
-  yield waitForInput();
-
-  // Check that we'll save the form data state correctly.
-  let state = JSON.parse(ss.getBrowserState());
-  let {formdata} = state.windows[0].tabs[1].entries[0];
-  is(formdata.id.txt, "m", "txt's value is correct");
-
-  // Change the number of session history entries to invalidate the cache.
-  browser.loadURI(URL + "#");
-  TabStateCache.delete(browser);
-
-  // Check that we'll save the form data state correctly.
-  let state = JSON.parse(ss.getBrowserState());
-  let {formdata} = state.windows[0].tabs[1].entries[1];
-  is(formdata.id.txt, "m", "txt's value is correct");
-
-  // Clean up.
-  gBrowser.removeTab(tab);
-}
-
-function waitForLoad(aElement) {
-  aElement.addEventListener("load", function onLoad() {
-    aElement.removeEventListener("load", onLoad, true);
-    executeSoon(next);
-  }, true);
-}
-
-function waitForInput() {
-  let mm = gBrowser.selectedBrowser.messageManager;
-
-  mm.addMessageListener("SessionStore:input", function onInput() {
-    mm.removeMessageListener("SessionStore:input", onInput);
-    executeSoon(next);
-  });
-}
-
-function waitForStorageChange() {
-  let mm = gBrowser.selectedBrowser.messageManager;
-
-  mm.addMessageListener("SessionStore:MozStorageChanged", function onChanged() {
-    mm.removeMessageListener("SessionStore:MozStorageChanged", onChanged);
-    executeSoon(next);
-  });
-}
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_916390_sample.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE HTML>
-<html dir="ltr" xml:lang="en-US" lang="en-US">
-  <head>
-    <meta charset="utf-8">
-    <title>bug 916390</title>
-  </head>
-  <body>
-    <input id="txt" />
-  </body>
-</html>
--- a/browser/components/sessionstore/test/browser_broadcast.js
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -133,22 +133,17 @@ add_task(function flush_on_tabclose_racy
 
   let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://example.com"].test, "on-tab-close-racy",
     "sessionStorage data has been merged correctly to prevent data loss");
 });
 
 function promiseNewWindow() {
   let deferred = Promise.defer();
-
-  whenNewWindowLoaded({private: false}, function (win) {
-    win.messageManager.loadFrameScript(FRAME_SCRIPT, true);
-    deferred.resolve(win);
-  });
-
+  whenNewWindowLoaded({private: false}, deferred.resolve);
   return deferred.promise;
 }
 
 function closeWindow(win) {
   let deferred = Promise.defer();
   let outerID = win.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils)
                    .outerWindowID;
--- a/browser/components/sessionstore/test/browser_form_restore_events.js
+++ b/browser/components/sessionstore/test/browser_form_restore_events.js
@@ -1,65 +1,63 @@
-/* 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/. */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  /** Originally a test for Bug 476161, but then expanded to include all input types in bug 640136 **/
+"use strict";
 
-  waitForExplicitFinish();
-
-  let file = Components.classes["@mozilla.org/file/directory_service;1"]
-             .getService(Components.interfaces.nsIProperties)
-             .get("TmpD", Components.interfaces.nsIFile);
+const URL = ROOT + "browser_form_restore_events_sample.html";
 
-  let testURL = "http://mochi.test:8888/browser/" +
-    "browser/components/sessionstore/test/browser_form_restore_events_sample.html";
-  let tab = gBrowser.addTab(testURL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    let doc = tab.linkedBrowser.contentDocument;
+/**
+ * Originally a test for Bug 476161, but then expanded to include all input
+ * types in bug 640136.
+ */
+add_task(function () {
+  // Load a page with some form elements.
+  let tab = gBrowser.addTab(URL);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
 
-    // text fields
-    doc.getElementById("modify01").value += Math.random();
-    doc.getElementById("modify02").value += " " + Date.now();
+  // text fields
+  yield setInputValue(browser, {id: "modify01", value: Math.random()});
+  yield setInputValue(browser, {id: "modify02", value: Date.now()});